use core::iter::once;
use core::mem;
use core::num::NonZero;
use core::panic::AssertUnwindSafe;
use core::sync::atomic::{AtomicUsize, Ordering};
use std::ffi::OsStr;
use std::panic::{catch_unwind, resume_unwind};
use std::sync::{Barrier, Mutex, OnceLock, PoisonError};
use std::thread;
use camino::{Utf8Path, Utf8PathBuf};
use cargo_metadata::{CargoOpt, Metadata, MetadataCommand, Target, TargetKind};
use syn::visit::{self, Visit};
use walkdir::WalkDir;
use super::compile_fail::{CompileFailTarget, compile_fail_targets};
use super::glob::{Glob, normalize_separators};
use super::killers::Killers;
use super::shard::shard_of;
use super::{Diff, Plan, TargetFile, modules};
use crate::cfg::{CfgSet, Cfgs, features};
use crate::commands::{FeatureArgs, SelectArgs};
use crate::error::{Error, error};
use crate::exec::CargoOptions;
use crate::model::{Interner, Mutant, MutantId, Outcome};
use crate::ops::collect;
use crate::ops::registry::Selection;
use crate::parse::SourceFile;
use crate::{HashMap, HashSet, Result, suppress};
#[cfg(test)]
pub fn plan(args: &SelectArgs, selection: &Selection, shard: Option<(u32, u32)>, notify: &mut impl FnMut(&str)) -> Result<Plan> {
let survey = Survey::new(args, shard)?;
plan_survey(survey, selection, notify)
}
pub(crate) fn plan_for_build(
args: &SelectArgs,
selection: &Selection,
shard: Option<(u32, u32)>,
cargo: &CargoOptions,
notify: &mut impl FnMut(&str),
) -> Result<Plan> {
let survey = Survey::for_build(args, shard, cargo)?;
plan_survey(survey, selection, notify)
}
fn plan_survey(survey: Survey, selection: &Selection, notify: &mut impl FnMut(&str)) -> Result<Plan> {
notify(&format!("{} for mutants", crate::report::quantity(survey.files.len(), "file")));
let mut ordinals = 0;
let scanned = survey.scan(None, selection, &mut ordinals)?;
report_by_package(&survey.files, &scanned.mutants, notify);
Ok(survey.into_plan(scanned))
}
#[derive(Debug)]
pub struct Survey {
pub root: Utf8PathBuf,
pub files: Vec<TargetFile>,
declaration_files: HashMap<String, Vec<Utf8PathBuf>>,
pub reach: HashMap<String, HashSet<String>>,
pub selected: Vec<String>,
pub tests: Vec<String>,
pub compile_fail: Vec<CompileFailTarget>,
by_package: HashMap<String, Vec<usize>>,
roots: HashMap<String, Vec<Utf8PathBuf>>,
specs: HashMap<String, (Utf8PathBuf, String)>,
cfgs: Cfgs,
pub(super) source_dirs: Vec<Utf8PathBuf>,
external_inputs: Vec<Utf8PathBuf>,
untracked_build_script_inputs: bool,
diff: Option<Diff>,
shard: Option<(u32, u32)>,
settled: HashMap<MutantId, Outcome>,
exclude_trait_impls: Vec<String>,
}
#[derive(Debug, Default)]
pub struct Scanned {
pub mutants: Vec<Mutant>,
pub suppressed: usize,
pub idle: Vec<suppress::Idle>,
pub sharded_out: usize,
pub settled_out: usize,
pub digests: HashMap<Utf8PathBuf, String>,
pub skipped: Vec<String>,
}
impl Survey {
#[cfg(test)]
pub fn new(args: &SelectArgs, shard: Option<(u32, u32)>) -> Result<Self> {
let config = crate::config::Config::resolve(args)?;
let cargo = config.cargo_options();
Self::for_build(args, shard, &cargo)
}
pub fn for_build(args: &SelectArgs, shard: Option<(u32, u32)>, cargo: &CargoOptions) -> Result<Self> {
Self::for_build_with_cache_inputs(args, shard, cargo, false)
}
#[expect(clippy::too_many_lines, reason = "workspace selection is one ordered metadata pass")]
pub(crate) fn for_build_with_cache_inputs(
args: &SelectArgs,
shard: Option<(u32, u32)>,
cargo: &CargoOptions,
cache_inputs: bool,
) -> Result<Self> {
cargo.validate()?;
let features = features::from_extra(&args.features, &cargo.extra);
let metadata = load_metadata(&args.dir, &features)?;
let root = Utf8PathBuf::from(metadata.workspace_root.as_str());
let external_inputs = if cache_inputs {
external_path_inputs(&args.dir, &features, &root)?
} else {
ExternalPathInputs::default()
};
let enabled = features::enabled(&metadata, &features);
let mut files: Vec<TargetFile> = Vec::new();
let mut seen: HashSet<Utf8PathBuf> = HashSet::default();
let mut exclusion_files: Vec<TargetFile> = Vec::new();
let mut exclusion_seen: HashSet<(Utf8PathBuf, String)> = HashSet::default();
let mut declaration_files: HashMap<String, Vec<Utf8PathBuf>> = HashMap::default();
let mut declaration_seen: HashSet<Utf8PathBuf> = HashSet::default();
let mut roots: HashMap<String, Vec<Utf8PathBuf>> = HashMap::default();
let mut specs: HashMap<String, (Utf8PathBuf, String)> = HashMap::default();
let mut source_dirs: HashSet<Utf8PathBuf> = HashSet::default();
let mut diff = args.in_diff.as_ref().map(|path| Diff::read(path)).transpose()?;
let patterns = FilePatterns::new(args);
let checking_patterns = !args.files.is_empty() || !args.exclude_files.is_empty();
let checking_exclusions = !args.exclude_trait_impls.is_empty();
let collecting_walked = checking_patterns || diff.is_some();
let mut walked: Vec<Utf8PathBuf> = Vec::new();
if let Some(named) = unknown_packages(&metadata, args) {
return Err(error!("no package named `{named}` in this workspace").usage());
}
let selected = selected_packages(&metadata, args);
for package in metadata.workspace_packages() {
let mutating = selected.contains(package.name.as_str());
if let Some(directory) = Utf8Path::new(package.manifest_path.as_str()).parent() {
let relative = directory
.strip_prefix(&root)
.unwrap_or_else(|_outside| Utf8Path::new(""))
.to_owned();
let _replaced = specs.insert(package.name.to_string(), (relative, package.version.to_string()));
}
if !mutating && !checking_patterns && !checking_exclusions {
continue;
}
for target in &package.targets {
if let Some(directory) = Utf8Path::new(target.src_path.as_str()).parent() {
let _added = source_dirs.insert(directory.to_owned());
}
if !is_mutable_target(target, enabled.get(package.name.as_str())) {
continue;
}
let source_root = Utf8Path::new(target.src_path.as_str());
if mutating {
roots.entry(package.name.to_string()).or_default().push(source_root.to_owned());
}
let Some(directory) = source_root.parent() else {
continue;
};
for absolute in walk_rust_files(directory)? {
let relative = placed_under(&root, &absolute, &package.name)?;
if checking_exclusions && exclusion_seen.insert((absolute.clone(), package.name.to_string())) {
exclusion_files.push(TargetFile {
path: relative.clone(),
absolute: absolute.clone(),
package: package.name.to_string(),
});
}
if collecting_walked {
walked.push(relative.clone());
}
if !mutating {
continue;
}
if declaration_seen.insert(absolute.clone()) {
declaration_files
.entry(package.name.to_string())
.or_default()
.push(absolute.clone());
}
if !patterns.includes(&relative) {
continue;
}
if !seen.insert(absolute.clone()) {
continue;
}
files.push(TargetFile {
path: relative,
absolute,
package: package.name.to_string(),
});
}
}
}
files.sort_by(|left, right| left.path.cmp(&right.path));
for paths in declaration_files.values_mut() {
paths.sort();
}
exclusion_files.sort_by(|left, right| left.path.cmp(&right.path));
let cfgs = configuration(&root, cargo, &enabled);
validate_trait_exclusions(&args.exclude_trait_impls, &exclusion_files, &cfgs)?;
if let Some(diff) = diff.as_mut() {
diff.resolve(&root, &walked)?;
files.retain(|file| diff.touches_file(&file.path));
}
if let Some(pattern) = patterns.unmatched(&walked) {
return Err(error!(
"no source file matches `{pattern}`; patterns are relative to the workspace root and use `/` on every platform"
)
.usage());
}
let mut by_package: HashMap<String, Vec<usize>> = HashMap::default();
for (position, file) in files.iter().enumerate() {
by_package.entry(file.package.clone()).or_default().push(position);
}
Ok(Self {
root,
files,
declaration_files,
by_package,
reach: reachable(&metadata),
selected: sorted(selected),
tests: test_targets(&metadata),
compile_fail: compile_fail_targets(&metadata),
roots,
specs,
cfgs,
diff,
shard,
settled: HashMap::default(),
exclude_trait_impls: args.exclude_trait_impls.clone(),
source_dirs: sorted(source_dirs),
external_inputs: external_inputs.roots,
untracked_build_script_inputs: external_inputs.has_build_scripts,
})
}
#[must_use]
pub fn killers(&self) -> Killers {
let mut files = Vec::new();
let mut complete = true;
for directory in &self.source_dirs {
match walk_rust_files(directory) {
Ok(found) => files.extend(found),
Err(_failure) => complete = false,
}
}
Killers::scan_complete(&files, complete)
}
#[must_use]
pub(crate) fn external_inputs(&self) -> &[Utf8PathBuf] {
&self.external_inputs
}
#[must_use]
pub(crate) const fn has_untracked_build_script_inputs(&self) -> bool {
self.untracked_build_script_inputs
}
pub fn settle(&mut self, settled: HashMap<MutantId, Outcome>) {
self.settled = settled;
}
#[must_use]
pub fn packages(&self) -> Vec<String> {
let mut order: Vec<String> = Vec::new();
let mut seen: HashSet<&str> = HashSet::default();
for file in &self.files {
if seen.insert(file.package.as_str()) {
order.push(file.package.clone());
}
}
order
}
#[must_use]
pub fn skeleton(&self) -> Plan {
Plan {
root: self.root.clone(),
files: self.files.clone(),
mutants: Vec::new(),
suppressed: 0,
idle: Vec::new(),
sharded_out: 0,
settled_out: 0,
skipped: Vec::new(),
digests: HashMap::default(),
reach: self.reach.clone(),
specs: self.specs.clone(),
}
}
pub fn scan(&self, package: Option<&str>, selection: &Selection, ordinals: &mut u32) -> Result<Scanned> {
let files: Vec<&TargetFile> = package.map_or_else(
|| self.files.iter().collect(),
|wanted| {
self.by_package
.get(wanted)
.map(|positions| positions.iter().filter_map(|&position| self.files.get(position)).collect())
.unwrap_or_default()
},
);
let roots: Vec<Utf8PathBuf> = package.map_or_else(
|| self.roots.values().flatten().cloned().collect(),
|wanted| self.roots.get(wanted).cloned().unwrap_or_default(),
);
let declaration_files: Vec<(&Utf8Path, &CfgSet)> = package.map_or_else(
|| {
self.declaration_files
.iter()
.flat_map(|(package, paths)| {
let cfg = self.cfgs.for_package(package);
paths.iter().map(move |path| (path.as_path(), cfg))
})
.collect()
},
|wanted| {
let cfg = self.cfgs.for_package(wanted);
self.declaration_files
.get(wanted)
.map(|paths| paths.iter().map(|path| (path.as_path(), cfg)).collect())
.unwrap_or_default()
},
);
let Scan {
mut mutants,
mut suppressed,
idle,
skipped,
digests,
} = scan(&files, &declaration_files, &roots, selection, &self.cfgs)?;
if !self.exclude_trait_impls.is_empty() {
let mut excluded_suppressed = 0_usize;
mutants.retain(|mutant| {
let excluded = mutant
.trait_impl
.as_ref()
.is_some_and(|name| self.exclude_trait_impls.iter().any(|excluded| excluded == name.as_ref()));
if excluded && mutant.outcome == Outcome::Ignored {
excluded_suppressed = excluded_suppressed.saturating_add(1);
}
!excluded
});
suppressed = suppressed.saturating_sub(excluded_suppressed);
}
if let Some(diff) = self.diff.as_ref() {
mutants.retain(|mutant| {
let start = u32::try_from(mutant.line).unwrap_or(u32::MAX);
let end = u32::try_from(mutant.end_line).unwrap_or(u32::MAX);
diff.touches(&mutant.file, start, end)
});
}
let mut settled_out = 0_usize;
if !self.settled.is_empty() {
for mutant in &mut mutants {
if let Some(outcome) = self.settled.get(&mutant.id).copied() {
mutant.outcome = outcome;
settled_out = settled_out.saturating_add(1);
}
}
}
let is_live = |mutant: &Mutant| mutant.outcome == Outcome::Pending;
let before = mutants.iter().filter(|mutant| is_live(mutant)).count();
if let Some((count, index)) = self.shard {
mutants.retain(|mutant| !is_live(mutant) || shard_of(&mutant.id, count) == index);
}
Interner::default().share(&mut mutants);
let mut live = 0_usize;
for mutant in &mut mutants {
if is_live(mutant) {
live = live.saturating_add(1);
*ordinals = ordinals.saturating_add(1);
mutant.ordinal = *ordinals;
}
}
Ok(Scanned {
mutants,
suppressed,
idle,
sharded_out: before - live,
settled_out,
skipped,
digests,
})
}
#[must_use]
pub fn into_plan(self, scanned: Scanned) -> Plan {
let mut plan = Plan {
root: self.root,
files: self.files,
mutants: Vec::new(),
suppressed: 0,
idle: Vec::new(),
sharded_out: 0,
settled_out: 0,
skipped: Vec::new(),
digests: HashMap::default(),
reach: self.reach,
specs: self.specs,
};
plan.absorb(scanned);
plan.sort();
plan
}
}
fn unmatched_exclusion(index: usize, exclusion: &str) -> Error {
error!(
"exclude-trait-impls entry {} (`{}`) matched no trait implementations; check the lexical terminal trait name",
index + 1,
exclusion
)
.usage()
}
fn report_by_package(files: &[TargetFile], mutants: &[Mutant], notify: &mut impl FnMut(&str)) {
let mut order: Vec<&str> = Vec::new();
let mut counts: HashMap<&str, (usize, usize)> = HashMap::default();
for file in files {
let entry = counts.entry(file.package.as_str()).or_insert_with(|| {
order.push(file.package.as_str());
(0, 0)
});
entry.0 += 1;
}
for mutant in mutants {
if let Some(entry) = counts.get_mut(&*mutant.package) {
entry.1 += 1;
}
}
for package in order {
let (files, mutants) = counts.get(package).copied().unwrap_or((0, 0));
notify(&format!(
"{package}, {} in {}",
crate::report::quantity(mutants, "mutant"),
crate::report::quantity(files, "file")
));
}
}
fn mutate(file: &TargetFile, source: &SourceFile, selection: &Selection, cfgs: &Cfgs, defaults: &collect::Defaults) -> Result<Parsed> {
let cfg = cfgs.for_package(&file.package);
let declared = modules::declarations(&file.absolute, source.ast(), cfg);
let candidates = collect::check_stated_and_collect_with(source, selection, cfg, defaults)?;
let mut found = collect::into_mutants(source, &file.package, candidates);
let directives = suppress::directives_for(source, cfg)?;
let suppressed = suppress::suppress(&mut found, &directives);
let idle = suppress::idle(&file.path, &found, &directives, selection);
Ok(Parsed {
mutants: found,
suppressed,
idle,
declared,
digest: crate::discover::digest(source.text().as_bytes()),
})
}
struct TraitImplementations<'cfg> {
cfg: &'cfg CfgSet,
names: HashSet<String>,
}
impl TraitImplementations<'_> {
fn collect(file: &syn::File, cfg: &CfgSet) -> HashSet<String> {
let mut collector = TraitImplementations {
cfg,
names: HashSet::default(),
};
collector.visit_file(file);
collector.names
}
}
fn validate_trait_exclusions(exclusions: &[String], files: &[TargetFile], cfgs: &Cfgs) -> Result<()> {
if exclusions.is_empty() {
return Ok(());
}
let mut implementations: HashSet<String> = HashSet::default();
for file in files {
match SourceFile::read(&file.absolute) {
Ok(source) => implementations.extend(TraitImplementations::collect(source.ast(), cfgs.for_package(&file.package))),
Err(error) if error.is_skippable() => {}
Err(error) => return Err(error.into()),
}
if exclusions.iter().all(|exclusion| implementations.contains(exclusion.as_str())) {
return Ok(());
}
}
if let Some((index, exclusion)) = exclusions
.iter()
.enumerate()
.find(|(_index, exclusion)| !implementations.contains(exclusion.as_str()))
{
return Err(unmatched_exclusion(index, exclusion));
}
Ok(())
}
impl<'ast> Visit<'ast> for TraitImplementations<'_> {
#[expect(
clippy::renamed_function_params,
reason = "`item` identifies the syntax node more clearly than the trait declaration's `i`"
)]
fn visit_item(&mut self, item: &'ast syn::Item) {
if !self.cfg.holds_for(item_attributes(item)) {
return;
}
if let syn::Item::Impl(item) = item
&& let Some((path, _for_token)) = &item.trait_
&& let Some(segment) = path.segments.last()
{
let _new = self.names.insert(segment.ident.to_string());
}
visit::visit_item(self, item);
}
}
fn item_attributes(item: &syn::Item) -> &[syn::Attribute] {
match item {
syn::Item::Const(node) => &node.attrs,
syn::Item::Enum(node) => &node.attrs,
syn::Item::ExternCrate(node) => &node.attrs,
syn::Item::Fn(node) => &node.attrs,
syn::Item::ForeignMod(node) => &node.attrs,
syn::Item::Impl(node) => &node.attrs,
syn::Item::Macro(node) => &node.attrs,
syn::Item::Mod(node) => &node.attrs,
syn::Item::Static(node) => &node.attrs,
syn::Item::Struct(node) => &node.attrs,
syn::Item::Trait(node) => &node.attrs,
syn::Item::TraitAlias(node) => &node.attrs,
syn::Item::Type(node) => &node.attrs,
syn::Item::Union(node) => &node.attrs,
syn::Item::Use(node) => &node.attrs,
_ => &[],
}
}
fn scan(
files: &[&TargetFile],
declaration_files: &[(&Utf8Path, &CfgSet)],
roots: &[Utf8PathBuf],
selection: &Selection,
cfgs: &Cfgs,
) -> Result<Scan> {
let workers = thread::available_parallelism().map_or(1, NonZero::get).min(files.len().max(1));
let shared = Shared {
next: AtomicUsize::new(0),
partials: Mutex::new(Vec::new()),
skipped: Mutex::new(Vec::new()),
barrier: Barrier::new(workers),
defaults: OnceLock::new(),
};
let mut collected: Vec<(usize, Parsed)> = thread::scope(|scope| {
let handles: Vec<_> = (0..workers)
.map(|_worker| {
let shared = &shared;
scope.spawn(move || work(files, shared, selection, cfgs))
})
.collect();
let mut collected = Vec::new();
let mut failure: Option<(usize, Error)> = None;
for handle in handles {
match handle.join().unwrap_or_else(|payload| resume_unwind(payload)) {
Ok(mine) => collected.extend(mine),
Err((at, error)) => {
if failure.as_ref().is_none_or(|(seen, _)| at < *seen) {
failure = Some((at, error));
}
}
}
}
match failure {
Some((_at, error)) => Err(error),
None => Ok(collected),
}
})?;
collected.sort_by_key(|(index, _parsed)| *index);
let mut declared: Vec<(Utf8PathBuf, Vec<modules::Declaration>)> = collected
.iter()
.map(|(index, parsed)| {
let path = files.get(*index).map_or_else(Utf8PathBuf::new, |file| file.absolute.clone());
(path, parsed.declared.clone())
})
.collect();
let selected: HashSet<&Utf8Path> = files.iter().map(|file| file.absolute.as_path()).collect();
let extra_decl_files: Vec<(&Utf8Path, &CfgSet)> = declaration_files
.iter()
.filter(|&&(path, _cfg)| !selected.contains(path))
.map(|&(path, cfg)| (path, cfg))
.collect();
let declaration_skips: Vec<(Utf8PathBuf, String)> = if extra_decl_files.is_empty() {
Vec::new()
} else {
let Declarations {
declared: extra_declared,
skipped: extra_skipped,
} = parse_declarations_parallel(&extra_decl_files)?;
declared.extend(extra_declared);
extra_skipped
};
let excluded = modules::excluded_files(roots, &declared);
let total = collected.iter().map(|(_index, parsed)| parsed.mutants.len()).sum();
let mut mutants = Vec::with_capacity(total);
let mut digests: HashMap<Utf8PathBuf, String> = HashMap::default();
let mut suppressed = 0;
let mut idle = Vec::new();
for (index, parsed) in collected {
if files.get(index).is_some_and(|file| excluded.contains(&file.absolute)) {
continue;
}
if let Some(file) = files.get(index) {
let _replaced = digests.insert(file.path.clone(), parsed.digest);
}
mutants.extend(parsed.mutants);
suppressed += parsed.suppressed;
idle.extend(parsed.idle);
}
let selected_skips = shared.skipped.into_inner().unwrap_or_else(PoisonError::into_inner);
let mut unanalyzable: Vec<(Utf8PathBuf, String)> = selected_skips
.into_iter()
.map(|(at, message)| {
let path = files.get(at).map_or_else(Utf8PathBuf::new, |file| file.absolute.clone());
(path, message)
})
.chain(declaration_skips)
.collect();
unanalyzable.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1)));
unanalyzable.dedup();
Ok(Scan {
mutants,
suppressed,
idle,
skipped: unanalyzable.into_iter().map(|(_path, message)| message).collect(),
digests,
})
}
type DeclarationParse = (usize, Result<DeclarationOutcome>);
enum DeclarationOutcome {
Declared(Utf8PathBuf, Vec<modules::Declaration>),
Skipped(Utf8PathBuf, String),
}
struct Declarations {
declared: Vec<(Utf8PathBuf, Vec<modules::Declaration>)>,
skipped: Vec<(Utf8PathBuf, String)>,
}
fn parse_declarations_parallel(files: &[(&Utf8Path, &CfgSet)]) -> Result<Declarations> {
if files.is_empty() {
return Ok(Declarations {
declared: Vec::new(),
skipped: Vec::new(),
});
}
let workers = thread::available_parallelism().map_or(1, NonZero::get).min(files.len());
let mut results: Vec<DeclarationParse> = if workers <= 1 {
let mut results = Vec::with_capacity(files.len());
for (index, &(path, cfg)) in files.iter().enumerate() {
results.push((index, parse_declarations_of(path, cfg)));
}
results
} else {
let next = AtomicUsize::new(0);
thread::scope(|scope| {
let handles: Vec<_> = (0..workers)
.map(|_worker| {
let next = &next;
scope.spawn(move || {
let mut mine = Vec::new();
loop {
let index = next.fetch_add(1, Ordering::Relaxed);
let Some(&(path, cfg)) = files.get(index) else {
break;
};
mine.push((index, parse_declarations_of(path, cfg)));
}
mine
})
})
.collect();
let mut all = Vec::new();
for handle in handles {
all.extend(handle.join().unwrap_or_else(|payload| resume_unwind(payload)));
}
all
})
};
results.sort_by_key(|(index, _result)| *index);
let mut gathered = Declarations {
declared: Vec::with_capacity(results.len()),
skipped: Vec::new(),
};
for (_index, result) in results {
match result? {
DeclarationOutcome::Declared(path, declarations) => gathered.declared.push((path, declarations)),
DeclarationOutcome::Skipped(path, message) => gathered.skipped.push((path, message)),
}
}
gathered.declared.sort_by(|left, right| left.0.cmp(&right.0));
gathered.skipped.sort_by(|left, right| left.0.cmp(&right.0));
Ok(gathered)
}
fn parse_declarations_of(path: &Utf8Path, cfg: &CfgSet) -> Result<DeclarationOutcome> {
match SourceFile::read(path) {
Ok(source) => Ok(DeclarationOutcome::Declared(
path.to_owned(),
modules::declarations(path, source.ast(), cfg),
)),
Err(error) if error.is_skippable() => Ok(DeclarationOutcome::Skipped(path.to_owned(), error.to_string())),
Err(error) => Err(error.into()),
}
}
struct Scan {
mutants: Vec<Mutant>,
suppressed: usize,
idle: Vec<suppress::Idle>,
skipped: Vec<String>,
digests: HashMap<Utf8PathBuf, String>,
}
struct Shared {
next: AtomicUsize,
partials: Mutex<Vec<collect::Defaults>>,
skipped: Mutex<Vec<(usize, String)>>,
barrier: Barrier,
defaults: OnceLock<collect::Defaults>,
}
fn work(files: &[&TargetFile], shared: &Shared, selection: &Selection, cfgs: &Cfgs) -> Result<Vec<(usize, Parsed)>, (usize, Error)> {
let Shared {
next,
partials,
skipped,
barrier,
defaults,
} = shared;
let mut mine: Vec<(usize, SourceFile)> = Vec::new();
let mut index = collect::Defaults::default();
let mut failure: Option<(usize, Error)> = None;
let unwound = catch_unwind(AssertUnwindSafe(|| {
loop {
let at = next.fetch_add(1, Ordering::Relaxed);
let Some(file) = files.get(at) else { break };
#[cfg(test)]
#[cfg(not(miri))]
tests::panic_probe(&file.path);
match SourceFile::read(&file.absolute) {
Ok(mut source) => {
source.set_path(file.path.clone());
index.absorb(collect::Defaults::of_in(source.ast(), cfgs.for_package(&file.package)));
mine.push((at, source));
}
Err(error) if error.is_skippable() => {
skipped.lock().unwrap_or_else(PoisonError::into_inner).push((at, error.to_string()));
}
Err(error) => {
failure = Some((at, error.into()));
break;
}
}
}
}))
.err();
partials.lock().unwrap_or_else(PoisonError::into_inner).push(mem::take(&mut index));
let merged = if barrier.wait().is_leader() {
catch_unwind(AssertUnwindSafe(|| {
let mut merged = collect::Defaults::default();
for partial in partials.lock().unwrap_or_else(PoisonError::into_inner).drain(..) {
merged.absorb(partial);
}
let _first = defaults.set(merged);
}))
.err()
} else {
None
};
let _released = barrier.wait();
if let Some(payload) = unwound.or(merged) {
resume_unwind(payload);
}
if let Some(failed) = failure {
return Err(failed);
}
let Some(defaults) = defaults.get() else {
return Ok(Vec::new());
};
let mut parsed = Vec::with_capacity(mine.len());
for (at, source) in &mine {
let Some(file) = files.get(*at) else { continue };
match mutate(file, source, selection, cfgs, defaults) {
Ok(one) => parsed.push((*at, one)),
Err(error) => return Err((*at, error)),
}
}
Ok(parsed)
}
struct Parsed {
mutants: Vec<Mutant>,
suppressed: usize,
idle: Vec<suppress::Idle>,
declared: Vec<modules::Declaration>,
digest: String,
}
fn sorted<T: Ord + core::hash::Hash>(values: HashSet<T>) -> Vec<T> {
let mut ordered: Vec<T> = values.into_iter().collect();
ordered.sort_unstable();
ordered
}
fn reachable(metadata: &Metadata) -> HashMap<String, HashSet<String>> {
let member_packages: Vec<&str> = metadata.workspace_packages().iter().map(|p| p.name.as_str()).collect();
let member_count = member_packages.len();
let name_to_id: HashMap<&str, usize> = member_packages.iter().enumerate().map(|(id, name)| (*name, id)).collect();
let mut edges: Vec<Vec<usize>> = vec![Vec::new(); member_count];
let mut opaque: Vec<bool> = vec![false; member_count];
for package in metadata.workspace_packages() {
let Some(&src_id) = name_to_id.get(package.name.as_str()) else {
continue;
};
for dependency in &package.dependencies {
if let Some(&dst_id) = name_to_id.get(dependency.name.as_str()) {
edges[src_id].push(dst_id);
} else if dependency.path.is_some() {
opaque[src_id] = true;
}
}
}
reachable_ids(&edges, &opaque)
.into_iter()
.enumerate()
.map(|(id, reached)| {
let reachable_set: HashSet<String> = reached
.into_iter()
.map(|reached_id| member_packages[reached_id].to_owned())
.collect();
(member_packages[id].to_owned(), reachable_set)
})
.collect()
}
#[derive(Clone)]
struct Bitset {
words: Vec<u64>,
}
impl Bitset {
fn new(len: usize) -> Self {
Self {
words: vec![0; len.div_ceil(64)],
}
}
fn set(&mut self, index: usize) {
self.words[index / 64] |= 1 << (index % 64);
}
fn or_with(&mut self, other: &Self) {
for (word, other_word) in self.words.iter_mut().zip(&other.words) {
*word |= *other_word;
}
}
fn iter_set(&self) -> Vec<usize> {
let mut out = Vec::new();
for (word_index, word) in self.words.iter().enumerate() {
let mut remaining = *word;
while remaining != 0 {
let bit = remaining.trailing_zeros();
out.push(word_index * 64 + usize::try_from(bit).unwrap_or(0));
remaining &= remaining - 1;
}
}
out
}
}
struct Frame {
node: usize,
edge_at: usize,
}
fn strongly_connected_components(edges: &[Vec<usize>]) -> Vec<usize> {
let node_count = edges.len();
let mut index_of: Vec<Option<u32>> = vec![None; node_count];
let mut low_link: Vec<u32> = vec![0; node_count];
let mut on_stack: Vec<bool> = vec![false; node_count];
let mut comp_of: Vec<usize> = vec![usize::MAX; node_count];
let mut stack: Vec<usize> = Vec::new();
let mut next_index: u32 = 0;
let mut next_component: usize = 0;
for start in 0..node_count {
if index_of[start].is_some() {
continue;
}
let mut call_stack: Vec<Frame> = vec![Frame { node: start, edge_at: 0 }];
index_of[start] = Some(next_index);
low_link[start] = next_index;
next_index += 1;
stack.push(start);
on_stack[start] = true;
while let Some(frame) = call_stack.last_mut() {
let node = frame.node;
if frame.edge_at < edges[node].len() {
let neighbor = edges[node][frame.edge_at];
frame.edge_at += 1;
if let Some(neighbor_index) = index_of[neighbor] {
if on_stack[neighbor] {
low_link[node] = low_link[node].min(neighbor_index);
}
} else {
index_of[neighbor] = Some(next_index);
low_link[neighbor] = next_index;
next_index += 1;
stack.push(neighbor);
on_stack[neighbor] = true;
call_stack.push(Frame {
node: neighbor,
edge_at: 0,
});
}
} else {
call_stack.pop();
if let Some(parent) = call_stack.last() {
low_link[parent.node] = low_link[parent.node].min(low_link[node]);
}
if low_link[node] == index_of[node].expect("just indexed above, on the way in") {
loop {
let member = stack.pop().expect("root of this component pushed before this loop started");
on_stack[member] = false;
comp_of[member] = next_component;
if member == node {
break;
}
}
next_component += 1;
}
}
}
}
comp_of
}
fn reachable_ids(edges: &[Vec<usize>], opaque: &[bool]) -> Vec<HashSet<usize>> {
let node_count = edges.len();
let comp_of = strongly_connected_components(edges);
let component_count = comp_of.iter().copied().max().map_or(0, |max| max + 1);
let mut comp_edges: Vec<HashSet<usize>> = vec![HashSet::default(); component_count];
let mut comp_opaque: Vec<bool> = vec![false; component_count];
let mut comp_members: Vec<Vec<usize>> = vec![Vec::new(); component_count];
for node in 0..node_count {
let comp = comp_of[node];
comp_members[comp].push(node);
if opaque[node] {
comp_opaque[comp] = true;
}
for &neighbor in &edges[node] {
let neighbor_comp = comp_of[neighbor];
if neighbor_comp != comp {
let _added = comp_edges[comp].insert(neighbor_comp);
}
}
}
let mut comp_closure: Vec<Bitset> = Vec::with_capacity(component_count);
for (comp, successors) in comp_edges.iter().enumerate() {
let mut closure = Bitset::new(component_count);
closure.set(comp);
for &successor in successors {
let successor_closure = comp_closure[successor].clone();
closure.or_with(&successor_closure);
}
comp_closure.push(closure);
}
let all_nodes: HashSet<usize> = (0..node_count).collect();
let effective_opaque: Vec<bool> = (0..component_count)
.map(|comp| comp_closure[comp].iter_set().into_iter().any(|reached| comp_opaque[reached]))
.collect();
(0..node_count)
.map(|node| {
let comp = comp_of[node];
if effective_opaque[comp] {
all_nodes.clone()
} else {
comp_closure[comp]
.iter_set()
.into_iter()
.flat_map(|reached_comp| comp_members[reached_comp].iter().copied())
.collect()
}
})
.collect()
}
fn configuration(root: &Utf8Path, cargo: &CargoOptions, enabled: &HashMap<String, Vec<String>>) -> Cfgs {
let build = cargo.cfg_build(root);
let Ok(target) = crate::cfg::for_build(&build) else {
return Cfgs::unconditional();
};
Cfgs::new(&target, enabled)
}
fn placed_under(root: &Utf8Path, absolute: &Utf8Path, package: &str) -> Result<Utf8PathBuf> {
let Ok(inside) = absolute.strip_prefix(root) else {
return Err(error!(
"package `{package}` has the source file `{absolute}`, which lies outside the workspace root `{root}`; \
gamma cannot mutate a file it cannot place inside its scratch copy"
));
};
Ok(Utf8PathBuf::from(normalize_separators(inside.as_str())))
}
pub fn load_metadata(dir: &Utf8Path, features: &FeatureArgs) -> Result<Metadata> {
let mut command = MetadataCommand::new();
let _builder = command.current_dir(dir).no_deps();
if features.all_features {
let _builder = command.features(CargoOpt::AllFeatures);
}
if features.no_default_features {
let _builder = command.features(CargoOpt::NoDefaultFeatures);
}
if !features.features.is_empty() {
let named: Vec<String> = features
.features
.iter()
.flat_map(|entry| entry.split([',', ' ']))
.filter(|entry| !entry.is_empty())
.map(ToOwned::to_owned)
.collect();
let _builder = command.features(CargoOpt::SomeFeatures(named));
}
command
.exec()
.map_err(|cause| error!("could not read cargo metadata for `{dir}`").caused_by(cause))
}
#[derive(Default)]
struct ExternalPathInputs {
roots: Vec<Utf8PathBuf>,
has_build_scripts: bool,
}
fn external_path_inputs(dir: &Utf8Path, features: &FeatureArgs, root: &Utf8Path) -> Result<ExternalPathInputs> {
let mut command = MetadataCommand::new();
let _builder = command.current_dir(dir);
if features.all_features {
let _builder = command.features(CargoOpt::AllFeatures);
}
if features.no_default_features {
let _builder = command.features(CargoOpt::NoDefaultFeatures);
}
if !features.features.is_empty() {
let named: Vec<String> = features
.features
.iter()
.flat_map(|entry| entry.split([',', ' ']))
.filter(|entry| !entry.is_empty())
.map(ToOwned::to_owned)
.collect();
let _builder = command.features(CargoOpt::SomeFeatures(named));
}
let metadata = command
.exec()
.map_err(|cause| error!("could not read the external Cargo inputs for `{dir}`").caused_by(cause))?;
let workspace = crate::paths::physical(root)?;
let mut roots = Vec::new();
let mut has_build_scripts = false;
for package in metadata.packages {
has_build_scripts |= package.targets.iter().any(|target| target.kind.contains(&TargetKind::CustomBuild));
if package.source.is_some() {
continue;
}
let Some(directory) = Utf8Path::new(package.manifest_path.as_str()).parent() else {
return Err(error!(
"Cargo reported a package manifest without a parent at `{}`",
package.manifest_path
));
};
let directory = crate::paths::physical(directory)?;
if !directory.starts_with(&workspace) {
roots.push(directory);
}
}
roots.sort();
roots.dedup();
Ok(ExternalPathInputs { roots, has_build_scripts })
}
fn selected_packages(metadata: &Metadata, args: &SelectArgs) -> HashSet<String> {
let members = || {
metadata
.workspace_packages()
.iter()
.map(|package| package.name.to_string())
.collect()
};
let defaults = || -> Option<HashSet<String>> {
metadata.workspace_default_members.is_available().then(|| {
metadata
.workspace_default_packages()
.iter()
.map(|package| package.name.to_string())
.collect()
})
};
if !args.packages.is_empty() {
return args.packages.iter().cloned().collect();
}
if args.workspace {
return members();
}
if at_workspace_root(metadata, &args.dir)
&& let Some(selected) = defaults()
{
return selected;
}
if let Some(owner) = owning_package(metadata, &args.dir) {
return once(owner).collect();
}
defaults().unwrap_or_else(members)
}
fn at_workspace_root(metadata: &Metadata, dir: &Utf8Path) -> bool {
let resolve = |path: &Utf8Path| path.canonicalize_utf8().unwrap_or_else(|_unresolved| path.to_owned());
resolve(dir) == resolve(Utf8Path::new(metadata.workspace_root.as_str()))
}
fn owning_package(metadata: &Metadata, dir: &Utf8Path) -> Option<String> {
let absolute = dir.canonicalize_utf8().unwrap_or_else(|_unresolved| dir.to_owned());
let mut best: Option<(usize, String)> = None;
for package in metadata.workspace_packages() {
let Some(home) = Utf8Path::new(package.manifest_path.as_str()).parent() else {
continue;
};
let home = home.canonicalize_utf8().unwrap_or_else(|_unresolved| home.to_owned());
if !absolute.starts_with(&home) {
continue;
}
let depth = home.components().count();
if best.as_ref().is_none_or(|(deepest, _name)| depth > *deepest) {
best = Some((depth, package.name.to_string()));
}
}
best.map(|(_depth, name)| name)
}
fn unknown_packages<'args>(metadata: &Metadata, args: &'args SelectArgs) -> Option<&'args str> {
let known: HashSet<&str> = metadata.workspace_packages().iter().map(|package| package.name.as_str()).collect();
args.packages.iter().map(String::as_str).find(|wanted| !known.contains(wanted))
}
fn is_mutable_target(target: &Target, enabled: Option<&Vec<String>>) -> bool {
if target.name == crate::exec::RUNTIME_CRATE {
return false;
}
let kind = target
.kind
.iter()
.any(|kind| matches!(kind.to_string().as_str(), "lib" | "rlib" | "cdylib" | "bin"));
kind && enabled.is_none_or(|on| {
target.required_features.iter().all(|required| {
required.contains('/') || on.iter().any(|feature| feature == required)
})
})
}
fn test_targets(metadata: &Metadata) -> Vec<String> {
let mut names: Vec<String> = metadata
.workspace_packages()
.iter()
.flat_map(|package| package.targets.iter())
.filter(|target| target.test)
.map(|target| target.name.clone())
.collect();
names.sort();
names.dedup();
names
}
struct FilePatterns<'args> {
include: Vec<(&'args str, Glob)>,
exclude: Vec<(&'args str, Glob)>,
}
impl<'args> FilePatterns<'args> {
fn new(args: &'args SelectArgs) -> Self {
let compile = |patterns: &'args [String]| patterns.iter().map(|pattern| (pattern.as_str(), Glob::new(pattern))).collect();
Self {
include: compile(&args.files),
exclude: compile(&args.exclude_files),
}
}
fn unmatched(&self, walked: &[Utf8PathBuf]) -> Option<&'args str> {
self.include
.iter()
.chain(&self.exclude)
.find(|(_pattern, compiled)| !walked.iter().any(|path| compiled.matches(path.as_str())))
.map(|(pattern, _compiled)| *pattern)
}
fn includes(&self, path: &Utf8Path) -> bool {
let text = path.as_str();
if self.exclude.iter().any(|(_pattern, compiled)| compiled.matches(text)) {
return false;
}
self.include.is_empty() || self.include.iter().any(|(_pattern, compiled)| compiled.matches(text))
}
}
#[cfg(test)]
fn is_included(path: &Utf8Path, args: &SelectArgs) -> bool {
FilePatterns::new(args).includes(path)
}
fn walk_rust_files(directory: &Utf8Path) -> Result<Vec<Utf8PathBuf>> {
let mut found: Vec<Utf8PathBuf> = Vec::new();
for entry in WalkDir::new(directory) {
let entry = entry.map_err(|cause| error!("could not list the source files under `{directory}`").caused_by(cause))?;
if entry.file_type().is_dir() {
continue;
}
let rust = entry.path().extension() == Some(OsStr::new("rs"));
match Utf8PathBuf::from_path_buf(entry.into_path()) {
Ok(path) if rust => found.push(path),
Ok(_other) => {}
Err(path) if rust => {
return Err(error!(
"the source file `{}` under `{directory}` has a path that is not UTF-8, so it cannot be mutated",
path.display()
));
}
Err(_other) => {}
}
}
found.sort();
Ok(found)
}
#[cfg(test)]
#[cfg(not(miri))]
mod tests {
use std::fs;
use tempfile::TempDir;
use super::*;
use crate::testing::discover_fixture::counting_mutant;
const PANIC_PROBE: &str = "gamma_panic_probe.rs";
pub(super) fn panic_probe(path: &Utf8Path) {
assert!(
!path.as_str().ends_with(PANIC_PROBE),
"a survey worker panicking during phase one, on purpose"
);
}
#[test]
fn excludes_beat_includes() {
let args = SelectArgs {
files: vec!["src/**/*.rs".to_owned()],
exclude_files: vec!["generated.rs".to_owned()],
..SelectArgs::default()
};
assert!(is_included(Utf8Path::new("src/lexer.rs"), &args));
assert!(!is_included(Utf8Path::new("src/generated.rs"), &args));
}
#[test]
fn no_include_patterns_means_everything() {
assert!(is_included(Utf8Path::new("anything.rs"), &SelectArgs::default()));
}
#[test]
fn a_package_reaches_itself() {
let metadata = load_metadata(Utf8Path::new(env!("CARGO_MANIFEST_DIR")), &FeatureArgs::default()).expect("metadata");
let reach = reachable(&metadata);
for (package, reachable_from) in &reach {
assert!(reachable_from.contains(package), "{package} does not reach itself");
}
}
#[test]
fn a_dependent_reaches_what_it_depends_on() {
let metadata = load_metadata(Utf8Path::new(env!("CARGO_MANIFEST_DIR")), &FeatureArgs::default()).expect("metadata");
let reach = reachable(&metadata);
let from_binary = reach.get("cargo-gamma").expect("the binary crate is a workspace member");
assert!(from_binary.contains("cargo-gamma-lib"), "{from_binary:?}");
let from_library = reach.get("cargo-gamma-lib").expect("the library is a workspace member");
assert!(!from_library.contains("cargo-gamma"), "{from_library:?}");
}
fn brute_force_reachable(edges: &[Vec<usize>], opaque: &[bool]) -> Vec<HashSet<usize>> {
let node_count = edges.len();
(0..node_count)
.map(|start| {
let mut seen = vec![false; node_count];
let mut queue = std::collections::VecDeque::from([start]);
seen[start] = true;
let mut hits_opaque = false;
while let Some(current) = queue.pop_front() {
if opaque[current] {
hits_opaque = true;
}
for &neighbor in &edges[current] {
if !seen[neighbor] {
seen[neighbor] = true;
queue.push_back(neighbor);
}
}
}
if hits_opaque {
(0..node_count).collect()
} else {
seen.iter()
.enumerate()
.filter(|(_id, reached)| **reached)
.map(|(id, _)| id)
.collect()
}
})
.collect()
}
fn xorshift(state: &mut u64) -> u64 {
*state ^= *state << 13;
*state ^= *state >> 7;
*state ^= *state << 17;
*state
}
#[test]
fn a_chain_gives_every_node_a_strictly_smaller_id_than_its_predecessor() {
let edges = vec![vec![1], vec![2], vec![3], vec![]];
let comp = strongly_connected_components(&edges);
assert_ne!(comp[0], comp[1]);
assert_ne!(comp[1], comp[2]);
assert_ne!(comp[2], comp[3]);
assert!(comp[3] < comp[2]);
assert!(comp[2] < comp[1]);
assert!(comp[1] < comp[0]);
}
#[test]
fn a_full_cycle_collapses_into_one_component_that_reaches_itself_and_its_own_dependency() {
let edges = vec![vec![1], vec![2], vec![0, 3], vec![]];
let opaque = [false; 4];
let reach = reachable_ids(&edges, &opaque);
for member in [0, 1, 2] {
assert_eq!(reach[member], [0, 1, 2, 3].into_iter().collect::<HashSet<_>>(), "member {member}");
}
assert_eq!(reach[3], std::iter::once(3).collect::<HashSet<_>>());
}
#[test]
fn a_dense_graph_matches_the_brute_force_oracle() {
let edges = vec![
vec![1, 2],
vec![3],
vec![3, 4],
vec![5],
vec![5],
std::iter::once(6).collect(),
vec![],
];
let opaque = [false; 7];
assert_eq!(reachable_ids(&edges, &opaque), brute_force_reachable(&edges, &opaque));
}
#[test]
fn an_opaque_node_taints_itself_and_everything_that_can_reach_it() {
let edges = vec![vec![1], vec![2], vec![]];
let opaque = [false, false, true];
let reach = reachable_ids(&edges, &opaque);
for member in [0, 1, 2] {
assert_eq!(reach[member], [0, 1, 2].into_iter().collect::<HashSet<_>>(), "member {member}");
}
}
#[test]
fn an_opaque_member_of_a_cycle_taints_the_whole_component() {
let edges = vec![vec![1], vec![0], vec![]];
let opaque = [false, true, false];
let reach = reachable_ids(&edges, &opaque);
assert_eq!(reach[0], [0, 1, 2].into_iter().collect::<HashSet<_>>());
assert_eq!(reach[1], [0, 1, 2].into_iter().collect::<HashSet<_>>());
assert_eq!(reach[2], std::iter::once(2).collect::<HashSet<_>>());
}
#[test]
fn randomized_graphs_with_cycles_and_opaque_nodes_match_the_brute_force_oracle() {
let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
for trial in 0..200 {
let node_count = 1 + usize::try_from(xorshift(&mut state) % 12).unwrap_or(0);
let mut edges: Vec<Vec<usize>> = vec![Vec::new(); node_count];
for (from, out_edges) in edges.iter_mut().enumerate() {
for to in 0..node_count {
if from != to && xorshift(&mut state).is_multiple_of(3) {
out_edges.push(to);
}
}
}
let opaque: Vec<bool> = (0..node_count).map(|_| xorshift(&mut state).is_multiple_of(10)).collect();
assert_eq!(
reachable_ids(&edges, &opaque),
brute_force_reachable(&edges, &opaque),
"trial {trial} with {node_count} nodes: {edges:?}, opaque {opaque:?}"
);
}
}
#[test]
fn external_path_dependencies_and_local_build_scripts_are_cache_inputs() {
let directory = crate::testing::workdir("survey-external-inputs-");
let container = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("UTF-8 path");
let root = container.join("workspace");
let dependency = container.join("dependency");
fs::create_dir_all(root.join("src")).expect("workspace source");
fs::create_dir_all(dependency.join("src")).expect("dependency source");
fs::write(
root.join("Cargo.toml"),
"[workspace]\n\n[package]\nname = \"workspace\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\ndependency = { path = \"../dependency\" }\n",
)
.expect("workspace manifest");
fs::write(root.join("src/lib.rs"), "pub fn workspace() {}\n").expect("workspace source");
fs::write(
dependency.join("Cargo.toml"),
"[package]\nname = \"dependency\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.expect("dependency manifest");
fs::write(dependency.join("src/lib.rs"), "pub fn dependency() {}\n").expect("dependency source");
fs::write(dependency.join("build.rs"), "fn main() {}\n").expect("dependency build script");
let inputs = external_path_inputs(&root, &FeatureArgs::default(), &root).expect("metadata");
assert_eq!(inputs.roots, vec![crate::paths::physical(&dependency).expect("dependency path")]);
assert!(
inputs.has_build_scripts,
"a local build script can read external paths Cargo metadata does not enumerate"
);
}
#[test]
fn a_registry_build_script_makes_the_snapshot_uncacheable() {
let root = Utf8Path::new(env!("CARGO_MANIFEST_DIR"));
let mut command = MetadataCommand::new();
let _builder = command.current_dir(root);
let metadata = command.exec().expect("workspace metadata");
let blake3 = metadata
.packages
.iter()
.find(|package| package.name == "blake3")
.expect("the workspace resolves blake3");
assert!(blake3.source.is_some(), "blake3 must be a registry package");
assert!(
blake3.targets.iter().any(|target| target.kind.contains(&TargetKind::CustomBuild)),
"blake3 must expose its build script in metadata"
);
let workspace = Utf8Path::new(metadata.workspace_root.as_str());
let inputs = external_path_inputs(workspace, &FeatureArgs::default(), workspace).expect("metadata");
assert!(
inputs.has_build_scripts,
"a registry build script can read uncaptured inputs, so its snapshot must be incomplete"
);
}
#[test]
fn discovery_refuses_inline_and_file_cargo_configuration_before_metadata() {
let (_directory, root) = workspace();
let args = SelectArgs {
dir: root,
..SelectArgs::default()
};
for extra in [
vec!["--config".to_owned(), "build.target = \"wasm32-wasip1\"".to_owned()],
vec!["--config=outside.toml".to_owned()],
] {
let failure = Survey::for_build(
&args,
None,
&CargoOptions {
extra,
..CargoOptions::default()
},
)
.expect_err("unmodelled Cargo configuration must stop discovery");
assert!(failure.is_usage(), "{failure}");
assert!(failure.to_string().contains("--config"), "{failure}");
}
}
#[test]
fn discovery_propagates_a_configuration_failure() {
let (_directory, root) = workspace();
let config = root.join("gamma.toml");
fs::create_dir_all(config.parent().expect("configuration parent")).expect("configuration parent");
fs::write(&config, "cargo-args = [\n").expect("malformed configuration");
let failure = Survey::new(
&SelectArgs {
dir: root,
..SelectArgs::default()
},
None,
)
.expect_err("configuration failure must not become default discovery settings");
assert!(failure.is_usage(), "{failure}");
assert!(failure.to_string().contains("gamma.toml"), "{failure}");
}
#[test]
fn a_narrow_survey_keeps_unselected_dependency_roots() {
let directory = TempDir::new().expect("a temporary directory");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the temporary path is not UTF-8");
write(&root, "Cargo.toml", "[workspace]\nmembers = [\"a\", \"b\"]\nresolver = \"3\"\n");
write(
&root,
"a/Cargo.toml",
"[package]\nname = \"a\"\nversion = \"0.1.0\"\nedition = \"2024\"\n[dependencies]\nb = { path = \"../b\" }\n",
);
write(&root, "a/src/lib.rs", "pub fn a() -> bool { b::b() }\n");
write(
&root,
"b/Cargo.toml",
"[package]\nname = \"b\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
);
write(&root, "b/src/lib.rs", "pub fn b() -> bool { true }\n");
let survey = Survey::for_build(
&SelectArgs {
dir: root,
packages: vec!["a".to_owned()],
..SelectArgs::default()
},
None,
&CargoOptions::default(),
)
.expect("the narrowed workspace should survey");
assert_eq!(survey.selected, ["a"]);
assert_eq!(survey.specs.get("a").map(|(path, _version)| path.as_str()), Some("a"));
assert_eq!(survey.specs.get("b").map(|(path, _version)| path.as_str()), Some("b"));
}
#[test]
fn a_package_that_depends_through_a_non_member_reaches_the_whole_workspace() {
let directory = TempDir::new().expect("a temporary directory");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the temporary path is not UTF-8");
write(
&root,
"Cargo.toml",
"[workspace]\nmembers = [\"core\", \"app\"]\nexclude = [\"facade\"]\nresolver = \"3\"\n",
);
write(
&root,
"core/Cargo.toml",
"[package]\nname = \"core\"\nversion = \"0.0.0\"\nedition = \"2024\"\n",
);
write(&root, "core/src/lib.rs", "pub fn add(a: i32, b: i32) -> i32 { a + b }\n");
write(
&root,
"facade/Cargo.toml",
"[package]\nname = \"facade\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n\
[dependencies]\ncore = { path = \"../core\" }\n\n[workspace]\n",
);
write(&root, "facade/src/lib.rs", "pub use core::add;\n");
write(
&root,
"app/Cargo.toml",
"[package]\nname = \"app\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n\
[dependencies]\nfacade = { path = \"../facade\" }\n",
);
write(&root, "app/src/lib.rs", "pub fn go() -> i32 { facade::add(1, 2) }\n");
let metadata = load_metadata(&root, &FeatureArgs::default()).expect("metadata");
let reach = reachable(&metadata);
let from_app = reach.get("app").expect("app is a workspace member");
assert!(from_app.contains("core"), "{from_app:?}");
let from_core = reach.get("core").expect("core is a workspace member");
assert!(!from_core.contains("app"), "{from_core:?}");
}
#[test]
fn a_dependent_of_an_opaque_package_is_never_ordered_before_it() {
let directory = TempDir::new().expect("a temporary directory");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the temporary path is not UTF-8");
write(
&root,
"Cargo.toml",
"[workspace]\nmembers = [\"opaque\", \"dependent\", \"unrelated\"]\nexclude = [\"outside\"]\nresolver = \"3\"\n",
);
write(
&root,
"outside/Cargo.toml",
"[package]\nname = \"outside\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n[workspace]\n",
);
write(&root, "outside/src/lib.rs", "pub fn f() {}\n");
write(
&root,
"opaque/Cargo.toml",
"[package]\nname = \"opaque\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n\
[dependencies]\noutside = { path = \"../outside\" }\n",
);
write(&root, "opaque/src/lib.rs", "pub fn f() {}\n");
write(
&root,
"dependent/Cargo.toml",
"[package]\nname = \"dependent\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n\
[dependencies]\nopaque = { path = \"../opaque\" }\n",
);
write(&root, "dependent/src/lib.rs", "pub fn f() {}\n");
write(
&root,
"unrelated/Cargo.toml",
"[package]\nname = \"unrelated\"\nversion = \"0.0.0\"\nedition = \"2024\"\n",
);
write(&root, "unrelated/src/lib.rs", "pub fn f() {}\n");
let metadata = load_metadata(&root, &FeatureArgs::default()).expect("metadata");
let reach = reachable(&metadata);
let of = |name: &str| reach.get(name).cloned().unwrap_or_default();
assert!(of("dependent").is_superset(&of("opaque")), "{reach:?}");
let packages = vec!["opaque".to_owned(), "dependent".to_owned(), "unrelated".to_owned()];
let stages = crate::discover::stages(&packages, &reach);
let place = |name: &str| {
stages
.iter()
.position(|stage| stage.iter().any(|member| member == name))
.expect("every package is placed somewhere")
};
assert!(place("opaque") <= place("dependent"), "{stages:?}");
}
#[test]
fn a_run_from_inside_a_member_selects_that_member_alone() {
let home = TempDir::new().expect("could not create a temporary directory");
let root = Utf8Path::from_path(home.path()).expect("the temporary path is not UTF-8");
write(
root,
"Cargo.toml",
"[workspace]\nmembers = [\"alpha\", \"beta\"]\nresolver = \"2\"\n",
);
write(
root,
"alpha/Cargo.toml",
"[package]\nname = \"alpha\"\nversion = \"0.0.0\"\nedition = \"2024\"\n",
);
write(root, "alpha/src/lib.rs", "pub fn a() {}\n");
write(
root,
"beta/Cargo.toml",
"[package]\nname = \"beta\"\nversion = \"0.0.0\"\nedition = \"2024\"\n",
);
write(root, "beta/src/lib.rs", "pub fn b() {}\n");
let metadata = load_metadata(root, &FeatureArgs::default()).expect("metadata");
let inside = SelectArgs {
dir: root.join("alpha"),
..SelectArgs::default()
};
assert_eq!(selected_packages(&metadata, &inside), once("alpha".to_owned()).collect());
let deeper = SelectArgs {
dir: root.join("alpha/src"),
..SelectArgs::default()
};
assert_eq!(selected_packages(&metadata, &deeper), once("alpha".to_owned()).collect());
}
#[test]
fn the_whole_workspace_is_selected_only_when_asked_for() {
let home = TempDir::new().expect("could not create a temporary directory");
let root = Utf8Path::from_path(home.path()).expect("the temporary path is not UTF-8");
write(
root,
"Cargo.toml",
"[workspace]\nmembers = [\"alpha\", \"beta\"]\nresolver = \"2\"\n",
);
write(
root,
"alpha/Cargo.toml",
"[package]\nname = \"alpha\"\nversion = \"0.0.0\"\nedition = \"2024\"\n",
);
write(root, "alpha/src/lib.rs", "pub fn a() {}\n");
write(
root,
"beta/Cargo.toml",
"[package]\nname = \"beta\"\nversion = \"0.0.0\"\nedition = \"2024\"\n",
);
write(root, "beta/src/lib.rs", "pub fn b() {}\n");
let metadata = load_metadata(root, &FeatureArgs::default()).expect("metadata");
let both: HashSet<String> = ["alpha".to_owned(), "beta".to_owned()].into_iter().collect();
let whole = SelectArgs {
dir: root.join("alpha"),
workspace: true,
..SelectArgs::default()
};
assert_eq!(selected_packages(&metadata, &whole), both);
let named = SelectArgs {
dir: root.join("alpha"),
packages: vec!["beta".to_owned()],
..SelectArgs::default()
};
assert_eq!(selected_packages(&metadata, &named), once("beta".to_owned()).collect());
let outside = SelectArgs {
dir: root.to_owned(),
..SelectArgs::default()
};
assert_eq!(selected_packages(&metadata, &outside), both);
}
#[test]
fn the_workspace_root_honours_the_default_members() {
let home = TempDir::new().expect("could not create a temporary directory");
let root = Utf8Path::from_path(home.path()).expect("the temporary path is not UTF-8");
write(
root,
"Cargo.toml",
"[workspace]\nmembers = [\"alpha\", \"beta\"]\ndefault-members = [\"alpha\"]\nresolver = \"2\"\n",
);
write(
root,
"alpha/Cargo.toml",
"[package]\nname = \"alpha\"\nversion = \"0.0.0\"\nedition = \"2024\"\n",
);
write(root, "alpha/src/lib.rs", "pub fn a() {}\n");
write(
root,
"beta/Cargo.toml",
"[package]\nname = \"beta\"\nversion = \"0.0.0\"\nedition = \"2024\"\n",
);
write(root, "beta/src/lib.rs", "pub fn b() {}\n");
let metadata = load_metadata(root, &FeatureArgs::default()).expect("metadata");
let at_root = SelectArgs {
dir: root.to_owned(),
..SelectArgs::default()
};
assert_eq!(selected_packages(&metadata, &at_root), once("alpha".to_owned()).collect());
}
#[test]
fn a_root_package_workspace_honours_the_default_members() {
let home = TempDir::new().expect("could not create a temporary directory");
let root = Utf8Path::from_path(home.path()).expect("the temporary path is not UTF-8");
write(
root,
"Cargo.toml",
"[package]\nname = \"host\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n\
[workspace]\nmembers = [\"alpha\"]\ndefault-members = [\"alpha\"]\nresolver = \"2\"\n",
);
write(root, "src/lib.rs", "pub fn h() {}\n");
write(
root,
"alpha/Cargo.toml",
"[package]\nname = \"alpha\"\nversion = \"0.0.0\"\nedition = \"2024\"\n",
);
write(root, "alpha/src/lib.rs", "pub fn a() {}\n");
let metadata = load_metadata(root, &FeatureArgs::default()).expect("metadata");
let at_root = SelectArgs {
dir: root.to_owned(),
..SelectArgs::default()
};
assert_eq!(selected_packages(&metadata, &at_root), once("alpha".to_owned()).collect());
}
#[test]
fn a_member_outside_the_workspace_root_is_refused_rather_than_given_an_absolute_path() {
let home = TempDir::new().expect("could not create a temporary directory");
let base = Utf8Path::from_path(home.path()).expect("the temporary path is not UTF-8");
let root = base.join("w");
write(&root, "Cargo.toml", "[workspace]\nmembers = [\"../sibling\"]\nresolver = \"2\"\n");
write(
base,
"sibling/Cargo.toml",
"[package]\nname = \"sibling\"\nversion = \"0.0.0\"\nedition = \"2024\"\nworkspace = \"../w\"\n",
);
write(base, "sibling/src/lib.rs", "pub fn s() -> i32 { 1 + 2 }\n");
let args = SelectArgs {
dir: root,
..SelectArgs::default()
};
let failure = Survey::new(&args, None).expect_err("a member outside the root must be refused");
assert!(failure.to_string().contains("outside the workspace root"), "{failure}");
}
#[test]
fn a_registry_dependency_does_not_make_a_package_opaque() {
let metadata = load_metadata(Utf8Path::new(env!("CARGO_MANIFEST_DIR")), &FeatureArgs::default()).expect("metadata");
let reach = reachable(&metadata);
let from_library = reach.get("cargo-gamma-lib").expect("the library is a workspace member");
assert!(
!from_library.contains("cargo-gamma"),
"this crate has many registry dependencies: {from_library:?}"
);
}
#[test]
fn a_mutant_from_a_package_no_file_was_walked_for_does_not_inflate_anothers_count() {
let files = [TargetFile {
path: Utf8PathBuf::from("core/src/lib.rs"),
absolute: Utf8PathBuf::from("/tree/core/src/lib.rs"),
package: "core".to_owned(),
}];
let mutants = [counting_mutant("core"), counting_mutant("ghost")];
let mut lines = Vec::new();
report_by_package(&files, &mutants, &mut |line: &str| lines.push(line.to_owned()));
assert_eq!(lines.len(), 1, "{lines:?}");
assert!(lines[0].starts_with("core, 1 mutant in 1"), "{lines:?}");
}
fn workspace() -> (TempDir, Utf8PathBuf) {
let directory = TempDir::new().expect("a temporary directory");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the temporary path is not UTF-8");
write(
&root,
"Cargo.toml",
"[workspace]\nmembers = [\"core\", \"app\"]\nresolver = \"3\"\n",
);
write(
&root,
"core/Cargo.toml",
"[package]\nname = \"core\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[features]\nextra = []\n",
);
write(&root, "core/src/lib.rs", "pub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n");
write(&root, "core/src/generated.rs", "pub fn scale(x: i32) -> i32 {\n x * 2\n}\n");
write(
&root,
"app/Cargo.toml",
"[package]\nname = \"app\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\ncore = { path = \"../core\" }\n",
);
write(&root, "app/src/main.rs", "fn main() {\n let _ = 1 + 1;\n}\n");
write(&root, "app/examples/demo.rs", "fn main() {\n let _ = 2 + 2;\n}\n");
write(&root, "app/tests/it.rs", "#[test]\nfn works() {\n assert_eq!(1 + 1, 2);\n}\n");
(directory, root)
}
#[test]
fn scanning_a_package_at_a_time_reaches_the_same_population_as_scanning_the_workspace() {
let (_directory, root) = wide_workspace();
let survey = survey(&root, SelectArgs::default());
let selection = Selection::parse("all").expect("every mutator resolves");
let mut ordinals = 0;
let whole = survey.scan(None, &selection, &mut ordinals).expect("the fixture must scan");
let mut ordinals = 0;
let mut piecemeal: Vec<MutantId> = Vec::new();
for package in survey.packages() {
let scanned = survey
.scan(Some(&package), &selection, &mut ordinals)
.expect("the fixture must scan");
piecemeal.extend(scanned.mutants.iter().map(|mutant| mutant.id.clone()));
}
let mut expected: Vec<MutantId> = whole.mutants.iter().map(|mutant| mutant.id.clone()).collect();
let mut found = piecemeal.clone();
expected.sort();
found.sort();
assert_eq!(found, expected, "package-by-package scanning reached a different population");
let mut ordinals = 0;
let absent = survey
.scan(Some("nothing-by-this-name"), &selection, &mut ordinals)
.expect("scanning nothing is not an error");
assert!(absent.mutants.is_empty(), "{:?}", absent.mutants);
}
#[test]
fn every_mutant_exclusion_rule_must_match() {
let (_directory, root) = workspace();
write(
&root,
"core/src/lib.rs",
"trait Diagnostic { fn value(&self) -> i32; }\n\
struct Message;\n\
impl Diagnostic for Message { fn value(&self) -> i32 { 1 + 2 } }\n",
);
let failure = Survey::new(
&SelectArgs {
dir: root,
exclude_trait_impls: vec!["Diagnostic".to_owned(), "Diagnostc".to_owned()],
..SelectArgs::default()
},
None,
)
.expect_err("one unmatched entry must refuse the selection");
assert!(failure.is_usage());
assert!(failure.to_string().contains("entry 2"), "{failure}");
assert!(failure.to_string().contains("Diagnostc"), "{failure}");
assert!(failure.to_string().contains("matched no trait implementations"), "{failure}");
}
#[test]
fn exclusion_validation_matches_implementations_without_selected_mutants() {
let (_directory, root) = workspace();
write(
&root,
"core/src/lib.rs",
"trait Diagnostic { fn write(&self); }\n\
struct Message;\n\
impl Diagnostic for Message { fn write(&self) {} }\n\
pub fn compare(left: i32, right: i32) -> bool { left < right }\n",
);
let survey = survey(
&root,
SelectArgs {
exclude_trait_impls: vec!["Diagnostic".to_owned()],
..SelectArgs::default()
},
);
let mut ordinals = 0;
let scanned = survey
.scan(None, &Selection::parse("relational").expect("selection"), &mut ordinals)
.expect("the implementation exists even though it contains no selected mutant");
assert!(!scanned.mutants.is_empty(), "the ordinary relational mutant remains selected");
}
#[test]
fn package_scans_apply_exclusions_after_workspace_validation() {
let (_directory, root) = workspace();
write(
&root,
"core/src/lib.rs",
"trait Diagnostic { fn value(&self) -> i32; }\n\
struct Message;\n\
impl Diagnostic for Message { fn value(&self) -> i32 { 1 + 2 } }\n",
);
let survey = survey(
&root,
SelectArgs {
exclude_trait_impls: vec!["Diagnostic".to_owned()],
..SelectArgs::default()
},
);
let packages = survey.packages();
assert_eq!(packages, ["app", "core"], "the non-matching package must be scanned first");
let selection = Selection::parse("arith.add_to_sub").expect("selection");
let mut ordinals = 0;
let first = survey
.scan(Some(&packages[0]), &selection, &mut ordinals)
.expect("workspace validation already established the name");
let second = survey
.scan(Some(&packages[1]), &selection, &mut ordinals)
.expect("the matching package is scanned normally");
assert!(!first.mutants.is_empty(), "the first package still contributes ordinary mutants");
assert!(
second
.mutants
.iter()
.all(|mutant| mutant.trait_impl.as_deref() != Some("Diagnostic")),
"{:?}",
second.mutants
);
}
#[test]
fn exclusion_validation_is_independent_of_package_selection() {
let (_directory, root) = workspace();
write(
&root,
"core/src/lib.rs",
"struct Message;\n\
impl core::fmt::Debug for Message {\n\
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {\n\
formatter.write_str(\"message\")\n\
}\n\
}\n",
);
let survey = Survey::new(
&SelectArgs {
dir: root,
packages: vec!["app".to_owned()],
exclude_trait_impls: vec!["Debug".to_owned()],
..SelectArgs::default()
},
None,
)
.expect("a workspace-wide exclusion may match outside the selected package");
assert_eq!(survey.packages(), ["app"]);
}
fn wide_workspace() -> (TempDir, Utf8PathBuf) {
let directory = TempDir::new().expect("a temporary directory");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the temporary path is not UTF-8");
write(
&root,
"Cargo.toml",
"[workspace]\nmembers = [\"core\", \"mid\", \"app\"]\nresolver = \"3\"\n",
);
write(
&root,
"core/Cargo.toml",
"[package]\nname = \"core\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[[bin]]\nname = \"core-cli\"\npath = \"src/main.rs\"\n",
);
write(
&root,
"core/src/lib.rs",
"#[cfg(test)]\nmod helpers;\n\npub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n",
);
write(&root, "core/src/main.rs", "fn main() {\n let _ = 1 + 1;\n}\n");
write(&root, "core/src/helpers.rs", "pub fn double(x: i32) -> i32 {\n x * 2\n}\n");
write(
&root,
"mid/Cargo.toml",
"[package]\nname = \"mid\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\ncore = { path = \"../core\" }\n",
);
write(&root, "mid/src/lib.rs", "pub fn triple(x: i32) -> i32 {\n x * 3\n}\n");
write(
&root,
"app/Cargo.toml",
"[package]\nname = \"app\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\ncore = { path = \"../core\" }\nmid = { path = \"../mid\" }\n",
);
write(&root, "app/src/main.rs", "fn main() {\n let _ = 2 + 2;\n}\n");
(directory, root)
}
fn repeatable_workspace() -> (TempDir, Utf8PathBuf) {
let (directory, root) = wide_workspace();
for at in 0..32 {
write(
&root,
&format!("core/src/filler{at}.rs"),
"pub fn value(x: i32) -> i32 {\n x + 1\n}\n",
);
}
write(
&root,
"mid/src/quiet.rs",
"#[gamma::skip(reason = \"the population has to be stable including what was withheld\")]\npub fn quiet(x: i32) -> i32 {\n x - 1\n}\n",
);
(directory, root)
}
fn fingerprint(mutant: &Mutant) -> String {
format!(
"{}|{}|{}|{}|{:?}|{}|{}|{}|{}|{:?}|{:?}|{:?}",
mutant.ordinal,
mutant.id,
mutant.file,
mutant.package,
mutant.span,
mutant.line,
mutant.column,
mutant.mutator,
mutant.item_path,
mutant.shape,
mutant.outcome,
mutant.replacement,
)
}
#[test]
fn scanning_an_unchanged_tree_twice_produces_the_same_population() {
let (_directory, root) = repeatable_workspace();
let selection = Selection::parse("all").expect("every mutator resolves");
let mut populations = Vec::new();
let mut counts = Vec::new();
for _ in 0..3 {
let survey = survey(&root, SelectArgs::default());
let mut ordinals = 0;
let scanned = survey.scan(None, &selection, &mut ordinals).expect("the fixture must scan");
let plan = survey.into_plan(scanned);
counts.push((
plan.files.iter().map(|file| file.path.to_string()).collect::<Vec<_>>(),
plan.suppressed,
plan.sharded_out,
plan.settled_out,
));
populations.push(plan.mutants.iter().map(fingerprint).collect::<Vec<_>>());
}
assert!(
!populations[0].is_empty(),
"the fixture produced no mutants, so this proves nothing"
);
assert!(counts[0].1 > 0, "the fixture suppressed nothing, so the withheld set is untested");
assert_eq!(populations[1], populations[0], "the second scan found a different population");
assert_eq!(populations[2], populations[0], "the third scan found a different population");
assert_eq!(counts[1], counts[0], "the second scan disagreed about the files or the counts");
assert_eq!(counts[2], counts[0], "the third scan disagreed about the files or the counts");
}
fn probe_workspace() -> (TempDir, Utf8PathBuf) {
let (directory, root) = wide_workspace();
for at in 0..64 {
write(
&root,
&format!("core/src/filler{at}.rs"),
"pub fn value(x: i32) -> i32 {\n x + 1\n}\n",
);
}
write(&root, &format!("core/src/{PANIC_PROBE}"), "pub fn nothing() {}\n");
(directory, root)
}
#[test]
fn a_worker_that_panics_while_parsing_does_not_wedge_the_others_at_the_barrier() {
let (directory, root) = probe_workspace();
let panicked = crate::testing::within(crate::testing::WATCHDOG, "the scan", move || {
catch_unwind(AssertUnwindSafe(|| {
let survey = survey(&root, SelectArgs::default());
let mut ordinals = 0;
survey
.scan(None, &Selection::parse("@all").expect("selection"), &mut ordinals)
.map(|scanned| scanned.mutants.len())
}))
.is_err()
});
assert!(panicked, "the worker's panic must reach the caller, not be swallowed");
drop(directory);
}
fn write(root: &Utf8Path, relative: &str, text: &str) {
let path = root.join(relative);
fs::create_dir_all(path.parent().expect("every fixture path has a parent").as_std_path())
.expect("could not create the fixture directory");
fs::write(path.as_std_path(), text).expect("could not write the fixture file");
}
#[test]
fn a_directory_that_cannot_be_walked_is_reported_rather_than_passed_off_as_empty() {
let directory = TempDir::new().expect("a temporary directory");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the temporary path is not UTF-8");
let missing = root.join("vanished");
let error = walk_rust_files(&missing).expect_err("a directory that is not there cannot have been walked");
assert!(error.to_string().contains("vanished"), "{error}");
write(&root, "here/one.rs", "pub fn f() {}\n");
write(&root, "here/notes.txt", "not a source file\n");
let listed = walk_rust_files(&root.join("here")).expect("a readable directory walks");
assert_eq!(listed, vec![root.join("here/one.rs")]);
}
#[cfg(unix)]
#[test]
fn a_source_file_with_a_path_that_is_not_utf8_is_reported_rather_than_dropped() {
use std::os::unix::ffi::OsStrExt;
let directory = TempDir::new().expect("a temporary directory");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the temporary path is not UTF-8");
write(&root, "src/lib.rs", "pub fn f() {}\n");
let odd = directory.path().join("src").join(OsStr::from_bytes(b"lat\xffin.rs"));
fs::write(&odd, "pub fn g() {}\n").expect("the fixture file is written");
let error = walk_rust_files(&root.join("src")).expect_err("a source file nobody can name is not skipped");
assert!(error.to_string().contains("not UTF-8"), "{error}");
fs::rename(&odd, directory.path().join("src").join(OsStr::from_bytes(b"lat\xffin.txt"))).expect("renamed");
let listed = walk_rust_files(&root.join("src")).expect("a file that is not Rust says nothing about the walk");
assert_eq!(listed, vec![root.join("src/lib.rs")]);
}
fn survey(root: &Utf8Path, args: SelectArgs) -> Survey {
Survey::new(
&SelectArgs {
dir: root.to_owned(),
..args
},
None,
)
.expect("the fixture workspace must survey")
}
#[test]
fn an_unknown_suppression_directive_fails_the_scan_rather_than_being_ignored() {
let (_directory, root) = workspace();
write(
&root,
"core/src/lib.rs",
"#[gamma::note]\npub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n",
);
let survey = survey(&root, SelectArgs::default());
let mut ordinals = 0;
let error = survey
.scan(None, &Selection::parse("all").expect("every mutator resolves"), &mut ordinals)
.expect_err("the directive names no known intent");
assert!(error.to_string().contains("unknown directive"), "{error}");
}
#[test]
fn a_file_too_deep_to_walk_is_left_out_by_name_rather_than_stopping_the_scan() {
let (_directory, root) = workspace();
let depth = 512;
let deep = format!("pub fn deep() -> i32 {{\n {}1{}\n}}\n", "(".repeat(depth), ")".repeat(depth));
write(&root, "core/src/deep.rs", &deep);
write(
&root,
"core/src/lib.rs",
"mod deep;\n\npub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n",
);
let survey = survey(&root, SelectArgs::default());
let mut ordinals = 0;
let scanned = survey
.scan(None, &Selection::parse("all").expect("every mutator resolves"), &mut ordinals)
.expect("a file this tool cannot walk does not make the workspace unmeasurable");
assert_eq!(scanned.skipped.len(), 1, "{:?}", scanned.skipped);
let note = scanned.skipped.first().expect("one file was skipped");
assert!(note.contains("deep.rs"), "the skip does not name the file: {note}");
assert!(note.contains("nests deeper"), "the skip does not say why: {note}");
assert!(
scanned.mutants.iter().any(|mutant| mutant.file.as_str().ends_with("lib.rs")),
"{:?}",
scanned.mutants.iter().map(|mutant| mutant.file.as_str()).collect::<Vec<_>>()
);
}
#[test]
fn a_file_too_deep_to_walk_is_skipped_the_same_way_when_only_its_declarations_are_needed() {
let (_directory, root) = workspace();
let depth = 512;
let deep = format!("pub fn deep() -> i32 {{\n {}1{}\n}}\n", "(".repeat(depth), ")".repeat(depth));
write(&root, "core/src/deep.rs", &deep);
write(
&root,
"core/src/lib.rs",
"mod deep;\n\npub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n",
);
let scan_with = |args: SelectArgs| {
let survey = survey(&root, args);
let mut ordinals = 0;
survey
.scan(None, &Selection::parse("all").expect("every mutator resolves"), &mut ordinals)
.expect("a file this tool cannot walk does not make the workspace unmeasurable")
};
let wide = scan_with(SelectArgs::default());
let narrow = scan_with(SelectArgs {
files: vec!["core/src/lib.rs".to_owned()],
..SelectArgs::default()
});
assert_eq!(narrow.skipped, wide.skipped, "the same file must be reported either way");
assert_eq!(narrow.skipped.len(), 1, "{:?}", narrow.skipped);
let note = narrow.skipped.first().expect("one file was skipped");
assert!(note.contains("deep.rs"), "the skip does not name the file: {note}");
assert!(note.contains("nests deeper"), "the skip does not say why: {note}");
assert!(
narrow.mutants.iter().any(|mutant| mutant.file.as_str().ends_with("lib.rs")),
"{:?}",
narrow.mutants.iter().map(|mutant| mutant.file.as_str()).collect::<Vec<_>>()
);
}
#[test]
fn a_stated_value_that_cannot_be_honoured_fails_the_scan_rather_than_being_ignored() {
let (_directory, root) = workspace();
write(
&root,
"core/src/lib.rs",
"#[gamma::value(1, 2)]\npub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n",
);
let survey = survey(&root, SelectArgs::default());
let mut ordinals = 0;
let error = survey
.scan(None, &Selection::parse("all").expect("every mutator resolves"), &mut ordinals)
.expect_err("two expressions are not one value");
assert!(error.to_string().contains("states one value"), "{error}");
assert!(error.to_string().contains("core/src/lib.rs:1"), "{error}");
}
#[test]
fn a_stated_value_reaches_the_population_as_a_mutant_of_its_own() {
let (_directory, root) = workspace();
write(
&root,
"core/src/lib.rs",
"#[gamma::value(a - b)]\npub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n",
);
let survey = survey(&root, SelectArgs::default());
let mut ordinals = 0;
let scanned = survey
.scan(None, &Selection::parse("fn_value").expect("every mutator resolves"), &mut ordinals)
.expect("a well-formed stated value is not an error");
let stated: Vec<&Mutant> = scanned
.mutants
.iter()
.filter(|mutant| &*mutant.mutator == "fn_value.stated")
.collect();
assert_eq!(stated.len(), 1, "{:?}", scanned.mutants);
assert_eq!(stated[0].replacement, "a - b");
}
#[test]
fn files_reached_twice_are_listed_once_and_test_only_modules_are_dropped() {
let (_directory, root) = wide_workspace();
let survey = survey(&root, SelectArgs::default());
let mut ordinals = 0;
let scanned = survey
.scan(None, &Selection::parse("@all").expect("selection"), &mut ordinals)
.expect("scan");
let plan = survey.into_plan(scanned);
let listed: Vec<&Utf8PathBuf> = plan.files.iter().map(|file| &file.path).collect();
let core_lib = Utf8PathBuf::from("core/src/lib.rs");
assert_eq!(listed.iter().filter(|path| ****path == core_lib).count(), 1, "{listed:?}");
assert!(
!plan.mutants.iter().any(|mutant| mutant.file.as_str().ends_with("helpers.rs")),
"{:?}",
plan.mutants.iter().map(|mutant| &mutant.file).collect::<Vec<_>>()
);
}
#[test]
fn a_package_reached_by_two_paths_is_walked_once() {
let (_directory, root) = wide_workspace();
let survey = survey(&root, SelectArgs::default());
let mut ordinals = 0;
let scanned = survey
.scan(None, &Selection::parse("@all").expect("selection"), &mut ordinals)
.expect("scan");
let plan = survey.into_plan(scanned);
let reached = plan.reach.get("app").expect("app should have a reachable set");
assert!(reached.contains("core"), "{reached:?}");
assert!(reached.contains("mid"), "{reached:?}");
}
#[test]
fn every_test_target_in_the_workspace_is_named() {
let (_directory, root) = workspace();
let survey = survey(&root, SelectArgs::default());
assert!(survey.tests.contains(&"it".to_owned()), "{:?}", survey.tests);
assert!(survey.tests.contains(&"core".to_owned()), "{:?}", survey.tests);
assert!(survey.tests.contains(&"app".to_owned()), "{:?}", survey.tests);
assert!(!survey.tests.contains(&"demo".to_owned()), "{:?}", survey.tests);
}
#[test]
fn a_proc_macro_target_yields_no_mutants_while_its_neighbours_still_do() {
let (_directory, root) = workspace();
write(
&root,
"Cargo.toml",
"[workspace]\nmembers = [\"core\", \"app\", \"macros\"]\nresolver = \"3\"\n",
);
write(
&root,
"macros/Cargo.toml",
"[package]\nname = \"macros\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\nproc-macro = true\n",
);
write(&root, "macros/src/lib.rs", "pub fn widen(x: i32) -> i32 {\n x + 1\n}\n");
let survey = survey(&root, SelectArgs::default());
let mut ordinals = 0;
let scanned = survey
.scan(None, &Selection::parse("all").expect("every mutator resolves"), &mut ordinals)
.expect("the fixture must scan");
let mutated: HashSet<&str> = scanned.mutants.iter().map(|mutant| mutant.file.as_str()).collect();
assert!(
!mutated.iter().any(|file| file.contains("macros")),
"a proc-macro target must not be mutated: {mutated:?}"
);
assert!(
mutated.iter().any(|file| file.contains("core")),
"ordinary libraries must still be mutated: {mutated:?}"
);
}
#[test]
fn the_guard_runtime_yields_no_mutants_while_its_neighbours_still_do() {
let (_directory, root) = workspace();
write(
&root,
"Cargo.toml",
"[workspace]\nmembers = [\"core\", \"app\", \"rt\"]\nresolver = \"3\"\n",
);
write(
&root,
"rt/Cargo.toml",
&format!(
"[package]\nname = \"rt\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\nname = \"{}\"\n",
crate::exec::RUNTIME_CRATE
),
);
write(&root, "rt/src/lib.rs", "pub fn widen(x: i32) -> i32 {\n x + 1\n}\n");
let survey = survey(&root, SelectArgs::default());
let mut ordinals = 0;
let scanned = survey
.scan(None, &Selection::parse("all").expect("every mutator resolves"), &mut ordinals)
.expect("the fixture must scan");
let mutated: HashSet<&str> = scanned.mutants.iter().map(|mutant| mutant.file.as_str()).collect();
assert!(
!mutated.iter().any(|file| file.contains("rt")),
"the guard runtime must not be mutated: {mutated:?}"
);
assert!(
mutated.iter().any(|file| file.contains("core")),
"ordinary libraries must still be mutated: {mutated:?}"
);
}
#[test]
fn test_targets_are_named_even_for_packages_left_unmutated() {
let (_directory, root) = workspace();
let args = SelectArgs {
dir: root,
packages: vec!["core".to_owned()],
..SelectArgs::default()
};
let survey = Survey::new(&args, None).expect("survey");
assert!(survey.tests.contains(&"it".to_owned()), "{:?}", survey.tests);
}
#[test]
fn a_file_filter_still_recognizes_a_test_only_module_declared_outside_the_filter() {
let (_directory, root) = workspace();
write(
&root,
"core/src/lib.rs",
"#[cfg(test)]\n#[path = \"reader_tests.rs\"]\nmod tests;\n",
);
write(
&root,
"core/src/reader_tests.rs",
"#[test]\nfn reads() {\n assert_eq!(1 + 2, 3);\n}\n",
);
let survey = survey(
&root,
SelectArgs {
files: vec!["core/src/reader_tests.rs".to_owned()],
..SelectArgs::default()
},
);
let mut ordinals = 0;
let scanned = survey
.scan(None, &Selection::parse("@all").expect("selection"), &mut ordinals)
.expect("scan");
assert!(scanned.mutants.is_empty(), "{:?}", scanned.mutants);
}
#[test]
fn a_package_that_is_not_in_the_workspace_is_a_usage_error() {
let (_directory, root) = workspace();
let args = SelectArgs {
dir: root,
packages: vec!["nosuch".to_owned()],
..SelectArgs::default()
};
let error = Survey::new(&args, None).expect_err("an unknown package must not survey");
assert!(error.to_string().contains("nosuch"), "{error}");
assert!(error.is_usage(), "{error}");
}
#[test]
fn naming_one_package_leaves_the_others_alone() {
let (_directory, root) = workspace();
let plan = survey(
&root,
SelectArgs {
packages: vec!["core".to_owned()],
..SelectArgs::default()
},
);
assert!(plan.files.iter().all(|file| file.package == "core"), "{:?}", plan.files);
}
#[test]
fn a_pattern_naming_another_package_is_still_matched_when_one_package_is_named() {
let (_directory, root) = workspace();
let args = SelectArgs {
dir: root,
packages: vec!["core".to_owned()],
exclude_files: vec!["app/**".to_owned()],
..SelectArgs::default()
};
let plan = Survey::new(&args, None).expect("a pattern naming an unselected package must not be an error");
assert!(plan.files.iter().all(|file| file.package == "core"), "{:?}", plan.files);
}
#[test]
fn a_pattern_matching_nothing_in_the_whole_workspace_is_still_an_error() {
let (_directory, root) = workspace();
let args = SelectArgs {
dir: root,
packages: vec!["core".to_owned()],
exclude_files: vec!["nosuch/**".to_owned()],
..SelectArgs::default()
};
let error = Survey::new(&args, None).expect_err("a pattern matching nothing must not survey");
assert!(error.to_string().contains("nosuch/**"), "{error}");
assert!(error.is_usage(), "{error}");
}
#[test]
fn a_test_or_example_target_is_never_mutated() {
let (_directory, root) = workspace();
let plan = survey(&root, SelectArgs::default());
let paths: Vec<&str> = plan.files.iter().map(|file| file.path.as_str()).collect();
assert!(!paths.iter().any(|path| path.contains("examples")), "{paths:?}");
assert!(!paths.iter().any(|path| path.contains("tests")), "{paths:?}");
assert!(paths.iter().any(|path| path.ends_with("main.rs")), "{paths:?}");
}
#[test]
fn an_excluded_file_is_dropped_before_it_is_parsed() {
let (_directory, root) = workspace();
let plan = survey(
&root,
SelectArgs {
exclude_files: vec!["**/generated.rs".to_owned()],
..SelectArgs::default()
},
);
assert!(
!plan.files.iter().any(|file| file.path.as_str().ends_with("generated.rs")),
"{:?}",
plan.files
);
}
#[test]
fn a_diff_that_cannot_be_read_fails_the_survey_rather_than_being_ignored() {
let (_directory, root) = workspace();
let error = Survey::new(
&SelectArgs {
dir: root.clone(),
in_diff: Some(root.join("no-such.patch")),
..SelectArgs::default()
},
None,
)
.expect_err("the diff file does not exist");
assert!(error.to_string().contains("no-such.patch"), "{error}");
}
#[test]
fn a_file_the_diff_never_mentions_is_dropped_before_it_is_parsed() {
let (_directory, root) = workspace();
write(
&root,
"change.patch",
"--- a/core/src/lib.rs\n+++ b/core/src/lib.rs\n@@ -1,2 +1,2 @@\n one\n+ a + b\n",
);
let plan = survey(
&root,
SelectArgs {
in_diff: Some(root.join("change.patch")),
..SelectArgs::default()
},
);
assert_eq!(plan.files.len(), 1, "{:?}", plan.files);
assert!(plan.files[0].path.as_str().ends_with("lib.rs"), "{:?}", plan.files);
}
#[test]
fn a_diff_written_with_mnemonic_prefixes_selects_the_same_files() {
let (_directory, root) = workspace();
write(
&root,
"change.patch",
"diff --git i/core/src/lib.rs w/core/src/lib.rs\n--- i/core/src/lib.rs\n+++ w/core/src/lib.rs\n@@ -1,2 +1,2 @@\n one\n+ a + b\n",
);
let plan = survey(
&root,
SelectArgs {
in_diff: Some(root.join("change.patch")),
..SelectArgs::default()
},
);
assert_eq!(plan.files.len(), 1, "{:?}", plan.files);
assert!(plan.files[0].path.as_str().ends_with("lib.rs"), "{:?}", plan.files);
}
#[test]
fn a_diff_that_names_nothing_in_this_workspace_fails_the_survey() {
let (_directory, root) = workspace();
write(
&root,
"change.patch",
"--- a/elsewhere/src/lib.rs\n+++ b/elsewhere/src/lib.rs\n@@ -1,2 +1,2 @@\n one\n+ a + b\n",
);
let error = Survey::new(
&SelectArgs {
dir: root.clone(),
in_diff: Some(root.join("change.patch")),
..SelectArgs::default()
},
None,
)
.expect_err("a diff that names no file here must not pass for an empty change");
assert!(error.is_usage(), "{error}");
assert!(error.to_string().contains("elsewhere/src/lib.rs"), "{error}");
}
#[test]
fn a_diff_that_touches_no_source_file_selects_nothing_without_failing() {
let (_directory, root) = workspace();
write(
&root,
"change.patch",
"--- a/Cargo.toml\n+++ b/Cargo.toml\n@@ -1,2 +1,2 @@\n one\n+two\n",
);
let plan = survey(
&root,
SelectArgs {
in_diff: Some(root.join("change.patch")),
..SelectArgs::default()
},
);
assert!(plan.files.is_empty(), "{:?}", plan.files);
}
#[test]
fn a_changed_file_still_only_yields_mutants_on_the_changed_lines() {
let (_directory, root) = workspace();
write(
&root,
"core/src/lib.rs",
"pub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n\npub fn sub(a: i32, b: i32) -> i32 {\n a - b\n}\n",
);
write(
&root,
"change.patch",
"--- a/core/src/lib.rs\n+++ b/core/src/lib.rs\n@@ -1,2 +1,2 @@\n head\n+ a + b\n",
);
let plan = survey(
&root,
SelectArgs {
in_diff: Some(root.join("change.patch")),
packages: vec!["core".to_owned()],
..SelectArgs::default()
},
);
let mut ordinals = 0;
let scanned = plan
.scan(None, &Selection::parse("all").expect("every mutator resolves"), &mut ordinals)
.expect("the fixture must scan");
assert!(!scanned.mutants.is_empty(), "the changed line yields nothing");
assert!(
scanned.mutants.iter().all(|mutant| mutant.line <= 2 && mutant.end_line >= 2),
"{:?}",
scanned.mutants
);
assert!(
scanned.mutants.iter().all(|mutant| &*mutant.item_path == "add"),
"{:?}",
scanned.mutants
);
}
#[test]
fn a_mutant_is_selected_by_its_whole_extent_not_just_its_first_line() {
let (_directory, root) = workspace();
write(
&root,
"core/src/lib.rs",
"pub fn add(a: i32, b: i32, c: i32) -> i32 {\n a\n + b\n + c\n}\n",
);
write(
&root,
"change.patch",
"--- a/core/src/lib.rs\n+++ b/core/src/lib.rs\n@@ -2,3 +2,3 @@\n a\n- + x\n+ + b\n + c\n",
);
let plan = survey(
&root,
SelectArgs {
in_diff: Some(root.join("change.patch")),
packages: vec!["core".to_owned()],
..SelectArgs::default()
},
);
let mut ordinals = 0;
let scanned = plan
.scan(None, &Selection::parse("all").expect("every mutator resolves"), &mut ordinals)
.expect("the fixture must scan");
assert!(!scanned.mutants.is_empty(), "a site edited on an interior line was dropped");
assert!(
scanned.mutants.iter().any(|mutant| mutant.line == 2 && mutant.end_line >= 3),
"no multi-line site reached the touched interior line: {:?}",
scanned.mutants
);
assert!(
scanned.mutants.iter().all(|mutant| mutant.line <= 3 && mutant.end_line >= 3),
"{:?}",
scanned.mutants
);
}
#[test]
fn a_mutant_an_earlier_report_settled_is_kept_with_its_verdict_and_never_run() {
let (_directory, root) = workspace();
let mut plan = survey(
&root,
SelectArgs {
packages: vec!["core".to_owned()],
..SelectArgs::default()
},
);
let selection = Selection::parse("all").expect("every mutator resolves");
let mut ordinals = 0;
let first = plan.scan(None, &selection, &mut ordinals).expect("the fixture must scan");
let settled: HashMap<MutantId, Outcome> = first.mutants.iter().map(|mutant| (mutant.id.clone(), Outcome::Killed)).collect();
assert!(!settled.is_empty(), "the fixture yielded no mutants to settle");
plan.settle(settled.clone());
let mut ordinals = 0;
let second = plan.scan(None, &selection, &mut ordinals).expect("the fixture must scan");
assert_eq!(second.mutants.len(), first.mutants.len());
assert_eq!(second.settled_out, settled.len());
assert_eq!(ordinals, 0, "a settled mutant is not work and must take no ordinal");
assert!(
second.mutants.iter().all(|mutant| mutant.outcome == Outcome::Killed),
"{:?}",
second.mutants
);
assert_eq!(crate::model::Summary::of(&second.mutants).scored(), Some(100.0));
}
#[test]
fn an_unsettled_mutant_is_still_work_in_an_iterative_run() {
let (_directory, root) = workspace();
let mut plan = survey(
&root,
SelectArgs {
packages: vec!["core".to_owned()],
..SelectArgs::default()
},
);
let selection = Selection::parse("all").expect("every mutator resolves");
let mut ordinals = 0;
let first = plan.scan(None, &selection, &mut ordinals).expect("the fixture must scan");
let live: Vec<&Mutant> = first.mutants.iter().filter(|mutant| mutant.ordinal > 0).collect();
assert!(live.len() > 1, "the fixture needs more than one live mutant");
let settled: HashMap<MutantId, Outcome> = live.iter().skip(1).map(|mutant| (mutant.id.clone(), Outcome::Killed)).collect();
plan.settle(settled.clone());
let mut ordinals = 0;
let second = plan.scan(None, &selection, &mut ordinals).expect("the fixture must scan");
assert_eq!(second.mutants.len(), first.mutants.len());
assert_eq!(second.settled_out, settled.len());
assert_eq!(ordinals, 1, "only the mutant that is still open is work");
}
#[test]
fn a_feature_selection_is_carried_into_the_metadata_it_surveys() {
let (_directory, root) = workspace();
for features in [
FeatureArgs {
all_features: true,
..FeatureArgs::default()
},
FeatureArgs {
no_default_features: true,
..FeatureArgs::default()
},
FeatureArgs {
features: vec!["core/extra, ".to_owned()],
..FeatureArgs::default()
},
] {
let metadata = load_metadata(&root, &features).expect("the fixture workspace must produce metadata");
assert_eq!(metadata.workspace_packages().len(), 2, "{features:?}");
}
}
fn gated_workspace() -> (TempDir, Utf8PathBuf) {
let directory = TempDir::new().expect("a temporary directory");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the temporary path is not UTF-8");
write(&root, "Cargo.toml", "[workspace]\nmembers = [\"gated\"]\nresolver = \"3\"\n");
write(
&root,
"gated/Cargo.toml",
concat!(
"[package]\nname = \"gated\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n",
"[features]\ncli = []\n\n",
"[lib]\nname = \"gated\"\npath = \"src/lib.rs\"\n\n",
"[[bin]]\nname = \"gated-cli\"\npath = \"cli/main.rs\"\nrequired-features = [\"cli\"]\n\n",
"[[bin]]\nname = \"gated-plain\"\npath = \"plain/main.rs\"\n",
),
);
write(
&root,
"gated/src/lib.rs",
concat!(
"pub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n\n",
"#[cfg(feature = \"cli\")]\npub fn only_with_cli(a: i32, b: i32) -> i32 {\n a + b\n}\n",
),
);
write(&root, "gated/cli/main.rs", "fn main() {\n let _ = 1 + 1;\n}\n");
write(&root, "gated/plain/main.rs", "fn main() {\n let _ = 2 + 2;\n}\n");
(directory, root)
}
#[test]
fn a_binary_whose_required_features_are_off_is_not_surveyed() {
let (_directory, root) = gated_workspace();
let survey = survey(&root, SelectArgs::default());
let listed: Vec<&Utf8PathBuf> = survey.files.iter().map(|file| &file.path).collect();
assert!(listed.contains(&&Utf8PathBuf::from("gated/src/lib.rs")), "{listed:?}");
assert!(listed.contains(&&Utf8PathBuf::from("gated/plain/main.rs")), "{listed:?}");
assert!(!listed.contains(&&Utf8PathBuf::from("gated/cli/main.rs")), "{listed:?}");
}
#[test]
fn a_binary_whose_required_features_are_on_is_surveyed_like_any_other() {
let (_directory, root) = gated_workspace();
let survey = survey(
&root,
SelectArgs {
features: FeatureArgs {
all_features: true,
..FeatureArgs::default()
},
..SelectArgs::default()
},
);
let listed: Vec<&Utf8PathBuf> = survey.files.iter().map(|file| &file.path).collect();
assert!(listed.contains(&&Utf8PathBuf::from("gated/src/lib.rs")), "{listed:?}");
assert!(listed.contains(&&Utf8PathBuf::from("gated/plain/main.rs")), "{listed:?}");
assert!(listed.contains(&&Utf8PathBuf::from("gated/cli/main.rs")), "{listed:?}");
}
#[test]
fn a_feature_selector_in_the_passthrough_arguments_reaches_the_closure() {
let (_directory, root) = gated_workspace();
let selected = CargoOptions {
extra: vec!["--features".to_owned(), "cli".to_owned()],
..CargoOptions::default()
};
let survey = Survey::for_build(
&SelectArgs {
dir: root.clone(),
..SelectArgs::default()
},
None,
&selected,
)
.expect("the fixture workspace must survey");
let listed: Vec<&Utf8PathBuf> = survey.files.iter().map(|file| &file.path).collect();
assert!(listed.contains(&&Utf8PathBuf::from("gated/cli/main.rs")), "{listed:?}");
let items = mutated_items(&root, &selected);
assert!(items.iter().any(|item| item.contains("only_with_cli")), "{items:?}");
let plain = mutated_items(&root, &CargoOptions::default());
assert!(!plain.iter().any(|item| item.contains("only_with_cli")), "{plain:?}");
}
#[test]
fn a_required_feature_decides_a_binary_only_when_the_closure_can_answer() {
let (_directory, root) = gated_workspace();
let metadata = load_metadata(&root, &FeatureArgs::default()).expect("the fixture workspace must produce metadata");
let packages = metadata.workspace_packages();
let package = packages.first().expect("the fixture workspace has one member");
let gated = package
.targets
.iter()
.find(|target| target.name == "gated-cli")
.expect("the fixture declares the gated binary");
assert!(
!is_mutable_target(gated, Some(&Vec::new())),
"cargo does not build it with the feature off"
);
assert!(is_mutable_target(gated, Some(&vec!["cli".to_owned()])));
assert!(is_mutable_target(gated, None));
let mut cross = (*gated).clone();
cross.required_features = vec!["serde/derive".to_owned()];
assert!(is_mutable_target(&cross, Some(&Vec::new())));
}
fn predicate_workspace() -> (TempDir, Utf8PathBuf) {
let directory = TempDir::new().expect("a temporary directory");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the temporary path is not UTF-8");
write(&root, "Cargo.toml", "[workspace]\nmembers = [\"probe\"]\nresolver = \"3\"\n");
write(
&root,
"probe/Cargo.toml",
"[package]\nname = \"probe\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
);
write(
&root,
"probe/src/lib.rs",
concat!(
"#[cfg(debug_assertions)]\npub fn debug_only(a: i32, b: i32) -> i32 {\n a + b\n}\n\n",
"#[cfg(not(debug_assertions))]\npub fn release_only(a: i32, b: i32) -> i32 {\n a + b\n}\n\n",
"#[cfg(target_os = \"solaris\")]\npub fn solaris_only(a: i32, b: i32) -> i32 {\n a + b\n}\n\n",
"#[cfg(gamma_probe)]\npub fn custom_only(a: i32, b: i32) -> i32 {\n a + b\n}\n",
),
);
(directory, root)
}
fn mutated_items(root: &Utf8Path, cargo: &CargoOptions) -> Vec<String> {
mutated_items_with_assertions(root, cargo, None)
}
fn mutated_items_with_assertions(root: &Utf8Path, cargo: &CargoOptions, debug_assertions: Option<bool>) -> Vec<String> {
let mut survey = Survey::for_build(
&SelectArgs {
dir: root.to_owned(),
..SelectArgs::default()
},
None,
cargo,
)
.expect("the fixture workspace must survey");
if let Some(debug_assertions) = debug_assertions {
let mut features = HashMap::default();
let _old = features.insert("probe".to_owned(), Vec::new());
survey.cfgs = Cfgs::new(
&crate::cfg::for_build(&crate::cfg::Build {
debug_assertions: Some(debug_assertions),
..crate::cfg::Build::default()
})
.expect("the host compiler must describe its cfg predicates"),
&features,
);
}
let mut ordinals = 0;
let scanned = survey
.scan(None, &Selection::parse("arith.add_to_sub").expect("selection"), &mut ordinals)
.expect("the fixture must scan");
scanned.mutants.iter().map(|mutant| mutant.item_path.to_string()).collect()
}
#[test]
fn the_profile_decides_which_half_of_a_debug_assertions_gate_is_surveyed() {
let (_directory, root) = predicate_workspace();
let debug = mutated_items_with_assertions(&root, &CargoOptions::default(), Some(true));
let release = mutated_items_with_assertions(
&root,
&CargoOptions {
profile: Some("release".to_owned()),
..CargoOptions::default()
},
Some(false),
);
assert!(debug.iter().any(|item| item.contains("debug_only")), "{debug:?}");
assert!(!debug.iter().any(|item| item.contains("release_only")), "{debug:?}");
assert!(release.iter().any(|item| item.contains("release_only")), "{release:?}");
assert!(!release.iter().any(|item| item.contains("debug_only")), "{release:?}");
}
#[test]
fn a_cross_target_build_surveys_the_code_that_target_compiles() {
let (_directory, root) = predicate_workspace();
let host = mutated_items(&root, &CargoOptions::default());
let elsewhere = mutated_items(
&root,
&CargoOptions {
extra: vec!["--target".to_owned(), "x86_64-pc-solaris".to_owned()],
..CargoOptions::default()
},
);
assert!(!host.iter().any(|item| item.contains("solaris_only")), "{host:?}");
assert!(elsewhere.iter().any(|item| item.contains("solaris_only")), "{elsewhere:?}");
}
#[test]
fn a_custom_predicate_from_the_build_flags_is_surveyed_as_present() {
const CHILD: &str = "CARGO_GAMMA_CONFIG_RUSTFLAGS_TEST_CHILD";
if std::env::var_os(CHILD).is_none() {
let status = std::process::Command::new(std::env::current_exe().expect("the test executable has a path"))
.args([
"--exact",
"discover::survey::tests::a_custom_predicate_from_the_build_flags_is_surveyed_as_present",
"--nocapture",
])
.env(CHILD, "1")
.env_remove("CARGO_ENCODED_RUSTFLAGS")
.env_remove("RUSTFLAGS")
.env_remove("CARGO_BUILD_RUSTFLAGS")
.status()
.expect("the isolated test process runs");
assert!(status.success(), "{status}");
return;
}
let (_directory, root) = predicate_workspace();
let plain = mutated_items(&root, &CargoOptions::default());
assert!(!plain.iter().any(|item| item.contains("custom_only")), "{plain:?}");
write(&root, ".cargo/config.toml", "[build]\nrustflags = [\"--cfg\", \"gamma_probe\"]\n");
let flagged = mutated_items(&root, &CargoOptions::default());
assert!(flagged.iter().any(|item| item.contains("custom_only")), "{flagged:?}");
}
#[test]
fn metadata_that_cannot_be_read_names_the_directory() {
let directory = TempDir::new().expect("a temporary directory");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the temporary path is not UTF-8");
let error = load_metadata(&root, &FeatureArgs::default()).expect_err("a directory with no manifest has no metadata");
assert!(error.to_string().contains(root.as_str()), "{error}");
}
}