use std::fs;
use camino::{Utf8Path, Utf8PathBuf};
use toml_edit::{DocumentMut, Item, Table, Value};
use super::workspace::absolute;
use crate::Result;
use crate::error::error;
pub(crate) const RUNTIME_CRATE: &str = "gamma_rt";
pub(super) const RUNTIME_PACKAGE: &str = "cargo-gamma-rt";
const DEPENDENCY_TABLES: [&str; 3] = ["dependencies", "dev-dependencies", "build-dependencies"];
#[derive(Debug)]
pub(super) struct Manifest {
path: Utf8PathBuf,
document: DocumentMut,
changed: bool,
within: Utf8PathBuf,
}
impl Manifest {
pub(super) fn read(path: &Utf8Path) -> Result<Self> {
let text = fs::read_to_string(path.as_std_path()).map_err(|cause| error!("could not read `{path}`").caused_by(cause))?;
let document = text
.parse::<DocumentMut>()
.map_err(|cause| error!("could not parse `{path}`: {cause}"))?;
Ok(Self {
path: path.to_owned(),
document,
changed: false,
within: Utf8PathBuf::new(),
})
}
pub(super) fn save(&self) -> Result<()> {
if !self.changed {
return Ok(());
}
fs::write(self.path.as_std_path(), self.document.to_string())
.map_err(|cause| error!("could not update `{}`", self.path).caused_by(cause))
}
pub(super) fn anchor_paths(&mut self, original: &Utf8Path, within: &Utf8Path) {
self.within = within.to_owned();
for name in DEPENDENCY_TABLES {
self.anchor_table(name, original);
}
self.anchor_table("replace", original);
if let Some(patch) = self.document.get_mut("patch").and_then(Item::as_table_like_mut) {
let registries: Vec<String> = patch.iter().map(|(name, _entry)| name.to_owned()).collect();
for registry in registries {
if let Some(table) = patch.get_mut(®istry).and_then(Item::as_table_like_mut) {
anchor_dependencies(table, original, &self.within, &mut self.changed);
}
}
}
if let Some(targets) = self.document.get_mut("target").and_then(Item::as_table_like_mut) {
let platforms: Vec<String> = targets.iter().map(|(name, _entry)| name.to_owned()).collect();
for platform in platforms {
let Some(table) = targets.get_mut(&platform).and_then(Item::as_table_like_mut) else {
continue;
};
for name in DEPENDENCY_TABLES {
if let Some(dependencies) = table.get_mut(name).and_then(Item::as_table_like_mut) {
anchor_dependencies(dependencies, original, &self.within, &mut self.changed);
}
}
}
}
if let Some(workspace) = self.document.get_mut("workspace").and_then(Item::as_table_like_mut)
&& let Some(dependencies) = workspace.get_mut("dependencies").and_then(Item::as_table_like_mut)
{
anchor_dependencies(dependencies, original, &self.within, &mut self.changed);
}
}
fn anchor_table(&mut self, name: &str, original: &Utf8Path) {
if let Some(table) = self.document.get_mut(name).and_then(Item::as_table_like_mut) {
anchor_dependencies(table, original, &self.within, &mut self.changed);
}
}
pub(super) fn link_runtime(&mut self, runtime: &Utf8Path) -> Result<()> {
let runtime = absolute(runtime);
let conflicting_target = self
.document
.get("target")
.and_then(Item::as_table_like)
.into_iter()
.flat_map(toml_edit::TableLike::iter)
.filter_map(|(_platform, target)| target.as_table_like()?.get("dependencies")?.as_table_like())
.any(|dependencies| {
dependencies.contains_key(RUNTIME_CRATE) && !dependency_points_to(dependencies.get(RUNTIME_CRATE), &runtime)
});
if conflicting_target {
return Err(Self::runtime_name_reserved(&self.path));
}
if let Some(targets) = self.document.get_mut("target").and_then(Item::as_table_like_mut) {
for (_platform, target) in targets.iter_mut() {
let Some(dependencies) = target
.as_table_like_mut()
.and_then(|table| table.get_mut("dependencies"))
.and_then(Item::as_table_like_mut)
else {
continue;
};
self.changed |= dependencies.remove(RUNTIME_CRATE).is_some();
self.changed |= dependencies.remove("cargo-gamma-rt").is_some();
}
}
let dependencies = self.document.entry("dependencies").or_insert_with(|| Item::Table(Table::new()));
let Some(table) = dependencies.as_table_like_mut() else {
return Ok(());
};
if table.contains_key(RUNTIME_CRATE) && !dependency_points_to(table.get(RUNTIME_CRATE), &runtime) {
return Err(Self::runtime_name_reserved(&self.path));
}
let _existing_runtime = table.remove("cargo-gamma-rt");
let mut entry = toml_edit::InlineTable::new();
let _package = entry.insert("package", Value::from(RUNTIME_PACKAGE));
let _replaced = entry.insert("path", Value::from(runtime.as_str()));
let _added = table.insert(RUNTIME_CRATE, Item::Value(Value::InlineTable(entry)));
self.changed = true;
Ok(())
}
pub(super) fn redirect_runtime(&mut self, runtime: &Utf8Path) -> Result<()> {
let runtime = absolute(runtime);
self.redirect_workspace_runtime(&runtime)?;
let top_level = self
.document
.get("dependencies")
.and_then(Item::as_table_like)
.is_some_and(|dependencies| dependencies.contains_key(RUNTIME_CRATE) || dependencies.contains_key("cargo-gamma-rt"));
let targeted = self
.document
.get("target")
.and_then(Item::as_table_like)
.into_iter()
.flat_map(toml_edit::TableLike::iter)
.filter_map(|(_platform, target)| target.as_table_like()?.get("dependencies")?.as_table_like())
.any(|dependencies| dependencies.contains_key(RUNTIME_CRATE) || dependencies.contains_key("cargo-gamma-rt"));
if top_level || targeted {
self.link_runtime(&runtime)?;
}
Ok(())
}
fn redirect_workspace_runtime(&mut self, runtime: &Utf8Path) -> Result<()> {
let Some(dependencies) = self
.document
.get_mut("workspace")
.and_then(Item::as_table_like_mut)
.and_then(|workspace| workspace.get_mut("dependencies"))
.and_then(Item::as_table_like_mut)
else {
return Ok(());
};
let aliased = dependencies.get(RUNTIME_CRATE);
if aliased.is_some() && !dependency_points_to(aliased, runtime) {
return Err(Self::runtime_name_reserved(&self.path));
}
if aliased.is_some() || dependencies.contains_key(RUNTIME_PACKAGE) {
let _aliased = dependencies.remove(RUNTIME_CRATE);
let _canonical = dependencies.remove(RUNTIME_PACKAGE);
let mut aliased_entry = toml_edit::InlineTable::new();
let _package = aliased_entry.insert("package", Value::from(RUNTIME_PACKAGE));
let _path = aliased_entry.insert("path", Value::from(runtime.as_str()));
let _runtime = dependencies.insert(RUNTIME_CRATE, Item::Value(Value::InlineTable(aliased_entry)));
let mut canonical_entry = toml_edit::InlineTable::new();
let _path = canonical_entry.insert("path", Value::from(runtime.as_str()));
let _runtime = dependencies.insert(RUNTIME_PACKAGE, Item::Value(Value::InlineTable(canonical_entry)));
self.changed = true;
}
Ok(())
}
fn runtime_name_reserved(path: &Utf8Path) -> crate::error::Error {
error!(
"`gamma_rt` is already a dependency in `{path}` but cargo-gamma reserves that crate name for its guard runtime.\n\
Rename that dependency so cargo-gamma can instrument this package."
)
.usage()
}
}
fn dependency_points_to(item: Option<&Item>, runtime: &Utf8Path) -> bool {
let Some(specification) = item.and_then(Item::as_table_like) else {
return false;
};
specification.get("workspace").and_then(Item::as_bool) == Some(true)
|| specification.get("package").and_then(Item::as_str) == Some(RUNTIME_PACKAGE)
|| specification
.get("path")
.and_then(Item::as_str)
.is_some_and(|path| absolute(Utf8Path::new(path)) == runtime)
}
fn anchor_dependencies(table: &mut dyn toml_edit::TableLike, original: &Utf8Path, within: &Utf8Path, changed: &mut bool) {
let names: Vec<String> = table.iter().map(|(name, _entry)| name.to_owned()).collect();
for name in names {
let Some(entry) = table.get_mut(&name) else {
continue;
};
let Some(specification) = entry.as_table_like_mut() else {
continue;
};
let Some(path) = specification.get("path").and_then(|item| item.as_str()) else {
continue;
};
let Some(anchored) = anchor(path, original, within) else {
continue;
};
let _replaced = specification.insert("path", Item::Value(Value::from(portable_path(&anchored))));
*changed = true;
}
}
fn anchor(path: &str, original: &Utf8Path, within: &Utf8Path) -> Option<Utf8PathBuf> {
let candidate = Utf8Path::new(path);
if candidate.is_absolute() || !escapes(&within.join(candidate)) {
return None;
}
Some(normalize(&original.join(candidate)))
}
fn escapes(path: &Utf8Path) -> bool {
let mut depth = 0_i32;
for component in path.components() {
match component.as_str() {
"." => {}
".." => {
depth -= 1;
if depth < 0 {
return true;
}
}
_named => depth += 1,
}
}
false
}
fn normalize(path: &Utf8Path) -> Utf8PathBuf {
let mut resolved = Utf8PathBuf::new();
for component in path.components() {
match component.as_str() {
"." => {}
".." => {
if !resolved.pop() {
resolved.push("..");
}
}
named => resolved.push(named),
}
}
resolved
}
fn portable_path(path: &Utf8Path) -> String {
path.as_str().replace('\\', "/")
}
pub(super) fn anchor_cargo_config(root: &Utf8Path, original: &Utf8Path) -> Result<()> {
for name in ["config.toml", "config"] {
let path = root.join(".cargo").join(name);
let _destination = crate::paths::require_within(&path, root, "a scratch Cargo configuration")?;
if !path.as_std_path().is_file() {
continue;
}
let mut manifest = Manifest::read(&path)?;
if let Some(paths) = manifest.document.get_mut("paths").and_then(Item::as_array_mut) {
for entry in paths.iter_mut() {
let Some(anchored) = entry.as_str().and_then(|path| anchor(path, original, Utf8Path::new(""))) else {
continue;
};
*entry = Value::from(portable_path(&anchored));
manifest.changed = true;
}
}
manifest.save()?;
}
Ok(())
}
pub(super) const CAP_LINTS: &str = "--cap-lints=allow";
pub(super) fn cap_lints(root: &Utf8Path) -> Result<()> {
let path = root.join(".cargo").join("config.toml");
let legacy = root.join(".cargo").join("config");
let path = if !path.as_std_path().is_file() && legacy.as_std_path().is_file() {
legacy
} else {
path
};
let _destination = crate::paths::require_within(&path, root, "a scratch Cargo configuration")?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent.as_std_path()).map_err(|cause| error!("could not create `{parent}`").caused_by(cause))?;
}
if !path.as_std_path().is_file() {
fs::write(path.as_std_path(), format!("[build]\nrustflags = [\"{CAP_LINTS}\"]\n"))
.map_err(|cause| error!("could not write `{path}`").caused_by(cause))?;
return Ok(());
}
let mut manifest = Manifest::read(&path)?;
let mut found = false;
if let Some(build) = manifest.document.get_mut("build") {
let Some(build) = build.as_table_like_mut() else {
return Err(error!(
"Cargo configuration `{path}` has a non-table `build` setting; use a `[build]` table so cargo-gamma can add `{CAP_LINTS}`"
));
};
if let Some(flags) = build.get_mut("rustflags") {
found |= append_flag(flags);
}
}
if let Some(targets) = manifest.document.get_mut("target").and_then(Item::as_table_like_mut) {
for (_name, entry) in targets.iter_mut() {
let Some(table) = entry.as_table_like_mut() else {
continue;
};
if let Some(flags) = table.get_mut("rustflags") {
found |= append_flag(flags);
}
}
}
if !found {
let build = manifest
.document
.entry("build")
.or_insert(Item::Table(Table::new()))
.as_table_like_mut()
.ok_or_else(|| error!("Cargo configuration `{path}` has a non-table `build` setting"))?;
let _previous = build.insert("rustflags", Item::Value(Value::Array(core::iter::once(CAP_LINTS).collect())));
}
manifest.changed = true;
manifest.save()
}
fn append_flag(flags: &mut Item) -> bool {
if let Some(array) = flags.as_array_mut() {
array.push(CAP_LINTS);
return true;
}
if let Some(text) = flags.as_str() {
*flags = Item::Value(Value::from(format!("{text} {CAP_LINTS}")));
return true;
}
false
}
#[cfg(test)]
#[cfg(not(miri))]
mod tests {
use super::*;
fn fixed(text: &str, original: &str) -> String {
within(text, original, "")
}
fn within(text: &str, original: &str, within: &str) -> String {
let temporary = tempfile::tempdir().unwrap();
let path = Utf8PathBuf::from_path_buf(temporary.path().join("Cargo.toml")).unwrap();
fs::write(path.as_std_path(), text).unwrap();
let mut manifest = Manifest::read(&path).unwrap();
manifest.anchor_paths(Utf8Path::new(original), Utf8Path::new(within));
manifest.document.to_string()
}
#[test]
fn a_dependency_written_as_a_bare_version_is_left_alone() {
let text = "[dependencies]\nserde = \"1\"\ncore = { path = \"../core\" }\n";
let fixed = fixed(text, "/src/app");
assert!(fixed.contains("serde = \"1\""), "{fixed}");
assert!(fixed.contains("/src/core"), "{fixed}");
}
#[test]
fn a_sibling_inside_the_copied_tree_is_left_alone() {
let fixed = within("[dependencies]\ncore = { path = \"../core\" }\n", "/src/app", "app");
assert!(fixed.contains("path = \"../core\""), "{fixed}");
}
#[test]
fn a_path_leaving_the_copied_tree_is_anchored_even_from_a_nested_package() {
let fixed = within("[dependencies]\nshared = { path = \"../../shared\" }\n", "/src/work/app", "app");
assert!(fixed.contains("path = \"/src/shared\""), "{fixed}");
}
#[test]
fn a_path_leaving_the_package_is_anchored() {
let fixed = fixed("[dependencies]\nshared = { path = \"../shared\" }\n", "/src/app");
assert!(fixed.contains("path = \"/src/shared\""), "{fixed}");
}
#[test]
fn a_path_staying_inside_the_package_is_left_alone() {
let fixed = fixed("[dependencies]\ninner = { path = \"crates/inner\" }\n", "/src/app");
assert!(fixed.contains("path = \"crates/inner\""), "{fixed}");
}
#[test]
fn an_absolute_path_is_left_alone() {
let fixed = fixed("[dependencies]\nshared = { path = \"/elsewhere/shared\" }\n", "/src/app");
assert!(fixed.contains("path = \"/elsewhere/shared\""), "{fixed}");
}
#[test]
fn a_path_that_descends_before_climbing_is_judged_on_the_whole_journey() {
let fixed = fixed("[dependencies]\nshared = { path = \"crates/../../shared\" }\n", "/src/app");
assert!(fixed.contains("path = \"/src/shared\""), "{fixed}");
}
#[test]
fn every_kind_of_dependency_table_is_covered() {
let text = "[dependencies]\na = { path = \"../a\" }\n\
[dev-dependencies]\nb = { path = \"../b\" }\n\
[build-dependencies]\nc = { path = \"../c\" }\n\
[target.'cfg(unix)'.dependencies]\nd = { path = \"../d\" }\n\
[patch.crates-io]\ne = { path = \"../e\" }\n\
[workspace.dependencies]\nf = { path = \"../f\" }\n";
let fixed = fixed(text, "/src/app");
for crate_name in ["a", "b", "c", "d", "e", "f"] {
assert!(fixed.contains(&format!("path = \"/src/{crate_name}\"")), "{crate_name} in {fixed}");
}
}
#[test]
fn a_patch_entry_that_is_not_a_table_does_not_stop_the_next_registry_from_being_anchored() {
let text = "[patch]\nbroken = \"not a table\"\n\n\
[patch.crates-io]\nshared = { path = \"../shared\" }\n";
let fixed = fixed(text, "/src/app");
assert!(fixed.contains("broken = \"not a table\""), "{fixed}");
assert!(fixed.contains("path = \"/src/shared\""), "{fixed}");
}
#[test]
fn comments_and_formatting_survive() {
let text = "# keep me\n[dependencies]\n# and me\nshared = { path = \"../shared\" } # trailing\n";
let fixed = fixed(text, "/src/app");
assert!(fixed.contains("# keep me"), "{fixed}");
assert!(fixed.contains("# and me"), "{fixed}");
assert!(fixed.contains("# trailing"), "{fixed}");
}
#[test]
fn a_version_only_dependency_is_untouched() {
let fixed = fixed("[dependencies]\nserde = \"1\"\n", "/src/app");
assert!(fixed.contains("serde = \"1\""), "{fixed}");
}
#[test]
fn target_entries_that_are_not_tables_are_skipped() {
let fixed = fixed(
"[target]\nnot_a_table = \"ignored\"\n[dependencies]\na = { path = \"../a\" }\n",
"/src/app",
);
assert!(fixed.contains("not_a_table = \"ignored\""), "{fixed}");
assert!(fixed.contains("path = \"/src/a\""), "{fixed}");
}
#[test]
fn dependencies_without_a_path_are_skipped() {
let fixed = fixed(
"[dependencies]\nserde = { version = \"1\" }\nlocal = { path = \"../local\" }\n",
"/src/app",
);
assert!(fixed.contains("serde = { version = \"1\" }"), "{fixed}");
assert!(fixed.contains("path = \"/src/local\""), "{fixed}");
}
fn linked(text: &str) -> String {
let temporary = tempfile::tempdir().unwrap();
let path = Utf8PathBuf::from_path_buf(temporary.path().join("Cargo.toml")).unwrap();
fs::write(path.as_std_path(), text).unwrap();
let mut manifest = Manifest::read(&path).unwrap();
manifest.link_runtime(Utf8Path::new("/scratch/rt")).unwrap();
manifest.document.to_string()
}
#[test]
fn the_runtime_is_added_to_a_package_without_it() {
let text = linked("[package]\nname = \"x\"\n");
let runtime = absolute(Utf8Path::new("/scratch/rt"));
assert!(text.contains("gamma_rt"), "{text}");
assert!(text.contains(runtime.as_str()), "{text}");
}
#[test]
fn a_relative_runtime_path_is_written_absolute() {
let temporary = tempfile::tempdir().unwrap();
let path = Utf8PathBuf::from_path_buf(temporary.path().join("Cargo.toml")).unwrap();
fs::write(path.as_std_path(), "[package]\nname = \"x\"\n").unwrap();
let mut manifest = Manifest::read(&path).unwrap();
manifest.link_runtime(Utf8Path::new("scratch/gamma/rt")).unwrap();
let text = manifest.document.to_string();
let expected = absolute(Utf8Path::new("scratch/gamma/rt"));
assert!(expected.is_absolute(), "{expected}");
assert!(text.contains(expected.as_str()), "{text}");
}
#[test]
fn the_runtime_is_added_to_an_existing_dependency_table() {
let text = linked("[package]\nname = \"x\"\n\n[dependencies]\nserde = \"1\"\n");
assert!(text.contains("gamma_rt"), "{text}");
assert!(text.contains("serde = \"1\""), "{text}");
}
#[test]
fn a_malformed_dependency_table_cannot_receive_the_runtime() {
let text = linked("dependencies = \"not a table\"\n[package]\nname = \"x\"\n");
assert!(!text.contains("gamma_rt"), "{text}");
assert!(text.contains("dependencies = \"not a table\""), "{text}");
}
#[test]
fn an_existing_runtime_dependency_is_replaced_by_the_vendored_one() {
let runtime = absolute(Utf8Path::new("/scratch/rt"));
for text in [
"[dependencies]\ncargo-gamma-rt = { workspace = true }\n",
"[dependencies]\ngamma_rt = { path = \"/scratch/rt\" }\n",
"[dependencies]\ngamma_rt = { package = \"cargo-gamma-rt\", version = \"0.1\" }\n",
"[dependencies]\ngamma_rt = { workspace = true }\n",
] {
let linked = linked(text);
assert_eq!(linked.matches("gamma_rt").count(), 1, "{linked}");
assert!(linked.contains("package = \"cargo-gamma-rt\""), "{linked}");
assert!(linked.contains(runtime.as_str()), "{linked}");
}
}
#[test]
fn an_existing_runtime_is_redirected_before_its_package_is_instrumented() {
let temporary = tempfile::tempdir().unwrap();
let path = Utf8PathBuf::from_path_buf(temporary.path().join("Cargo.toml")).unwrap();
let runtime = absolute(Utf8Path::new("/scratch/rt"));
fs::write(
path.as_std_path(),
"[package]\nname = \"x\"\n\n[dependencies]\ncargo-gamma-rt = { workspace = true }\n",
)
.unwrap();
let mut manifest = Manifest::read(&path).unwrap();
manifest.redirect_runtime(&runtime).unwrap();
let text = manifest.document.to_string();
assert!(text.contains("package = \"cargo-gamma-rt\""), "{text}");
assert!(text.contains(runtime.as_str()), "{text}");
}
#[test]
fn a_workspace_runtime_alias_is_redirected_before_members_inherit_it() {
let temporary = tempfile::tempdir().unwrap();
let path = Utf8PathBuf::from_path_buf(temporary.path().join("Cargo.toml")).unwrap();
let runtime = absolute(Utf8Path::new("/scratch/rt"));
fs::write(
path.as_std_path(),
"[workspace]\nmembers = []\n\n\
[workspace.dependencies]\ngamma_rt = { package = \"cargo-gamma-rt\", version = \"0.1\" }\n",
)
.unwrap();
let mut manifest = Manifest::read(&path).unwrap();
manifest.redirect_runtime(&runtime).unwrap();
let text = manifest.document.to_string();
assert_eq!(text.matches("\ngamma_rt =").count(), 1, "{text}");
assert!(text.contains("gamma_rt = { package = \"cargo-gamma-rt\""), "{text}");
assert!(text.contains("cargo-gamma-rt = { path"), "{text}");
assert!(text.contains(runtime.as_str()), "{text}");
}
#[test]
fn a_canonical_workspace_runtime_remains_available_to_inheriting_dependencies() {
let temporary = tempfile::tempdir().unwrap();
let path = Utf8PathBuf::from_path_buf(temporary.path().join("Cargo.toml")).unwrap();
let runtime = absolute(Utf8Path::new("/scratch/rt"));
fs::write(
path.as_std_path(),
"[workspace]\nmembers = []\n\n\
[workspace.dependencies]\ncargo-gamma-rt = \"0.1\"\n",
)
.unwrap();
let mut manifest = Manifest::read(&path).unwrap();
manifest.redirect_runtime(&runtime).unwrap();
let text = manifest.document.to_string();
assert!(text.contains("gamma_rt = { package = \"cargo-gamma-rt\", path"), "{text}");
assert!(text.contains("cargo-gamma-rt = { path"), "{text}");
assert_eq!(text.matches(runtime.as_str()).count(), 2, "{text}");
}
#[test]
fn an_unrelated_workspace_dependency_cannot_occupy_the_runtime_name() {
let temporary = tempfile::tempdir().unwrap();
let path = Utf8PathBuf::from_path_buf(temporary.path().join("Cargo.toml")).unwrap();
let runtime = absolute(Utf8Path::new("/scratch/rt"));
fs::write(
path.as_std_path(),
"[workspace]\nmembers = []\n\n\
[workspace.dependencies]\ngamma_rt = { package = \"some-other-package\", version = \"1\" }\n",
)
.unwrap();
let mut manifest = Manifest::read(&path).unwrap();
let failure = manifest
.redirect_runtime(&runtime)
.expect_err("the generated guard's crate name must be reserved workspace-wide");
assert!(failure.is_usage(), "{failure}");
assert!(failure.to_string().contains("reserves that crate name"), "{failure}");
}
#[test]
fn an_unrelated_dependency_cannot_occupy_the_runtime_name() {
for dependency in [
"gamma_rt = \"1\"",
"gamma_rt = { package = \"some-other-package\", version = \"1\" }",
] {
let temporary = tempfile::tempdir().unwrap();
let path = Utf8PathBuf::from_path_buf(temporary.path().join("Cargo.toml")).unwrap();
fs::write(
path.as_std_path(),
format!("[package]\nname = \"x\"\n\n[dependencies]\n{dependency}\n"),
)
.unwrap();
let mut manifest = Manifest::read(&path).unwrap();
let failure = manifest
.link_runtime(Utf8Path::new("/scratch/rt"))
.expect_err("the generated guard's crate name must be reserved");
assert!(failure.is_usage(), "{failure}");
assert!(failure.to_string().contains("reserves that crate name"), "{failure}");
}
}
#[test]
fn a_dependency_the_library_target_cannot_see_does_not_count() {
let runtime = absolute(Utf8Path::new("/scratch/rt"));
for text in ["[dev-dependencies]\ngamma_rt = \"1\"\n", "[build-dependencies]\ngamma_rt = \"1\"\n"] {
assert!(linked(text).contains(runtime.as_str()), "{text}");
}
}
#[test]
fn a_cargo_config_path_override_is_anchored() {
let temporary = tempfile::tempdir().unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
fs::create_dir_all(root.join(".cargo").as_std_path()).unwrap();
fs::write(
root.join(".cargo").join("config.toml").as_std_path(),
"paths = [\"../vendored\", \"inside\"]\n",
)
.unwrap();
anchor_cargo_config(&root, Utf8Path::new("/src/app")).unwrap();
let text = fs::read_to_string(root.join(".cargo").join("config.toml").as_std_path()).unwrap();
assert!(text.contains("/src/vendored"), "{text}");
assert!(text.contains("\"inside\""), "{text}");
}
#[test]
fn a_cargo_config_with_no_paths_override_is_left_alone() {
let temporary = tempfile::tempdir().expect("tempdir");
let root = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).expect("utf8");
fs::create_dir_all(root.join(".cargo").as_std_path()).expect(".cargo");
fs::write(root.join(".cargo").join("config.toml").as_std_path(), "[net]\nretry = 3\n").expect("config");
anchor_cargo_config(&root, Utf8Path::new("/src/app")).expect("anchor");
let text = fs::read_to_string(root.join(".cargo").join("config.toml").as_std_path()).expect("read back");
assert!(text.contains("retry"), "{text}");
assert!(!text.contains("paths"), "{text}");
}
#[test]
fn the_lint_cap_is_added_to_configured_rustflags_rather_than_replacing_them() {
let temporary = tempfile::tempdir().unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
let config = root.join(".cargo").join("config.toml");
fs::create_dir_all(config.parent().unwrap().as_std_path()).unwrap();
fs::write(
config.as_std_path(),
"[build]\nrustflags = [\"--cfg\", \"loom\"]\n\n[target.x86_64-unknown-linux-gnu]\nrustflags = \"-C target-cpu=native\"\n",
)
.unwrap();
cap_lints(&root).unwrap();
let text = fs::read_to_string(config.as_std_path()).unwrap();
assert!(text.contains("loom"), "{text}");
assert!(text.contains("target-cpu=native"), "{text}");
assert_eq!(text.matches(CAP_LINTS).count(), 2, "{text}");
}
#[test]
fn a_tree_with_no_cargo_config_gets_one() {
let temporary = tempfile::tempdir().unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
cap_lints(&root).unwrap();
let text = fs::read_to_string(root.join(".cargo").join("config.toml").as_std_path()).unwrap();
assert!(text.contains(CAP_LINTS), "{text}");
}
#[test]
fn a_cargo_config_with_no_rustflags_gains_them() {
let temporary = tempfile::tempdir().unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
let config = root.join(".cargo").join("config.toml");
fs::create_dir_all(config.parent().unwrap().as_std_path()).unwrap();
fs::write(config.as_std_path(), "[net]\nretry = 3\n").unwrap();
cap_lints(&root).unwrap();
let text = fs::read_to_string(config.as_std_path()).unwrap();
assert!(text.contains(CAP_LINTS), "{text}");
assert!(text.contains("retry"), "{text}");
}
#[test]
fn a_scalar_build_configuration_is_reported_without_panicking() {
let temporary = tempfile::tempdir().expect("tempdir");
let root = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).expect("utf8");
let config = root.join(".cargo").join("config.toml");
fs::create_dir_all(config.parent().expect("parent").as_std_path()).expect("mkdir");
fs::write(config.as_std_path(), "build = \"not a table\"\n").expect("config");
let failure = cap_lints(&root).expect_err("a scalar build key cannot receive rustflags");
assert!(failure.to_string().contains("non-table `build`"), "{failure}");
assert!(failure.to_string().contains(config.as_str()), "{failure}");
}
#[test]
fn a_target_entry_that_is_not_a_table_does_not_stop_the_cap_from_landing_elsewhere() {
let temporary = tempfile::tempdir().expect("tempdir");
let root = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).expect("utf8");
let config = root.join(".cargo").join("config.toml");
fs::create_dir_all(config.parent().expect("parent").as_std_path()).expect("mkdir");
fs::write(
config.as_std_path(),
"[target]\nnot_a_table = \"ignored\"\n\n\
[target.'cfg(unix)']\nlinker = \"lld\"\n\n\
[target.x86_64-unknown-linux-gnu]\nrustflags = [\"--cfg\", \"loom\"]\n",
)
.expect("write config");
cap_lints(&root).expect("cap_lints");
let text = fs::read_to_string(config.as_std_path()).expect("read back");
assert!(text.contains("not_a_table = \"ignored\""), "{text}");
assert!(text.contains("linker = \"lld\""), "{text}");
assert_eq!(text.matches(CAP_LINTS).count(), 1, "{text}");
}
#[test]
fn a_rustflags_value_that_is_neither_an_array_nor_a_string_is_replaced() {
let temporary = tempfile::tempdir().expect("tempdir");
let root = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).expect("utf8");
let config = root.join(".cargo").join("config.toml");
fs::create_dir_all(config.parent().expect("parent").as_std_path()).expect("mkdir");
fs::write(config.as_std_path(), "[build]\nrustflags = 5\n").expect("write config");
cap_lints(&root).expect("cap_lints");
let text = fs::read_to_string(config.as_std_path()).expect("read back");
assert!(!text.contains("rustflags = 5"), "{text}");
assert!(text.contains(CAP_LINTS), "{text}");
}
#[test]
fn the_legacy_cargo_config_name_gains_the_cap_too() {
let temporary = tempfile::tempdir().unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
let config = root.join(".cargo").join("config");
fs::create_dir_all(config.parent().unwrap().as_std_path()).unwrap();
fs::write(config.as_std_path(), "[build]\nrustflags = [\"--cfg\", \"loom\"]\n").unwrap();
cap_lints(&root).unwrap();
let text = fs::read_to_string(config.as_std_path()).unwrap();
assert!(text.contains(CAP_LINTS), "{text}");
assert!(!root.join(".cargo").join("config.toml").as_std_path().exists());
}
#[test]
fn a_legacy_cargo_config_name_is_anchored() {
let temporary = tempfile::tempdir().unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
fs::create_dir_all(root.join(".cargo").as_std_path()).unwrap();
fs::write(root.join(".cargo").join("config").as_std_path(), "paths = [\"../vendored\"]\n").unwrap();
anchor_cargo_config(&root, Utf8Path::new("/src/app")).unwrap();
let text = fs::read_to_string(root.join(".cargo").join("config").as_std_path()).unwrap();
assert!(text.contains("/src/vendored"), "{text}");
}
#[test]
fn normalization_keeps_leading_parent_components() {
assert_eq!(normalize(Utf8Path::new("/../shared")), Utf8PathBuf::from("/../shared"));
}
#[test]
fn a_missing_cargo_config_is_not_an_error() {
let temporary = tempfile::tempdir().unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
anchor_cargo_config(&root, Utf8Path::new("/src/app")).unwrap();
}
#[test]
fn a_leading_current_directory_component_does_not_change_whether_a_path_escapes() {
assert!(!escapes(Utf8Path::new("./sibling/deeper")));
assert!(escapes(Utf8Path::new("./..")));
}
#[test]
fn normalization_drops_current_directory_components() {
assert_eq!(normalize(Utf8Path::new("a/./b")), Utf8PathBuf::from("a/b"));
}
#[test]
fn a_cargo_config_that_cannot_be_parsed_is_reported() {
let temporary = tempfile::tempdir().expect("tempdir");
let root = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).expect("utf8");
fs::create_dir_all(root.join(".cargo").as_std_path()).expect(".cargo");
fs::write(root.join(".cargo").join("config.toml").as_std_path(), "not [ valid toml").expect("config");
let error = anchor_cargo_config(&root, Utf8Path::new("/src/app")).expect_err("the file does not parse");
assert!(error.to_string().contains("could not parse"), "{error}");
}
#[cfg(unix)]
#[test]
fn a_cargo_config_that_cannot_be_saved_after_anchoring_is_reported() {
use std::os::unix::fs::PermissionsExt as _;
let temporary = tempfile::tempdir().expect("tempdir");
let root = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).expect("utf8");
let config = root.join(".cargo").join("config.toml");
fs::create_dir_all(config.parent().expect("parent").as_std_path()).expect(".cargo");
fs::write(config.as_std_path(), "paths = [\"../vendored\"]\n").expect("config");
fs::set_permissions(config.as_std_path(), fs::Permissions::from_mode(0o400)).expect("chmod");
let error = anchor_cargo_config(&root, Utf8Path::new("/src/app")).expect_err("the file cannot be written");
fs::set_permissions(config.as_std_path(), fs::Permissions::from_mode(0o644)).expect("chmod back");
assert!(error.to_string().contains("could not update"), "{error}");
}
#[cfg(unix)]
#[test]
fn a_cargo_config_that_cannot_be_created_reports_the_write_failure() {
use std::os::unix::fs::PermissionsExt as _;
let temporary = tempfile::tempdir().expect("tempdir");
let root = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).expect("utf8");
let cargo_dir = root.join(".cargo");
fs::create_dir_all(cargo_dir.as_std_path()).expect(".cargo");
fs::set_permissions(cargo_dir.as_std_path(), fs::Permissions::from_mode(0o500)).expect("chmod");
let error = cap_lints(&root).expect_err("the directory cannot be written into");
fs::set_permissions(cargo_dir.as_std_path(), fs::Permissions::from_mode(0o755)).expect("chmod back");
assert!(error.to_string().contains("could not write"), "{error}");
}
#[test]
fn cap_lints_reports_an_existing_config_that_cannot_be_parsed() {
let temporary = tempfile::tempdir().expect("tempdir");
let root = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).expect("utf8");
let config = root.join(".cargo").join("config.toml");
fs::create_dir_all(config.parent().expect("parent").as_std_path()).expect(".cargo");
fs::write(config.as_std_path(), "not [ valid toml").expect("config");
let error = cap_lints(&root).expect_err("the file does not parse");
assert!(error.to_string().contains("could not parse"), "{error}");
}
}