use std::collections::btree_map::Entry;
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::io::Write;
use camino::{Utf8Path, Utf8PathBuf};
use super::cli::UnsuppressArgs;
use super::dispatch::EXIT_OK;
use super::host::Host;
use super::suppress::{Written, recoverable, reject_external_sources, reverted};
use crate::discover::Plan;
use crate::error::error;
use crate::exec::CargoOptions;
use crate::report::{Styler, quantity};
#[cfg(test)]
pub(super) fn unsuppress<H: Host>(host: &mut H, args: &UnsuppressArgs, styler: Styler) -> crate::Result<i32> {
let config = crate::config::Config::resolve(&args.select)?;
let cargo = config.cargo_options();
unsuppress_with_cargo(host, args, styler, &cargo)
}
pub(super) fn unsuppress_with_cargo<H: Host>(
host: &mut H,
args: &UnsuppressArgs,
styler: Styler,
cargo: &CargoOptions,
) -> crate::Result<i32> {
let selection = args.select.selection()?;
let before = crate::discover::plan_for_build(&args.select, &selection, args.select.shard()?, cargo, &mut |_| {})?;
if before.idle.is_empty() {
writeln!(
host.error(),
"{} nothing to remove: every skip directive in scope suppressed something",
styler.verb("Finished")
)?;
return Ok(EXIT_OK);
}
let (removable, declined) = sort_out(&before)?;
report_declined(host, &declined, styler)?;
if removable.is_empty() {
return Ok(EXIT_OK);
}
let written = remove_all(host, args, &before, &removable)?;
let removed: usize = removable.values().map(|removal| removal.lines.len()).sum();
if !args.apply {
writeln!(
host.error(),
"{} {} in {} would be removed; pass `--apply` to do it",
styler.verb("Preview"),
quantity(removed, "skip directive"),
quantity(removable.len(), "file")
)?;
return Ok(EXIT_OK);
}
verify_or_revert_with_cargo(host, args, &before, removed, written, styler, cargo)
}
fn remove_all<H: Host>(host: &mut H, args: &UnsuppressArgs, before: &Plan, removable: &Removals) -> crate::Result<Written> {
if args.apply {
let paths: Vec<&Utf8Path> = removable.keys().map(Utf8PathBuf::as_path).collect();
reject_external_sources(&before.root, &paths)?;
recoverable(&before.root, &paths, args.allow_dirty)?;
}
let mut written = Written::new();
match remove_directives(host, args, before, removable, &mut written) {
Ok(()) => Ok(written),
Err(cause) => Err(reverted(&before.root, written, cause)),
}
}
fn remove_directives<H: Host>(
host: &mut H,
args: &UnsuppressArgs,
before: &Plan,
removable: &Removals,
written: &mut Written,
) -> crate::Result<()> {
for (path, removal) in removable {
let absolute = before.root.join(path);
let source = crate::parse::strip_bom(&removal.text);
let mut after = crate::fix::remove(source, &removal.lines);
if removal.text.len() != source.len() {
after.insert(0, crate::parse::BOM);
}
let _ = syn::parse_file(&after)
.map_err(|cause| error!("removing the directives would leave {absolute} unparseable").caused_by(cause))?;
if args.apply {
let destination = crate::paths::require_within(&absolute, &before.root, "a source edit")?;
let current = fs::read_to_string(&destination).map_err(|cause| error!("could not read `{absolute}`").caused_by(cause))?;
if current != removal.text {
return Err(error!(
"`{absolute}` changed since the run that planned this edit; nothing was removed from it"
));
}
match crate::elements::write_if_unchanged(&before.root, &destination, Some(¤t), &after)? {
crate::elements::Publication::Conflict => {
return Err(error!(
"`{absolute}` changed while this command was preparing to publish its edit; the editor's bytes were left alone"
));
}
crate::elements::Publication::Published => {
written.push(super::suppress::WrittenFile::new(destination, current, after));
}
crate::elements::Publication::PublishedUndurable(cause) => {
written.push(super::suppress::WrittenFile::new(destination, current, after));
return Err(cause);
}
}
} else {
write!(host.results(), "{}", crate::fix::diff(path, &removal.text, &after))?;
}
}
Ok(())
}
type Removals = BTreeMap<Utf8PathBuf, Removal>;
#[derive(Debug)]
struct Removal {
lines: BTreeSet<usize>,
text: String,
}
fn sort_out(plan: &Plan) -> crate::Result<(Removals, Vec<&crate::suppress::Idle>)> {
let mut removable = Removals::new();
let mut declined = Vec::new();
let mut sources: BTreeMap<&Utf8PathBuf, (String, Vec<String>)> = BTreeMap::new();
for idle in &plan.idle {
let (text, lines) = match sources.entry(&idle.file) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
let path = plan.root.join(&idle.file);
let text = fs::read_to_string(&path).map_err(|cause| error!("could not read `{path}`").caused_by(cause))?;
let source = crate::parse::strip_bom(&text);
let Some(recorded) = plan.digests.get(&idle.file) else {
return Err(error!(
"`{path}` has no recorded generation for this planned edit; the planned directive was left alone"
));
};
if crate::discover::digest(source.as_bytes()) != *recorded {
return Err(error!(
"`{path}` changed since the run that planned this edit; the planned directive was left alone. Re-run to plan against the file as it is now"
));
}
let split = source.lines().map(str::to_owned).collect();
entry.insert((text, split))
}
};
if idle
.line
.checked_sub(1)
.and_then(|index| lines.get(index))
.is_some_and(|line| crate::fix::removable(line))
{
let entry = removable.entry(idle.file.clone()).or_insert_with(|| Removal {
lines: BTreeSet::new(),
text: text.clone(),
});
let _ = entry.lines.insert(idle.line);
} else {
declined.push(idle);
}
}
Ok((removable, declined))
}
fn report_declined<H: Host>(host: &mut H, declined: &[&crate::suppress::Idle], styler: Styler) -> crate::Result<()> {
if declined.is_empty() {
return Ok(());
}
writeln!(
host.error(),
"{} {} share a line with something else and must be removed by hand",
styler.verb("Skipping"),
quantity(declined.len(), "skip directive")
)?;
for idle in declined {
writeln!(host.error(), " {}:{}: skip({})", idle.file, idle.line, idle.selectors)?;
}
Ok(())
}
#[cfg(test)]
fn verify_or_revert<H: Host>(
host: &mut H,
args: &UnsuppressArgs,
before: &Plan,
removed: usize,
written: Written,
styler: Styler,
) -> crate::Result<i32> {
let config = crate::config::Config::resolve(&args.select)?;
let cargo = config.cargo_options();
verify_or_revert_with_cargo(host, args, before, removed, written, styler, &cargo)
}
fn verify_or_revert_with_cargo<H: Host>(
host: &mut H,
args: &UnsuppressArgs,
before: &Plan,
removed: usize,
written: Written,
styler: Styler,
cargo: &CargoOptions,
) -> crate::Result<i32> {
let verified = (|| {
let selection = args.select.selection()?;
let after = crate::discover::plan_for_build(&args.select, &selection, args.select.shard()?, cargo, &mut |_| {})?;
Ok(crate::fix::verify(&before.mutants, &after.mutants, &BTreeSet::new()))
})();
let result = match verified {
Ok(result) => result,
Err(cause) => return Err(reverted(&before.root, written, cause)),
};
if result.is_clean() {
writeln!(
host.error(),
"{} {} from {}",
styler.verb("Removed"),
quantity(removed, "skip directive"),
quantity(written.len(), "file")
)?;
return Ok(EXIT_OK);
}
Err(reverted(
&before.root,
written,
error!(
"removing the directives changed what the run found ({} mutants stopped being suppressed, {} started)",
result.released.len(),
result.collateral.len()
),
))
}
#[cfg(test)]
#[cfg(not(miri))]
mod tests {
use super::*;
use crate::fixtures::crate_dir;
use crate::suppress::Idle;
use crate::testing::Sink;
#[cfg(unix)]
use crate::testing::workdir;
fn removal(source: &str) -> Removal {
Removal {
lines: core::iter::once(1).collect(),
text: source.to_owned(),
}
}
fn args(root: &Utf8PathBuf, apply: bool) -> UnsuppressArgs {
UnsuppressArgs {
select: crate::commands::SelectArgs {
dir: root.clone(),
..crate::commands::SelectArgs::default()
},
apply,
allow_dirty: false,
}
}
#[test]
fn a_preview_shows_the_removal_and_leaves_the_file_alone() {
let source = "// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n";
let (_dir, root) = crate_dir("unsuppress-preview-", source);
let mut host = Sink::default();
let code = unsuppress(&mut host, &args(&root, false), Styler::new(false)).expect("preview");
assert_eq!(code, EXIT_OK);
assert!(host.out().contains("-// #[gamma::skip(arith)]"), "{}", host.out());
assert!(host.err().contains("--apply"), "{}", host.err());
assert_eq!(fs::read_to_string(root.join("src/lib.rs")).expect("source"), source);
}
#[test]
fn applying_removes_the_directive_and_nothing_else() {
let source = "// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n";
let (_dir, root) = crate_dir("unsuppress-apply-", source);
let mut host = Sink::default();
let code = unsuppress(&mut host, &args(&root, true), Styler::new(false)).expect("apply");
assert_eq!(code, EXIT_OK);
assert_eq!(
fs::read_to_string(root.join("src/lib.rs")).expect("source"),
"pub fn f(a: i32) -> bool { a > 1 }\n"
);
assert!(host.err().contains("Removed 1 skip directive"), "{}", host.err());
}
#[cfg(unix)]
#[test]
fn applying_refuses_a_source_symlink_whose_referent_is_outside_the_workspace() {
let source = "// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n";
let (_workspace, root) = crate_dir("unsuppress-external-link-", source);
let external = workdir("unsuppress-external-referent-");
let outside = Utf8PathBuf::from_path_buf(external.path().join("outside.rs")).expect("UTF-8 path");
let link = root.join("src/lib.rs");
fs::write(&outside, source).expect("external source");
fs::remove_file(&link).expect("replace source with link");
std::os::unix::fs::symlink(outside.as_std_path(), link.as_std_path()).expect("source link");
let failure = unsuppress(&mut Sink::default(), &args(&root, true), Styler::new(false))
.expect_err("an external source referent must not be edited");
assert!(failure.to_string().contains("outside"), "{failure}");
assert_eq!(fs::read_to_string(&outside).expect("external source"), source);
}
#[test]
fn bom_prefixed_files_keep_the_bom_when_removing_first_and_later_directives() {
for (name, source, expected) in [
(
"first",
"\u{feff}// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n",
"\u{feff}pub fn f(a: i32) -> bool { a > 1 }\n",
),
(
"later",
"\u{feff}//! A crate.\n// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n",
"\u{feff}//! A crate.\npub fn f(a: i32) -> bool { a > 1 }\n",
),
] {
let (_dir, root) = crate_dir(&format!("unsuppress-bom-{name}-"), source);
let path = root.join("src/lib.rs");
let mut host = Sink::default();
let code = unsuppress(&mut host, &args(&root, true), Styler::new(false)).expect("apply");
let after = fs::read_to_string(path).expect("source");
assert_eq!(code, EXIT_OK, "{name}");
assert_eq!(after, expected, "{name}");
assert_eq!(after.chars().next(), Some(crate::parse::BOM), "{name}: {after:?}");
}
}
#[test]
fn a_verification_error_restores_every_removed_directive() {
let original = "// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n";
let (_dir, root) = crate_dir("unsuppress-verify-error-", original);
let path = root.join("src/lib.rs");
let removed = "pub fn f(a: i32) -> bool { a > 1 }\n";
fs::write(&path, removed).expect("edited");
let mut args = args(&root, true);
args.select.mutators = Some("not.a.mutator".to_owned());
let mut host = Sink::default();
let failure = verify_or_revert(
&mut host,
&args,
&Plan {
skipped: Vec::new(),
digests: crate::HashMap::default(),
root,
files: Vec::new(),
mutants: Vec::new(),
suppressed: 0,
idle: Vec::new(),
sharded_out: 0,
settled_out: 0,
reach: crate::HashMap::default(),
specs: crate::HashMap::default(),
},
1,
vec![super::super::suppress::WrittenFile::new(
path.clone(),
original.to_owned(),
removed.to_owned(),
)],
Styler::new(false),
)
.expect_err("selection must fail");
assert!(failure.to_string().contains("every edit has been reverted"), "{failure}");
assert_eq!(fs::read_to_string(path).expect("restored"), original);
}
#[test]
fn a_directive_that_still_suppresses_something_is_left_alone() {
let source = "// #[gamma::skip(arith)]\npub fn f(a: i32) -> i32 { a + 1 }\n";
let (_dir, root) = crate_dir("unsuppress-live-", source);
let mut host = Sink::default();
let code = unsuppress(&mut host, &args(&root, true), Styler::new(false)).expect("nothing");
assert_eq!(code, EXIT_OK);
assert!(host.err().contains("nothing to remove"), "{}", host.err());
assert_eq!(fs::read_to_string(root.join("src/lib.rs")).expect("source"), source);
}
#[test]
fn a_directive_that_shares_its_line_is_named_rather_than_removed() {
let source = "pub fn f(a: i32) -> bool { a > 1 } // #[gamma::skip(arith)]\n";
let (_dir, root) = crate_dir("unsuppress-declined-", source);
let mut host = Sink::default();
let code = unsuppress(&mut host, &args(&root, true), Styler::new(false)).expect("declined");
assert_eq!(code, EXIT_OK);
assert!(host.err().contains("by hand"), "{}", host.err());
assert!(host.err().contains("src/lib.rs:1"), "{}", host.err());
assert_eq!(fs::read_to_string(root.join("src/lib.rs")).expect("source"), source);
}
#[test]
fn several_directives_in_one_file_are_all_removed() {
let source =
"// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n// #[gamma::skip(arith)]\npub fn g(a: i32) -> bool { a < 1 }\n";
let (_dir, root) = crate_dir("unsuppress-several-", source);
let mut host = Sink::default();
let _ = unsuppress(&mut host, &args(&root, true), Styler::new(false)).expect("apply");
let text = fs::read_to_string(root.join("src/lib.rs")).expect("source");
assert!(!text.contains("gamma::skip"), "{text}");
assert!(text.contains("fn f") && text.contains("fn g"), "{text}");
}
#[test]
fn a_declined_directive_is_reported_with_its_place_and_selectors() {
let idle = Idle {
file: Utf8PathBuf::from("src/lib.rs"),
line: 7,
selectors: "arith".to_owned(),
reason: None,
};
let mut host = Sink::default();
report_declined(&mut host, &[&idle], Styler::new(false)).expect("note");
let text = host.err();
assert!(text.contains("1 skip directive"), "{text}");
assert!(text.contains("src/lib.rs:7"), "{text}");
assert!(text.contains("skip(arith)"), "{text}");
}
#[test]
fn nothing_declined_says_nothing() {
let mut host = Sink::default();
report_declined(&mut host, &[], Styler::new(false)).expect("note");
assert!(host.err().is_empty(), "{}", host.err());
}
#[test]
fn a_removal_that_changes_what_is_suppressed_puts_every_file_back() {
let source = "pub fn f(a: i32) -> i32 { a + 1 }\n";
let (_dir, root) = crate_dir("unsuppress-revert-", source);
let path = root.join("src/lib.rs");
fs::write(&path, "pub fn f(a: i32) -> i32 { a - 1 }\n").expect("edited");
let mut before = crate::discover::plan(
&crate::commands::SelectArgs {
dir: root.clone(),
..crate::commands::SelectArgs::default()
},
&crate::ops::registry::Selection::default_preset(),
None,
&mut |_| {},
)
.expect("plan");
before.mutants[0].suppression = Some(crate::model::Suppression {
channel: crate::model::Channel::Comment,
reason: None,
tag: None,
line: Some(1),
});
let mut host = Sink::default();
let written = vec![super::super::suppress::WrittenFile::new(
path.clone(),
source.to_owned(),
"pub fn f(a: i32) -> i32 { a - 1 }\n".to_owned(),
)];
let error = verify_or_revert(&mut host, &args(&root, true), &before, 1, written, Styler::new(false)).unwrap_err();
assert!(error.to_string().contains("reverted"), "{error}");
assert_eq!(fs::read_to_string(&path).expect("source"), source, "the file was not put back");
}
fn plan_at(root: &Utf8PathBuf) -> Plan {
Plan {
skipped: Vec::new(),
digests: crate::HashMap::default(),
root: root.clone(),
files: Vec::new(),
mutants: Vec::new(),
suppressed: 0,
idle: Vec::new(),
sharded_out: 0,
settled_out: 0,
reach: crate::HashMap::default(),
specs: crate::HashMap::default(),
}
}
#[test]
fn a_failure_on_the_second_file_puts_the_first_one_back_byte_for_byte() {
let source = "// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n";
let (_dir, root) = crate_dir("unsuppress-rollback-", source);
let first = root.join("src/lib.rs");
let original = fs::read(first.as_std_path()).expect("the original bytes");
let removable: Removals = [
(Utf8PathBuf::from("src/lib.rs"), removal(source)),
(Utf8PathBuf::from("src/zzz_gone.rs"), removal(source)),
]
.into_iter()
.collect();
let mut host = Sink::default();
let error = remove_all(&mut host, &args(&root, true), &plan_at(&root), &removable).expect_err("the second file");
assert!(error.to_string().contains("zzz_gone.rs"), "{error}");
assert!(error.to_string().contains("every edit has been reverted"), "{error}");
assert_eq!(
fs::read(first.as_std_path()).expect("the bytes afterwards"),
original,
"the first file was left with its directive removed"
);
}
#[test]
fn both_files_are_rewritten_when_nothing_fails() {
let source = "// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n";
let (_dir, root) = crate_dir("unsuppress-both-", source);
fs::write(root.join("src/other.rs"), source).expect("other");
let removable: Removals = [
(Utf8PathBuf::from("src/lib.rs"), removal(source)),
(Utf8PathBuf::from("src/other.rs"), removal(source)),
]
.into_iter()
.collect();
let mut host = Sink::default();
let written = remove_all(&mut host, &args(&root, true), &plan_at(&root), &removable).expect("both files");
assert_eq!(written.len(), 2);
assert!(!fs::read_to_string(root.join("src/lib.rs")).expect("lib").contains("gamma::skip"));
assert!(
!fs::read_to_string(root.join("src/other.rs"))
.expect("other")
.contains("gamma::skip")
);
}
#[test]
fn a_write_that_cannot_be_staged_leaves_the_source_untouched() {
let source = "// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n";
let (_dir, root) = crate_dir("unsuppress-partial-write-", source);
let path = root.join("src/lib.rs");
let original = fs::read(path.as_std_path()).expect("the original bytes");
let scratch = root.join(".blocked-stage");
fs::create_dir(scratch.as_std_path()).expect("block the staging file");
crate::elements::next_scratch_path(scratch);
let removable: Removals = core::iter::once((Utf8PathBuf::from("src/lib.rs"), removal(source))).collect();
let mut host = Sink::default();
let error = remove_all(&mut host, &args(&root, true), &plan_at(&root), &removable).expect_err("the write");
assert!(error.to_string().contains("lib.rs"), "{error}");
assert_eq!(
fs::read(path.as_std_path()).expect("the bytes afterwards"),
original,
"a failed write did not leave the source alone"
);
}
#[test]
fn a_file_that_changed_since_planning_is_left_alone_rather_than_edited_by_line_number() {
let source = "// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n";
let (_dir, root) = crate_dir("unsuppress-shifted-", source);
let path = root.join("src/lib.rs");
let mut plan = Plan {
idle: vec![Idle {
file: Utf8PathBuf::from("src/lib.rs"),
line: 1,
selectors: "arith".to_owned(),
reason: None,
}],
..plan_at(&root)
};
let _recorded = plan.digests.insert(
Utf8PathBuf::from("src/lib.rs"),
crate::discover::digest(crate::parse::strip_bom(source).as_bytes()),
);
let edited = "//! A crate.\n// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n";
fs::write(&path, edited).expect("the concurrent save");
let error = sort_out(&plan).expect_err("the changed file");
assert!(
error.to_string().contains("changed since the run that planned this edit"),
"{error}"
);
assert_eq!(
fs::read_to_string(&path).expect("the file afterwards"),
edited,
"the removal edited a file it had not examined"
);
}
#[test]
fn a_planned_idle_directive_replaced_with_a_live_one_is_retained() {
let idle = "// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n";
let live = "// #[gamma::skip(relational)]\npub fn f(a: i32) -> bool { a > 1 }\n";
let (_dir, root) = crate_dir("unsuppress-replaced-directive-", idle);
let path = root.join("src/lib.rs");
let mut plan = Plan {
idle: vec![Idle {
file: Utf8PathBuf::from("src/lib.rs"),
line: 1,
selectors: "arith".to_owned(),
reason: None,
}],
..plan_at(&root)
};
let _recorded = plan.digests.insert(
Utf8PathBuf::from("src/lib.rs"),
crate::discover::digest(crate::parse::strip_bom(idle).as_bytes()),
);
fs::write(&path, live).expect("replacement directive");
let error = sort_out(&plan).expect_err("the planned directive no longer exists");
assert!(error.to_string().contains("planned directive was left alone"), "{error}");
assert_eq!(fs::read_to_string(path).expect("source"), live);
}
#[test]
fn a_save_after_validation_and_before_unsuppress_publication_is_left_alone() {
let source = "// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n";
let (_dir, root) = crate_dir("unsuppress-publication-conflict-", source);
let path = root.join("src/lib.rs");
let editor = "//! saved by the editor\n// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n".to_owned();
let editor_path = path.clone();
let removable: Removals = core::iter::once((Utf8PathBuf::from("src/lib.rs"), removal(source))).collect();
crate::elements::before_next_publication(move |_| {
fs::write(editor_path, &editor).expect("the editor save");
});
let error = remove_all(&mut Sink::default(), &args(&root, true), &plan_at(&root), &removable)
.expect_err("the generation changed after validation");
assert!(
error.to_string().contains("changed while this command was preparing to publish"),
"{error}"
);
assert_eq!(
fs::read_to_string(path).expect("the editor's bytes"),
"//! saved by the editor\n// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n"
);
}
#[test]
fn a_post_rename_unsuppress_sync_failure_is_reverted() {
let source = "// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n";
let (_dir, root) = crate_dir("unsuppress-sync-failure-", source);
let path = root.join("src/lib.rs");
let removable: Removals = core::iter::once((Utf8PathBuf::from("src/lib.rs"), removal(source))).collect();
crate::elements::fail_next_directory_sync();
let error =
remove_all(&mut Sink::default(), &args(&root, true), &plan_at(&root), &removable).expect_err("the post-rename sync fails");
assert!(error.to_string().contains("injected directory sync failure"), "{error}");
assert!(error.to_string().contains("every edit has been reverted"), "{error}");
assert_eq!(fs::read_to_string(path).expect("directive restored"), source);
}
#[test]
fn removing_from_a_file_with_uncommitted_changes_is_refused_before_anything_is_deleted() {
let source = "// #[gamma::skip(arith)]\npub fn f(a: i32) -> bool { a > 1 }\n";
let (_dir, root) = crate_dir("unsuppress-dirty-", source);
let path = root.join("src/lib.rs");
let started = std::process::Command::new("git")
.arg("-C")
.arg(root.as_std_path())
.args(["init", "--quiet"])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
if !started.is_ok_and(|status| status.success()) {
return;
}
let removable: Removals = core::iter::once((Utf8PathBuf::from("src/lib.rs"), removal(source))).collect();
let mut arguments = args(&root, true);
let error = remove_all(&mut Sink::default(), &arguments, &plan_at(&root), &removable).expect_err("a dirty tree");
assert!(error.is_usage(), "{error}");
assert!(error.to_string().contains("--allow-dirty"), "{error}");
assert_eq!(
fs::read_to_string(&path).expect("afterwards"),
source,
"the refusal still edited the file"
);
arguments.allow_dirty = true;
let _ = remove_all(&mut Sink::default(), &arguments, &plan_at(&root), &removable).expect("the override");
assert!(!fs::read_to_string(&path).expect("afterwards").contains("gamma::skip"));
}
}