use std::path::{Path, PathBuf};
use crate::error::Error;
use crate::util;
#[derive(Debug)]
pub struct OverlayPlan {
pub upstream_manifest: PathBuf,
pub sibling_manifest: PathBuf,
pub upstream_already_has_dylib: bool,
pub dropped_comments: Vec<String>,
pub upstream_crate_name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SyntheticMetadata {
pub dylib_crate: String,
pub extern_crates: Vec<String>,
pub fixture_dirs: Vec<String>,
}
pub fn materialize_overlay(upstream_manifest_path: &Path) -> Result<OverlayPlan, Error> {
materialize_overlay_with_metadata(upstream_manifest_path, None)
}
pub fn materialize_overlay_with_metadata(
upstream_manifest_path: &Path,
synthetic_metadata: Option<&SyntheticMetadata>,
) -> Result<OverlayPlan, Error> {
materialize_overlay_inner(upstream_manifest_path, |_name| synthetic_metadata.cloned())
}
pub fn materialize_overlay_with_synthetic_metadata_builder<F>(
upstream_manifest_path: &Path,
builder: F,
) -> Result<OverlayPlan, Error>
where
F: FnOnce(Option<&str>) -> SyntheticMetadata,
{
materialize_overlay_inner(upstream_manifest_path, |name| Some(builder(name)))
}
fn materialize_overlay_inner<F>(
upstream_manifest_path: &Path,
synthetic_metadata: F,
) -> Result<OverlayPlan, Error>
where
F: FnOnce(Option<&str>) -> Option<SyntheticMetadata>,
{
let raw_bytes = std::fs::read(upstream_manifest_path).map_err(|e| {
Error::io(
e,
"reading upstream Cargo.toml for overlay",
Some(upstream_manifest_path.to_path_buf()),
)
})?;
let raw_text = String::from_utf8(raw_bytes).map_err(|e| {
Error::io(
std::io::Error::new(std::io::ErrorKind::InvalidData, e),
"decoding upstream Cargo.toml as UTF-8",
Some(upstream_manifest_path.to_path_buf()),
)
})?;
let dropped_comments = scan_dropped_comments(&raw_text);
let mut value: toml::Value =
toml::from_str(&raw_text).map_err(|e: toml::de::Error| Error::TomlParse {
path: upstream_manifest_path.to_path_buf(),
message: e.to_string(),
})?;
if is_workspace_root_manifest(&value) {
return Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--compat-root` must point to a single-crate Cargo.toml; \
`{}` is a workspace root (declares `[workspace]` without `[package]`). \
Pass a member crate's Cargo.toml as `--compat-root` instead.",
upstream_manifest_path.display()
),
});
}
let upstream_already_has_dylib = inspect_existing_crate_type(&value);
let upstream_crate_name = read_upstream_crate_name(&value);
let synthetic = synthetic_metadata(upstream_crate_name.as_deref());
let upstream_dir: PathBuf = upstream_manifest_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
if let toml::Value::Table(top) = &mut value {
let lib_table = top
.entry("lib".to_string())
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
if let toml::Value::Table(lib) = lib_table {
canonicalize_crate_type(lib)?;
} else {
return Err(Error::TomlParse {
path: upstream_manifest_path.to_path_buf(),
message: "`[lib]` must be a table, not an inline value".to_string(),
});
}
absolutize_path_bearing_keys(top, &upstream_dir);
if let Some(meta) = synthetic.as_ref() {
inject_synthetic_metadata(top, meta);
}
override_workspace_inheritance(top, upstream_manifest_path)?;
}
let serialized = serialize_canonical(&value)?;
let crate_dir = upstream_manifest_path
.parent()
.unwrap_or(upstream_manifest_path);
let sibling_path = crate_dir
.join("target")
.join("lihaaf-overlay")
.join("Cargo.toml");
let need_write = match std::fs::read(&sibling_path) {
Ok(existing) => existing != serialized,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
Err(e) => {
return Err(Error::io(
e,
"checking existing staged overlay for idempotent rerun",
Some(sibling_path.clone()),
));
}
};
if need_write {
util::write_file_atomic(&sibling_path, &serialized)?;
}
Ok(OverlayPlan {
upstream_manifest: upstream_manifest_path.to_path_buf(),
sibling_manifest: sibling_path,
upstream_already_has_dylib,
dropped_comments,
upstream_crate_name,
})
}
fn read_upstream_crate_name(value: &toml::Value) -> Option<String> {
value
.get("package")
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
.filter(|s| !s.is_empty())
.map(str::to_string)
}
const WORKSPACE_MEMBERSHIP_KEYS: &[&str] = &["members", "exclude", "default-members"];
fn override_workspace_inheritance(
top: &mut toml::map::Map<String, toml::Value>,
upstream_manifest_path: &Path,
) -> Result<(), Error> {
if let Some(toml::Value::Table(pkg)) = top.get("package")
&& pkg.contains_key("workspace")
{
return Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--compat-root` `{}` is a workspace member: \
`[package].workspace = \"...\"` declares membership in \
an ancestor workspace, which compat mode cannot reach. \
Compat mode currently supports only single-crate \
manifests and workspace-root manifests (where \
`[workspace]` lives in the same Cargo.toml). \
Pass the workspace-ROOT Cargo.toml as `--compat-root` \
instead; it will still resolve `{{ workspace = true }}` \
references in its own manifest because \
`[workspace.dependencies]` / `[workspace.package]` / \
`[workspace.lints]` are preserved in the staged overlay.",
upstream_manifest_path.display()
),
});
}
let has_local_workspace = top.get("workspace").is_some_and(|v| v.is_table());
if !has_local_workspace
&& let Some(ancestor_manifest) = detect_implicit_ancestor_workspace(upstream_manifest_path)?
{
return Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--compat-root` `{}` is an implicit workspace member: \
it has no local `[workspace]` table but an ancestor manifest \
at `{}` carries `[workspace]`. Cargo's baseline build walks \
up the filesystem and would apply the ancestor's `[patch]` / \
`[replace]` / `[profile]` / `resolver` / \
`[workspace.dependencies]` tables during dependency \
resolution, but the lihaaf overlay declares its own \
`[workspace]` and terminates cargo's walk-up at the staged \
manifest — producing a divergent dependency graph and \
false compat verdicts. Compat mode currently cannot copy \
the ancestor workspace state down into the overlay. \
Either invoke `cargo lihaaf --compat` from the workspace \
ROOT (`{}` or its containing directory), or restructure \
the fork so the crate-under-test has no ancestor workspace.",
upstream_manifest_path.display(),
ancestor_manifest.display(),
ancestor_manifest.display(),
),
});
}
if !has_local_workspace && manifest_has_inheritance_reference(top) {
return Err(Error::Cli {
clap_exit_code: 2,
message: format!(
"error: `--compat-root` `{}` is an implicit workspace member: \
it has no local `[workspace]` table but uses workspace \
inheritance (one or more `{{ workspace = true }}` \
references in `[package]` / `[dependencies]` / \
`[dev-dependencies]` / `[build-dependencies]` / \
`[target.<cfg>.<deps>]` / `[lints]`). Cargo discovers \
the ancestor workspace by walking up the filesystem, \
but compat mode cannot reach into that ancestor to \
copy down the `[workspace.dependencies]` / \
`[workspace.package]` / `[workspace.lints]` tables \
the inheritance references resolve against. \
Pass the workspace-ROOT Cargo.toml as `--compat-root` \
instead; the staged overlay preserves its \
`[workspace.*]` inheritance tables verbatim.",
upstream_manifest_path.display()
),
});
}
let mut new_workspace = if let Some(toml::Value::Table(existing)) = top.get("workspace") {
let mut cloned = existing.clone();
for key in WORKSPACE_MEMBERSHIP_KEYS {
cloned.remove(*key);
}
cloned
} else {
toml::map::Map::new()
};
for key in WORKSPACE_MEMBERSHIP_KEYS {
new_workspace.remove(*key);
}
top.insert("workspace".to_string(), toml::Value::Table(new_workspace));
Ok(())
}
fn detect_implicit_ancestor_workspace(
upstream_manifest_path: &Path,
) -> Result<Option<PathBuf>, Error> {
let Some(manifest_dir) = upstream_manifest_path.parent() else {
return Ok(None);
};
let mut current = manifest_dir.parent();
while let Some(dir) = current {
let candidate = dir.join("Cargo.toml");
match std::fs::read_to_string(&candidate) {
Ok(text) => {
match toml::from_str::<toml::Value>(&text) {
Ok(value) => {
if value.get("workspace").is_some_and(|v| v.is_table()) {
return Ok(Some(candidate));
}
}
Err(e) => {
eprintln!(
"lihaaf: warning: skipping ancestor Cargo.toml `{}` during \
workspace detection: TOML parse error: {}",
candidate.display(),
e
);
}
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
}
Err(e) => {
return Err(Error::io(
e,
"reading ancestor Cargo.toml during workspace detection",
Some(candidate),
));
}
}
current = dir.parent();
}
Ok(None)
}
fn manifest_has_inheritance_reference(top: &toml::map::Map<String, toml::Value>) -> bool {
let is_inheritance_table = |v: &toml::Value| -> bool {
v.as_table()
.and_then(|t| t.get("workspace"))
.and_then(|v| v.as_bool())
.unwrap_or(false)
};
let deps_table_has_inheritance =
|top: &toml::map::Map<String, toml::Value>, key: &str| -> bool {
let Some(toml::Value::Table(t)) = top.get(key) else {
return false;
};
t.values().any(is_inheritance_table)
};
if let Some(toml::Value::Table(pkg)) = top.get("package") {
for (k, v) in pkg.iter() {
if k == "workspace" {
continue;
}
if is_inheritance_table(v) {
return true;
}
}
}
for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
if deps_table_has_inheritance(top, section) {
return true;
}
}
if let Some(toml::Value::Table(targets)) = top.get("target") {
for cfg_value in targets.values() {
let Some(cfg_table) = cfg_value.as_table() else {
continue;
};
for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
if deps_table_has_inheritance(cfg_table, section) {
return true;
}
}
}
}
if let Some(toml::Value::Table(lints)) = top.get("lints") {
if lints
.get("workspace")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
return true;
}
if lints.values().any(is_inheritance_table) {
return true;
}
}
false
}
fn inject_synthetic_metadata(
top: &mut toml::map::Map<String, toml::Value>,
meta: &SyntheticMetadata,
) {
let package_entry = top
.entry("package".to_string())
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
let toml::Value::Table(package) = package_entry else {
return;
};
let metadata_entry = package
.entry("metadata".to_string())
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
let toml::Value::Table(metadata) = metadata_entry else {
return;
};
let mut lihaaf_table = toml::map::Map::new();
lihaaf_table.insert(
"dylib_crate".to_string(),
toml::Value::String(meta.dylib_crate.clone()),
);
lihaaf_table.insert(
"extern_crates".to_string(),
toml::Value::Array(
meta.extern_crates
.iter()
.cloned()
.map(toml::Value::String)
.collect(),
),
);
lihaaf_table.insert(
"fixture_dirs".to_string(),
toml::Value::Array(
meta.fixture_dirs
.iter()
.cloned()
.map(toml::Value::String)
.collect(),
),
);
metadata.insert("lihaaf".to_string(), toml::Value::Table(lihaaf_table));
}
fn absolutize_path_bearing_keys(
top: &mut toml::map::Map<String, toml::Value>,
upstream_dir: &Path,
) {
let to_abs_string = |relative: &str| -> String {
let joined = upstream_dir.join(relative);
crate::util::to_forward_slash(&joined.to_string_lossy())
};
let absolutize_string_at = |table: &mut toml::map::Map<String, toml::Value>,
key: &str,
upstream_dir: &Path| {
if let Some(toml::Value::String(s)) = table.get(key) {
let p = Path::new(s);
if !p.is_absolute() {
let abs = crate::util::to_forward_slash(&upstream_dir.join(p).to_string_lossy());
table.insert(key.to_string(), toml::Value::String(abs));
}
}
};
let absolutize_array_table_paths =
|top: &mut toml::map::Map<String, toml::Value>, section: &str, upstream_dir: &Path| {
if let Some(toml::Value::Array(entries)) = top.get_mut(section) {
for entry in entries.iter_mut() {
if let toml::Value::Table(t) = entry {
absolutize_string_at(t, "path", upstream_dir);
}
}
}
};
let absolutize_deps_paths =
|top: &mut toml::map::Map<String, toml::Value>, section: &str, upstream_dir: &Path| {
if let Some(toml::Value::Table(deps)) = top.get_mut(section) {
for (_name, dep) in deps.iter_mut() {
if let toml::Value::Table(t) = dep {
absolutize_string_at(t, "path", upstream_dir);
}
}
}
};
if let Some(toml::Value::Table(lib)) = top.get_mut("lib") {
let needs_inject = !lib.contains_key("path");
if needs_inject {
lib.insert(
"path".to_string(),
toml::Value::String(to_abs_string("src/lib.rs")),
);
} else {
absolutize_string_at(lib, "path", upstream_dir);
}
}
let upstream_build_rs = upstream_dir.join("build.rs");
if let Some(toml::Value::Table(pkg)) = top.get_mut("package") {
if pkg.contains_key("build") {
absolutize_string_at(pkg, "build", upstream_dir);
} else if upstream_build_rs.is_file() {
pkg.insert(
"build".to_string(),
toml::Value::String(to_abs_string("build.rs")),
);
}
}
absolutize_array_table_paths(top, "bin", upstream_dir);
absolutize_array_table_paths(top, "example", upstream_dir);
absolutize_array_table_paths(top, "test", upstream_dir);
absolutize_array_table_paths(top, "bench", upstream_dir);
if let Some(toml::Value::Table(pkg)) = top.get_mut("package") {
pkg.insert("autobins".to_string(), toml::Value::Boolean(false));
pkg.insert("autoexamples".to_string(), toml::Value::Boolean(false));
pkg.insert("autotests".to_string(), toml::Value::Boolean(false));
pkg.insert("autobenches".to_string(), toml::Value::Boolean(false));
}
absolutize_deps_paths(top, "dependencies", upstream_dir);
absolutize_deps_paths(top, "dev-dependencies", upstream_dir);
absolutize_deps_paths(top, "build-dependencies", upstream_dir);
if let Some(toml::Value::Table(targets)) = top.get_mut("target") {
for (_cfg, cfg_value) in targets.iter_mut() {
if let toml::Value::Table(cfg_table) = cfg_value {
absolutize_deps_paths(cfg_table, "dependencies", upstream_dir);
absolutize_deps_paths(cfg_table, "dev-dependencies", upstream_dir);
absolutize_deps_paths(cfg_table, "build-dependencies", upstream_dir);
}
}
}
if let Some(toml::Value::Table(ws)) = top.get_mut("workspace") {
for key in ["members", "exclude"] {
if let Some(toml::Value::Array(arr)) = ws.get_mut(key) {
for entry in arr.iter_mut() {
if let toml::Value::String(s) = entry {
let p = Path::new(s.as_str());
if !p.is_absolute() {
let abs = crate::util::to_forward_slash(
&upstream_dir.join(p).to_string_lossy(),
);
*entry = toml::Value::String(abs);
}
}
}
}
}
if let Some(toml::Value::Array(arr)) = ws.get_mut("default-members") {
for entry in arr.iter_mut() {
if let toml::Value::String(s) = entry {
let p = Path::new(s.as_str());
if !p.is_absolute() {
let abs =
crate::util::to_forward_slash(&upstream_dir.join(p).to_string_lossy());
*entry = toml::Value::String(abs);
}
}
}
}
absolutize_deps_paths(ws, "dependencies", upstream_dir);
}
if let Some(toml::Value::Table(pkg)) = top.get_mut("package") {
absolutize_string_at(pkg, "workspace", upstream_dir);
}
absolutize_patch_paths(top, upstream_dir);
absolutize_replace_paths(top, upstream_dir);
}
fn absolutize_patch_paths(top: &mut toml::map::Map<String, toml::Value>, upstream_dir: &Path) {
let Some(toml::Value::Table(patch)) = top.get_mut("patch") else {
return;
};
for (_registry, registry_value) in patch.iter_mut() {
if let toml::Value::Table(registry_table) = registry_value {
for (_krate, krate_value) in registry_table.iter_mut() {
if let toml::Value::Table(krate_table) = krate_value {
let needs_rewrite = krate_table
.get("path")
.and_then(|v| v.as_str())
.is_some_and(|s| !Path::new(s).is_absolute());
if needs_rewrite {
let s = krate_table
.get("path")
.and_then(|v| v.as_str())
.expect("needs_rewrite implies path exists");
let abs =
crate::util::to_forward_slash(&upstream_dir.join(s).to_string_lossy());
krate_table.insert("path".to_string(), toml::Value::String(abs));
}
}
}
}
}
}
fn absolutize_replace_paths(top: &mut toml::map::Map<String, toml::Value>, upstream_dir: &Path) {
let Some(toml::Value::Table(replace)) = top.get_mut("replace") else {
return;
};
for (_source_id, entry_value) in replace.iter_mut() {
if let toml::Value::Table(entry_table) = entry_value {
let needs_rewrite = entry_table
.get("path")
.and_then(|v| v.as_str())
.is_some_and(|s| !Path::new(s).is_absolute());
if needs_rewrite {
let s = entry_table
.get("path")
.and_then(|v| v.as_str())
.expect("needs_rewrite implies path exists");
let abs = crate::util::to_forward_slash(&upstream_dir.join(s).to_string_lossy());
entry_table.insert("path".to_string(), toml::Value::String(abs));
}
}
}
}
fn is_workspace_root_manifest(value: &toml::Value) -> bool {
let Some(top) = value.as_table() else {
return false;
};
let has_workspace = top.get("workspace").is_some_and(|v| v.is_table());
let has_package = top.get("package").is_some_and(|v| v.is_table());
has_workspace && !has_package
}
fn inspect_existing_crate_type(value: &toml::Value) -> bool {
let Some(lib) = value.get("lib") else {
return false;
};
let Some(ct) = lib.get("crate-type") else {
return false;
};
let Some(arr) = ct.as_array() else {
return false;
};
arr.iter().filter_map(|v| v.as_str()).any(|s| s == "dylib")
}
pub(crate) fn canonicalize_crate_type(
table: &mut toml::map::Map<String, toml::Value>,
) -> Result<(), Error> {
let existing: Vec<String> = match table.get("crate-type") {
None => Vec::new(),
Some(toml::Value::Array(arr)) => {
let mut out = Vec::with_capacity(arr.len());
for (idx, v) in arr.iter().enumerate() {
match v.as_str() {
Some(s) => out.push(s.to_string()),
None => {
return Err(Error::TomlParse {
path: PathBuf::from("<overlay>"),
message: format!(
"`[lib] crate-type` element at index {idx} is not a string; \
the overlay accepts only string crate-type entries"
),
});
}
}
}
out
}
Some(other) => {
return Err(Error::TomlParse {
path: PathBuf::from("<overlay>"),
message: format!(
"`[lib] crate-type` must be an array of strings, got `{}`",
type_name_of(other)
),
});
}
};
let mut out: Vec<String> = Vec::with_capacity(existing.len() + 2);
out.push("dylib".to_string());
out.push("rlib".to_string());
for entry in &existing {
if entry == "dylib" || entry == "rlib" {
continue;
}
if !out.contains(entry) {
out.push(entry.clone());
}
}
let array = out.into_iter().map(toml::Value::String).collect::<Vec<_>>();
table.insert("crate-type".to_string(), toml::Value::Array(array));
Ok(())
}
pub(crate) fn canonical_key_order() -> &'static [&'static str] {
&[
"package",
"lib",
"bin",
"example",
"test",
"bench",
"dependencies",
"dev-dependencies",
"build-dependencies",
"target",
"features",
"patch",
"replace",
"profile",
"workspace",
]
}
pub(crate) fn serialize_canonical(value: &toml::Value) -> Result<Vec<u8>, Error> {
let top = match value {
toml::Value::Table(t) => t,
other => {
return Err(Error::TomlParse {
path: PathBuf::from("<overlay>"),
message: format!(
"overlay serializer expected a TOML document (table) at the top level, got `{}`",
type_name_of(other)
),
});
}
};
let mut emitted: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
let mut order: Vec<String> = Vec::with_capacity(top.len());
for canonical in canonical_key_order() {
if top.contains_key(*canonical) {
order.push((*canonical).to_string());
emitted.insert(*canonical);
}
}
let mut leftovers: Vec<&String> = top
.keys()
.filter(|k| !emitted.contains(k.as_str()))
.collect();
leftovers.sort();
for k in leftovers {
order.push(k.clone());
}
let mut segments: Vec<String> = Vec::with_capacity(order.len());
for key in &order {
let v = top.get(key).expect("key came from `top`'s own iteration");
let mut wrapper = toml::map::Map::new();
wrapper.insert(key.clone(), v.clone());
let segment =
toml::ser::to_string(&toml::Value::Table(wrapper)).map_err(|e: toml::ser::Error| {
Error::TomlParse {
path: PathBuf::from("<overlay>"),
message: format!("overlay serializer failed for `{key}`: {e}"),
}
})?;
segments.push(segment);
}
let joined = segments.join("\n");
let normalized = post_process_output(&joined);
Ok(normalized.into_bytes())
}
fn post_process_output(input: &str) -> String {
let mut lines: Vec<&str> = Vec::with_capacity(input.lines().count());
for line in input.lines() {
let trimmed = line.trim_end_matches([' ', '\t', '\r']);
lines.push(trimmed);
}
let mut out = String::with_capacity(input.len());
let mut prev_blank = false;
for line in &lines {
let is_blank = line.is_empty();
if is_blank && prev_blank {
continue;
}
out.push_str(line);
out.push('\n');
prev_blank = is_blank;
}
while out.ends_with("\n\n") {
out.pop();
}
if !out.ends_with('\n') {
out.push('\n');
}
out
}
fn scan_dropped_comments(text: &str) -> Vec<String> {
let bytes = text.as_bytes();
let mut out: Vec<String> = Vec::new();
let mut i = 0usize;
let mut in_basic = false;
let mut in_literal = false;
let mut in_multi_basic = false;
let mut in_multi_literal = false;
while i < bytes.len() {
let b = bytes[i];
if in_multi_basic {
if b == b'\\' && i + 1 < bytes.len() {
i += 2;
continue;
}
if b == b'"' && i + 2 < bytes.len() && bytes[i + 1] == b'"' && bytes[i + 2] == b'"' {
in_multi_basic = false;
i += 3;
continue;
}
i += 1;
continue;
}
if in_multi_literal {
if b == b'\'' && i + 2 < bytes.len() && bytes[i + 1] == b'\'' && bytes[i + 2] == b'\'' {
in_multi_literal = false;
i += 3;
continue;
}
i += 1;
continue;
}
if in_basic {
if b == b'\\' && i + 1 < bytes.len() {
i += 2;
continue;
}
if b == b'"' {
in_basic = false;
i += 1;
continue;
}
if b == b'\n' {
in_basic = false;
i += 1;
continue;
}
i += 1;
continue;
}
if in_literal {
if b == b'\'' {
in_literal = false;
i += 1;
continue;
}
if b == b'\n' {
in_literal = false;
i += 1;
continue;
}
i += 1;
continue;
}
if b == b'#' {
let start = i + 1;
let mut end = start;
while end < bytes.len() && bytes[end] != b'\n' {
end += 1;
}
let body = &text[start..end];
out.push(body.trim().to_string());
i = end;
continue;
}
if b == b'"' {
if i + 2 < bytes.len() && bytes[i + 1] == b'"' && bytes[i + 2] == b'"' {
in_multi_basic = true;
i += 3;
continue;
}
in_basic = true;
i += 1;
continue;
}
if b == b'\'' {
if i + 2 < bytes.len() && bytes[i + 1] == b'\'' && bytes[i + 2] == b'\'' {
in_multi_literal = true;
i += 3;
continue;
}
in_literal = true;
i += 1;
continue;
}
i += 1;
}
out
}
#[cfg(test)]
fn extract_unquoted_comment(line: &str) -> Option<String> {
let comments = scan_dropped_comments(line);
comments.into_iter().next()
}
fn type_name_of(v: &toml::Value) -> &'static str {
match v {
toml::Value::String(_) => "string",
toml::Value::Integer(_) => "integer",
toml::Value::Float(_) => "float",
toml::Value::Boolean(_) => "boolean",
toml::Value::Datetime(_) => "datetime",
toml::Value::Array(_) => "array",
toml::Value::Table(_) => "table",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonicalize_inserts_dylib_rlib_when_absent() {
let mut t = toml::map::Map::new();
canonicalize_crate_type(&mut t).unwrap();
let ct = t.get("crate-type").unwrap().as_array().unwrap();
let strs: Vec<&str> = ct.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(strs, vec!["dylib", "rlib"]);
}
#[test]
fn canonicalize_prepends_dylib_to_rlib_only() {
let mut t = toml::map::Map::new();
t.insert(
"crate-type".into(),
toml::Value::Array(vec![toml::Value::String("rlib".into())]),
);
canonicalize_crate_type(&mut t).unwrap();
let ct = t.get("crate-type").unwrap().as_array().unwrap();
let strs: Vec<&str> = ct.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(strs, vec!["dylib", "rlib"]);
}
#[test]
fn canonicalize_appends_rlib_when_only_dylib() {
let mut t = toml::map::Map::new();
t.insert(
"crate-type".into(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
canonicalize_crate_type(&mut t).unwrap();
let ct = t.get("crate-type").unwrap().as_array().unwrap();
let strs: Vec<&str> = ct.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(strs, vec!["dylib", "rlib"]);
}
#[test]
fn canonicalize_preserves_cdylib_after_pair() {
let mut t = toml::map::Map::new();
t.insert(
"crate-type".into(),
toml::Value::Array(vec![toml::Value::String("cdylib".into())]),
);
canonicalize_crate_type(&mut t).unwrap();
let ct = t.get("crate-type").unwrap().as_array().unwrap();
let strs: Vec<&str> = ct.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(strs, vec!["dylib", "rlib", "cdylib"]);
}
#[test]
fn canonicalize_dedups_duplicates() {
let mut t = toml::map::Map::new();
t.insert(
"crate-type".into(),
toml::Value::Array(vec![
toml::Value::String("rlib".into()),
toml::Value::String("dylib".into()),
toml::Value::String("rlib".into()),
toml::Value::String("cdylib".into()),
]),
);
canonicalize_crate_type(&mut t).unwrap();
let ct = t.get("crate-type").unwrap().as_array().unwrap();
let strs: Vec<&str> = ct.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(strs, vec!["dylib", "rlib", "cdylib"]);
}
#[test]
fn canonicalize_rejects_non_string_element() {
let mut t = toml::map::Map::new();
t.insert(
"crate-type".into(),
toml::Value::Array(vec![toml::Value::Integer(1)]),
);
let err = canonicalize_crate_type(&mut t).unwrap_err();
let s = format!("{err:?}");
assert!(
s.contains("not a string"),
"diagnostic must name the failure: {s}"
);
}
#[test]
fn canonical_key_order_starts_with_package() {
assert_eq!(canonical_key_order()[0], "package");
}
#[test]
fn extract_unquoted_comment_strips_leading_hash() {
assert_eq!(
extract_unquoted_comment("# a leading comment"),
Some("a leading comment".into())
);
}
#[test]
fn extract_unquoted_comment_handles_trailing() {
assert_eq!(
extract_unquoted_comment(r#"name = "demo" # trailing"#),
Some("trailing".into())
);
}
#[test]
fn extract_unquoted_comment_ignores_hash_inside_string() {
assert_eq!(
extract_unquoted_comment(r#"url = "http://example.com/#anchor""#),
None
);
}
#[test]
fn extract_unquoted_comment_ignores_hash_inside_single_quote() {
assert_eq!(extract_unquoted_comment(r#"name = 'foo#bar'"#), None);
}
#[test]
fn scan_ignores_hash_inside_multiline_basic_string() {
let text = "description = \"\"\"\nline with #notacomment\n\"\"\"\n";
let comments = scan_dropped_comments(text);
assert!(
comments.iter().all(|c| !c.contains("notacomment")),
"multi-line basic string body must not be classified as a comment; got {comments:?}",
);
}
#[test]
fn scan_ignores_hash_inside_multiline_literal_string() {
let text = "description = '''\nline with #stillnotacomment\n'''\n";
let comments = scan_dropped_comments(text);
assert!(
comments.iter().all(|c| !c.contains("stillnotacomment")),
"multi-line literal string body must not be classified as a comment; got {comments:?}",
);
}
#[test]
fn scan_recognizes_comment_after_multiline_string_closes() {
let text = "description = \"\"\"\nblock\n\"\"\" # real comment\n";
let comments = scan_dropped_comments(text);
assert!(
comments.iter().any(|c| c == "real comment"),
"comment AFTER the multi-line string close must be captured; got {comments:?}",
);
assert!(
comments.iter().all(|c| !c.contains("block")),
"multi-line body must never appear as a comment; got {comments:?}",
);
}
#[test]
fn scan_basic_string_escape_does_not_strand_state() {
let text = "name = \"foo \\\" #notacomment\"\n# real\n";
let comments = scan_dropped_comments(text);
assert!(
!comments.iter().any(|c| c.contains("notacomment")),
"escaped quote inside basic string must keep scanner in-string; got {comments:?}",
);
assert!(
comments.iter().any(|c| c == "real"),
"comment on the following line must still be captured; got {comments:?}",
);
}
#[test]
fn post_process_strips_trailing_whitespace() {
let raw = "foo = 1 \nbar = 2\t\n";
let out = post_process_output(raw);
assert!(out.lines().all(|l| !l.ends_with(' ') && !l.ends_with('\t')));
}
#[test]
fn post_process_strips_cr() {
let raw = "foo = 1\r\nbar = 2\r\n";
let out = post_process_output(raw);
assert!(!out.contains('\r'));
}
#[test]
fn post_process_collapses_blank_runs() {
let raw = "foo = 1\n\n\n\nbar = 2\n";
let out = post_process_output(raw);
assert_eq!(out, "foo = 1\n\nbar = 2\n");
}
#[test]
fn serialize_canonical_emits_package_first() {
let input = r#"
[features]
default = []
[dependencies]
serde = "1"
[package]
name = "demo"
version = "0.1.0"
"#;
let val: toml::Value = toml::from_str(input).unwrap();
let bytes = serialize_canonical(&val).unwrap();
let out = String::from_utf8(bytes).unwrap();
let first_header = out.lines().find(|l| l.starts_with('[')).unwrap();
assert_eq!(first_header, "[package]", "got:\n{out}");
}
#[test]
fn absolutize_injects_lib_path_when_absent() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("demo".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
absolutize_path_bearing_keys(&mut top, upstream_dir);
let lib = top.get("lib").and_then(|v| v.as_table()).unwrap();
let path = lib.get("path").and_then(|v| v.as_str()).unwrap();
assert_eq!(
path, "/work/demo/src/lib.rs",
"[lib] path must be the absolute upstream src/lib.rs; got `{path}`"
);
}
#[test]
fn absolutize_leaves_absolute_lib_path_unchanged() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
lib.insert(
"path".to_string(),
toml::Value::String("/elsewhere/src/lib.rs".into()),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
absolutize_path_bearing_keys(&mut top, upstream_dir);
let lib = top.get("lib").and_then(|v| v.as_table()).unwrap();
let path = lib.get("path").and_then(|v| v.as_str()).unwrap();
assert_eq!(
path, "/elsewhere/src/lib.rs",
"an absolute [lib] path must be preserved; got `{path}`"
);
}
#[test]
fn absolutize_rewrites_relative_lib_path() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
lib.insert(
"path".to_string(),
toml::Value::String("custom/lib.rs".into()),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
absolutize_path_bearing_keys(&mut top, upstream_dir);
let lib = top.get("lib").and_then(|v| v.as_table()).unwrap();
let path = lib.get("path").and_then(|v| v.as_str()).unwrap();
assert_eq!(
path, "/work/demo/custom/lib.rs",
"a relative [lib] path must be absolutized against upstream_dir; got `{path}`"
);
}
#[test]
fn absolutize_rewrites_dependencies_path() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut deps = toml::map::Map::new();
let mut inner = toml::map::Map::new();
inner.insert("path".to_string(), toml::Value::String("impl".into()));
deps.insert("inner-impl".to_string(), toml::Value::Table(inner));
top.insert("dependencies".to_string(), toml::Value::Table(deps));
absolutize_path_bearing_keys(&mut top, upstream_dir);
let deps = top.get("dependencies").and_then(|v| v.as_table()).unwrap();
let inner = deps.get("inner-impl").and_then(|v| v.as_table()).unwrap();
let path = inner.get("path").and_then(|v| v.as_str()).unwrap();
assert_eq!(path, "/work/demo/impl");
}
#[test]
fn absolutize_rewrites_target_conditional_dependencies_path() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut targets = toml::map::Map::new();
let mut linux = toml::map::Map::new();
let mut deps = toml::map::Map::new();
let mut platform_dep = toml::map::Map::new();
platform_dep.insert("path".to_string(), toml::Value::String("linux-impl".into()));
deps.insert(
"platform-bits".to_string(),
toml::Value::Table(platform_dep),
);
linux.insert("dependencies".to_string(), toml::Value::Table(deps));
targets.insert(
r#"cfg(target_os = "linux")"#.to_string(),
toml::Value::Table(linux),
);
top.insert("target".to_string(), toml::Value::Table(targets));
absolutize_path_bearing_keys(&mut top, upstream_dir);
let targets = top.get("target").and_then(|v| v.as_table()).unwrap();
let linux = targets
.get(r#"cfg(target_os = "linux")"#)
.and_then(|v| v.as_table())
.unwrap();
let deps = linux
.get("dependencies")
.and_then(|v| v.as_table())
.unwrap();
let platform_dep = deps
.get("platform-bits")
.and_then(|v| v.as_table())
.unwrap();
let path = platform_dep.get("path").and_then(|v| v.as_str()).unwrap();
assert_eq!(
path, "/work/demo/linux-impl",
"[target.*.dependencies.X].path must be absolutized; got `{path}`"
);
}
#[test]
fn absolutize_rewrites_workspace_members_and_exclude() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("demo".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut ws = toml::map::Map::new();
ws.insert(
"members".to_string(),
toml::Value::Array(vec![
toml::Value::String("crate-a".into()),
toml::Value::String("crate-b".into()),
toml::Value::String("/elsewhere/crate-c".into()),
]),
);
ws.insert(
"exclude".to_string(),
toml::Value::Array(vec![toml::Value::String("scratch".into())]),
);
top.insert("workspace".to_string(), toml::Value::Table(ws));
absolutize_path_bearing_keys(&mut top, upstream_dir);
let ws = top.get("workspace").and_then(|v| v.as_table()).unwrap();
let members = ws.get("members").and_then(|v| v.as_array()).unwrap();
let member_strs: Vec<&str> = members.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(
member_strs,
vec![
"/work/demo/crate-a",
"/work/demo/crate-b",
"/elsewhere/crate-c"
],
"[workspace] members must be absolutized, leaving already-absolute entries alone"
);
let exclude = ws.get("exclude").and_then(|v| v.as_array()).unwrap();
let exclude_strs: Vec<&str> = exclude.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(exclude_strs, vec!["/work/demo/scratch"]);
}
#[test]
fn absolutize_does_not_inject_build_when_upstream_has_no_build_rs() {
let upstream_dir = Path::new("/work/demo-no-build-rs");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("demo".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
absolutize_path_bearing_keys(&mut top, upstream_dir);
let pkg = top.get("package").and_then(|v| v.as_table()).unwrap();
assert!(
!pkg.contains_key("build"),
"build key must not be injected when no upstream build.rs exists; \
got pkg keys {:?}",
pkg.keys().collect::<Vec<_>>()
);
}
#[test]
fn absolutize_disables_non_lib_auto_discovery() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("demo".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
absolutize_path_bearing_keys(&mut top, upstream_dir);
let pkg = top.get("package").and_then(|v| v.as_table()).unwrap();
for key in ["autobins", "autoexamples", "autotests", "autobenches"] {
let val = pkg.get(key).and_then(|v| v.as_bool());
assert_eq!(
val,
Some(false),
"[package] {key} must be `false` to disable cargo auto-discovery; \
got {val:?}",
);
}
}
#[test]
fn absolutize_rewrites_array_table_paths() {
let upstream_dir = Path::new("/work/demo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("dylib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
for (section, value) in [
("bin", "src/bin/foo.rs"),
("example", "examples/eg.rs"),
("test", "tests/it.rs"),
("bench", "benches/bench.rs"),
] {
let mut entry = toml::map::Map::new();
entry.insert("name".to_string(), toml::Value::String("target".into()));
entry.insert("path".to_string(), toml::Value::String(value.into()));
top.insert(
section.to_string(),
toml::Value::Array(vec![toml::Value::Table(entry)]),
);
}
absolutize_path_bearing_keys(&mut top, upstream_dir);
for (section, original) in [
("bin", "src/bin/foo.rs"),
("example", "examples/eg.rs"),
("test", "tests/it.rs"),
("bench", "benches/bench.rs"),
] {
let arr = top.get(section).and_then(|v| v.as_array()).unwrap();
let entry = arr[0].as_table().unwrap();
let path = entry.get("path").and_then(|v| v.as_str()).unwrap();
let expected = format!("/work/demo/{original}");
assert_eq!(
path, expected,
"[[{section}]] path must be absolutized to `{expected}`; got `{path}`"
);
}
}
#[test]
fn absolutizes_package_workspace_pointer() {
let upstream_dir = Path::new("/work/cxx");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("rlib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("cxx".into()));
pkg.insert("workspace".to_string(), toml::Value::String("../".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
absolutize_path_bearing_keys(&mut top, upstream_dir);
let pkg = top.get("package").and_then(|v| v.as_table()).unwrap();
let ws_ptr = pkg.get("workspace").and_then(|v| v.as_str()).unwrap();
assert_eq!(
ws_ptr, "/work/cxx/../",
"[package].workspace must be absolutized as Path::join (no normalization); got `{ws_ptr}`"
);
}
#[test]
fn absolutizes_workspace_default_members() {
let upstream_dir = Path::new("/work/repo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("rlib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("repo".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut ws = toml::map::Map::new();
ws.insert(
"default-members".to_string(),
toml::Value::Array(vec![
toml::Value::String("crate-a".into()),
toml::Value::String("crate-b".into()),
]),
);
top.insert("workspace".to_string(), toml::Value::Table(ws));
absolutize_path_bearing_keys(&mut top, upstream_dir);
let ws = top.get("workspace").and_then(|v| v.as_table()).unwrap();
let dm = ws
.get("default-members")
.and_then(|v| v.as_array())
.unwrap();
let dm_strs: Vec<&str> = dm.iter().filter_map(|v| v.as_str()).collect();
assert_eq!(
dm_strs,
vec!["/work/repo/crate-a", "/work/repo/crate-b"],
"[workspace].default-members must be absolutized; got {dm_strs:?}"
);
}
#[test]
fn absolutizes_workspace_dependencies_path() {
let upstream_dir = Path::new("/work/monorepo");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("rlib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("monorepo".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut ws = toml::map::Map::new();
let mut ws_deps = toml::map::Map::new();
let mut impl_dep = toml::map::Map::new();
impl_dep.insert("path".to_string(), toml::Value::String("impl".into()));
ws_deps.insert("my-impl".to_string(), toml::Value::Table(impl_dep));
let mut proc_macro_dep = toml::map::Map::new();
proc_macro_dep.insert("path".to_string(), toml::Value::String("proc-macro".into()));
ws_deps.insert(
"my-proc-macro".to_string(),
toml::Value::Table(proc_macro_dep),
);
ws.insert("dependencies".to_string(), toml::Value::Table(ws_deps));
top.insert("workspace".to_string(), toml::Value::Table(ws));
absolutize_path_bearing_keys(&mut top, upstream_dir);
let ws = top.get("workspace").and_then(|v| v.as_table()).unwrap();
let ws_deps = ws.get("dependencies").and_then(|v| v.as_table()).unwrap();
let impl_path = ws_deps
.get("my-impl")
.and_then(|v| v.as_table())
.and_then(|t| t.get("path"))
.and_then(|v| v.as_str())
.unwrap();
assert_eq!(
impl_path, "/work/monorepo/impl",
"[workspace.dependencies.my-impl].path must be absolutized; got `{impl_path}`"
);
let pm_path = ws_deps
.get("my-proc-macro")
.and_then(|v| v.as_table())
.and_then(|t| t.get("path"))
.and_then(|v| v.as_str())
.unwrap();
assert_eq!(
pm_path, "/work/monorepo/proc-macro",
"[workspace.dependencies.my-proc-macro].path must be absolutized; got `{pm_path}`"
);
}
#[test]
fn absolutizes_patch_registry_path() {
let upstream_dir = Path::new("/work/cxx");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("rlib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("cxx".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut cxx_entry = toml::map::Map::new();
cxx_entry.insert("path".to_string(), toml::Value::String(".".into()));
let mut cxx_build_entry = toml::map::Map::new();
cxx_build_entry.insert("path".to_string(), toml::Value::String("gen/build".into()));
let mut serde_entry = toml::map::Map::new();
serde_entry.insert(
"git".to_string(),
toml::Value::String("https://github.com/serde-rs/serde".into()),
);
serde_entry.insert("branch".to_string(), toml::Value::String("master".into()));
let mut crates_io = toml::map::Map::new();
crates_io.insert("cxx".to_string(), toml::Value::Table(cxx_entry));
crates_io.insert("cxx-build".to_string(), toml::Value::Table(cxx_build_entry));
crates_io.insert("serde".to_string(), toml::Value::Table(serde_entry));
let mut patch = toml::map::Map::new();
patch.insert("crates-io".to_string(), toml::Value::Table(crates_io));
top.insert("patch".to_string(), toml::Value::Table(patch));
absolutize_path_bearing_keys(&mut top, upstream_dir);
let patch = top.get("patch").and_then(|v| v.as_table()).unwrap();
let crates_io = patch.get("crates-io").and_then(|v| v.as_table()).unwrap();
let cxx_path = crates_io
.get("cxx")
.and_then(|v| v.as_table())
.and_then(|t| t.get("path"))
.and_then(|v| v.as_str())
.unwrap();
assert_eq!(
cxx_path, "/work/cxx/.",
"[patch.crates-io.cxx].path absolutized via Path::join preserves the `.`; \
cargo treats `/work/cxx/.` as equivalent to `/work/cxx`; got `{cxx_path}`"
);
let cxx_build_path = crates_io
.get("cxx-build")
.and_then(|v| v.as_table())
.and_then(|t| t.get("path"))
.and_then(|v| v.as_str())
.unwrap();
assert_eq!(
cxx_build_path, "/work/cxx/gen/build",
"[patch.crates-io.cxx-build].path must be absolutized; got `{cxx_build_path}`"
);
let serde = crates_io.get("serde").and_then(|v| v.as_table()).unwrap();
assert!(
!serde.contains_key("path"),
"git-form patch entry must not gain a path key"
);
assert_eq!(
serde.get("git").and_then(|v| v.as_str()),
Some("https://github.com/serde-rs/serde"),
"git URL in git-form patch entry must be unchanged"
);
assert_eq!(
serde.get("branch").and_then(|v| v.as_str()),
Some("master"),
"branch in git-form patch entry must be unchanged"
);
}
#[test]
fn absolutize_leaves_absolute_patch_path_unchanged() {
let upstream_dir = Path::new("/work/cxx");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("rlib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("cxx".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut abs_entry = toml::map::Map::new();
abs_entry.insert(
"path".to_string(),
toml::Value::String("/absolute/path/to/cxx".into()),
);
let mut crates_io = toml::map::Map::new();
crates_io.insert("cxx".to_string(), toml::Value::Table(abs_entry));
let mut patch = toml::map::Map::new();
patch.insert("crates-io".to_string(), toml::Value::Table(crates_io));
top.insert("patch".to_string(), toml::Value::Table(patch));
absolutize_path_bearing_keys(&mut top, upstream_dir);
let path = top
.get("patch")
.and_then(|v| v.as_table())
.and_then(|t| t.get("crates-io"))
.and_then(|v| v.as_table())
.and_then(|t| t.get("cxx"))
.and_then(|v| v.as_table())
.and_then(|t| t.get("path"))
.and_then(|v| v.as_str())
.unwrap();
assert_eq!(
path, "/absolute/path/to/cxx",
"an absolute [patch.*.*].path must be left unchanged; got `{path}`"
);
}
#[test]
fn absolutizes_replace_path() {
let upstream_dir = Path::new("/work/project");
let mut top = toml::map::Map::new();
let mut lib = toml::map::Map::new();
lib.insert(
"crate-type".to_string(),
toml::Value::Array(vec![toml::Value::String("rlib".into())]),
);
top.insert("lib".to_string(), toml::Value::Table(lib));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("project".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut cxx_entry = toml::map::Map::new();
cxx_entry.insert("path".to_string(), toml::Value::String("vendor/cxx".into()));
let mut serde_entry = toml::map::Map::new();
serde_entry.insert(
"git".to_string(),
toml::Value::String("https://github.com/serde-rs/serde".into()),
);
serde_entry.insert("rev".to_string(), toml::Value::String("abc123".into()));
let mut abs_entry = toml::map::Map::new();
abs_entry.insert(
"path".to_string(),
toml::Value::String("/pre-existing/absolute/path".into()),
);
let mut replace = toml::map::Map::new();
replace.insert("cxx:0.3.0".to_string(), toml::Value::Table(cxx_entry));
replace.insert("serde:1.0.0".to_string(), toml::Value::Table(serde_entry));
replace.insert("abs-dep:0.1.0".to_string(), toml::Value::Table(abs_entry));
top.insert("replace".to_string(), toml::Value::Table(replace));
absolutize_path_bearing_keys(&mut top, upstream_dir);
let replace_out = top.get("replace").and_then(|v| v.as_table()).unwrap();
let cxx_path = replace_out
.get("cxx:0.3.0")
.and_then(|v| v.as_table())
.and_then(|t| t.get("path"))
.and_then(|v| v.as_str())
.unwrap();
assert_eq!(
cxx_path, "/work/project/vendor/cxx",
"[replace.\"cxx:0.3.0\"].path must be absolutized; got `{cxx_path}`"
);
let serde_t = replace_out
.get("serde:1.0.0")
.and_then(|v| v.as_table())
.unwrap();
assert!(
!serde_t.contains_key("path"),
"git-form [replace] entry must not gain a `path` key"
);
assert_eq!(
serde_t.get("git").and_then(|v| v.as_str()),
Some("https://github.com/serde-rs/serde"),
"git URL in git-form [replace] entry must be unchanged"
);
let abs_path = replace_out
.get("abs-dep:0.1.0")
.and_then(|v| v.as_table())
.and_then(|t| t.get("path"))
.and_then(|v| v.as_str())
.unwrap();
assert_eq!(
abs_path, "/pre-existing/absolute/path",
"an already-absolute [replace] path must be left unchanged; got `{abs_path}`"
);
}
fn dummy_upstream_manifest_path() -> std::path::PathBuf {
std::path::PathBuf::from("/tmp/lihaaf-test-upstream/Cargo.toml")
}
#[test]
fn override_workspace_preserves_inheritance_tables() {
let mut top = toml::map::Map::new();
let mut ws = toml::map::Map::new();
ws.insert(
"members".to_string(),
toml::Value::Array(vec![toml::Value::String("crate-a".into())]),
);
ws.insert(
"exclude".to_string(),
toml::Value::Array(vec![toml::Value::String("scratch".into())]),
);
ws.insert(
"default-members".to_string(),
toml::Value::Array(vec![toml::Value::String("crate-a".into())]),
);
ws.insert("resolver".to_string(), toml::Value::String("2".into()));
let mut ws_deps = toml::map::Map::new();
let mut shared = toml::map::Map::new();
shared.insert("path".to_string(), toml::Value::String("/abs/utils".into()));
ws_deps.insert("shared-utils".to_string(), toml::Value::Table(shared));
ws.insert("dependencies".to_string(), toml::Value::Table(ws_deps));
let mut ws_pkg = toml::map::Map::new();
ws_pkg.insert("edition".to_string(), toml::Value::String("2021".into()));
ws_pkg.insert("version".to_string(), toml::Value::String("0.1.0".into()));
ws.insert("package".to_string(), toml::Value::Table(ws_pkg));
let mut ws_lints = toml::map::Map::new();
let mut ws_lints_rust = toml::map::Map::new();
ws_lints_rust.insert(
"unsafe_code".to_string(),
toml::Value::String("forbid".into()),
);
ws_lints.insert("rust".to_string(), toml::Value::Table(ws_lints_rust));
ws.insert("lints".to_string(), toml::Value::Table(ws_lints));
let mut ws_meta = toml::map::Map::new();
let mut ws_meta_tool = toml::map::Map::new();
ws_meta_tool.insert("key".to_string(), toml::Value::String("value".into()));
ws_meta.insert("my-tool".to_string(), toml::Value::Table(ws_meta_tool));
ws.insert("metadata".to_string(), toml::Value::Table(ws_meta));
let mut ws_future = toml::map::Map::new();
ws_future.insert(
"key".to_string(),
toml::Value::String("future-value".into()),
);
ws.insert(
"future-cargo-feature".to_string(),
toml::Value::Table(ws_future),
);
top.insert("workspace".to_string(), toml::Value::Table(ws));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("test".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
override_workspace_inheritance(&mut top, &dummy_upstream_manifest_path())
.expect("workspace-root case must succeed");
let ws_out = top.get("workspace").and_then(|v| v.as_table()).unwrap();
for stripped in ["members", "exclude", "default-members"] {
assert!(
!ws_out.contains_key(stripped),
"membership key `{stripped}` MUST be stripped; got keys: {:?}",
ws_out.keys().collect::<Vec<_>>()
);
}
assert!(
ws_out.contains_key("dependencies"),
"workspace.dependencies must survive"
);
assert!(
ws_out.contains_key("package"),
"workspace.package must survive"
);
assert!(ws_out.contains_key("lints"), "workspace.lints must survive");
assert!(
ws_out.contains_key("metadata"),
"workspace.metadata must survive"
);
assert!(
ws_out.contains_key("resolver"),
"workspace.resolver must survive"
);
assert!(
ws_out.contains_key("future-cargo-feature"),
"unknown `[workspace.X]` table must pass through (forward-compat)"
);
let ws_deps_out = ws_out
.get("dependencies")
.and_then(|v| v.as_table())
.unwrap();
let shared_out = ws_deps_out
.get("shared-utils")
.and_then(|v| v.as_table())
.unwrap();
assert_eq!(
shared_out.get("path").and_then(|v| v.as_str()),
Some("/abs/utils"),
"workspace.dependencies.shared-utils.path must pass through verbatim"
);
let ws_pkg_out = ws_out.get("package").and_then(|v| v.as_table()).unwrap();
assert_eq!(
ws_pkg_out.get("edition").and_then(|v| v.as_str()),
Some("2021"),
"workspace.package.edition must pass through verbatim"
);
}
#[test]
fn override_workspace_injects_empty_when_absent() {
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("test".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
assert!(!top.contains_key("workspace"));
override_workspace_inheritance(&mut top, &dummy_upstream_manifest_path())
.expect("missing `[workspace]` must inject an empty one");
let ws_out = top.get("workspace").and_then(|v| v.as_table()).unwrap();
assert!(
ws_out.is_empty(),
"injected `[workspace]` must be empty when upstream had none; got: {:?}",
ws_out.keys().collect::<Vec<_>>()
);
}
#[test]
fn override_workspace_rejects_workspace_member_manifest() {
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("member".into()));
pkg.insert("workspace".to_string(), toml::Value::String("../".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let err = override_workspace_inheritance(&mut top, &dummy_upstream_manifest_path())
.expect_err("workspace-member manifest must be rejected");
match err {
Error::Cli {
clap_exit_code,
message,
} => {
assert_eq!(
clap_exit_code, 2,
"exit code must be the clap usage code (2)"
);
assert!(
message.contains("workspace member"),
"rejection diagnostic must name the failure category; got: {message}"
);
assert!(
message.contains("[package].workspace"),
"rejection diagnostic must name the offending key; got: {message}"
);
assert!(
message.contains("/tmp/lihaaf-test-upstream/Cargo.toml"),
"rejection diagnostic must include the offending manifest path; got: {message}"
);
assert!(
!message.contains("implicit"),
"explicit rejection must not use the implicit-case wording; got: {message}"
);
}
other => panic!("expected Error::Cli for workspace-member rejection, got {other:?}"),
}
}
#[test]
fn override_workspace_rejects_implicit_workspace_member_manifest() {
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("member".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("workspace".to_string(), toml::Value::Boolean(true));
deps.insert("foo".to_string(), toml::Value::Table(foo));
top.insert("dependencies".to_string(), toml::Value::Table(deps));
let err = override_workspace_inheritance(&mut top, &dummy_upstream_manifest_path())
.expect_err("implicit workspace-member manifest must be rejected");
match err {
Error::Cli {
clap_exit_code,
message,
} => {
assert_eq!(
clap_exit_code, 2,
"exit code must match the explicit-rejection contract (clap usage code 2)"
);
assert!(
message.contains("implicit workspace member"),
"rejection diagnostic must name the implicit-member category; got: {message}"
);
assert!(
message.contains("no local `[workspace]`"),
"diagnostic must name the diagnostic structural signal; got: {message}"
);
assert!(
message.contains("workspace = true"),
"diagnostic must point at the inheritance-reference shape; got: {message}"
);
assert!(
message.contains("/tmp/lihaaf-test-upstream/Cargo.toml"),
"diagnostic must include the offending manifest path; got: {message}"
);
assert!(
!top.contains_key("workspace"),
"rejection must not leave a half-mutated `[workspace]` entry in place"
);
}
other => {
panic!("expected Error::Cli for implicit workspace-member rejection, got {other:?}")
}
}
}
#[test]
fn override_workspace_rejects_manifest_with_ancestor_workspace() {
let tmp = tempfile::tempdir().expect("tempdir for ancestor-workspace rejection test");
let parent_manifest = tmp.path().join("Cargo.toml");
std::fs::write(
&parent_manifest,
r#"[workspace]
members = ["sub"]
[patch.crates-io]
foo = { path = "../my-foo-fork" }
"#,
)
.expect("writing parent Cargo.toml");
let sub_dir = tmp.path().join("sub");
std::fs::create_dir_all(&sub_dir).expect("creating sub/ dir");
let sub_manifest = sub_dir.join("Cargo.toml");
std::fs::write(
&sub_manifest,
r#"[package]
name = "sub"
version = "0.1.0"
"#,
)
.expect("writing sub/Cargo.toml");
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("sub".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
let err = override_workspace_inheritance(&mut top, &sub_manifest)
.expect_err("manifest with ancestor workspace must be rejected");
match err {
Error::Cli {
clap_exit_code,
message,
} => {
assert_eq!(
clap_exit_code, 2,
"exit code must match the rejection contract (clap usage code 2)"
);
assert!(
message.contains("implicit workspace member"),
"diagnostic must name the implicit-member category; got: {message}"
);
assert!(
message.contains("ancestor manifest"),
"diagnostic must name the ancestor-detection signal; got: {message}"
);
let parent_str = parent_manifest.display().to_string();
assert!(
message.contains(&parent_str),
"diagnostic must include the ancestor manifest path `{parent_str}`; got: {message}"
);
assert!(
!message.contains("workspace = true"),
"ancestor-workspace rejection must not mention inheritance refs (this case has none); got: {message}"
);
assert!(
!top.contains_key("workspace"),
"rejection must not leave a half-mutated `[workspace]` entry in place"
);
}
other => {
panic!("expected Error::Cli for ancestor-workspace rejection, got {other:?}")
}
}
}
#[test]
fn override_workspace_allows_standalone_with_no_ancestor_workspace() {
let tmp = tempfile::tempdir().expect("tempdir for standalone-allows negative-case test");
let manifest = tmp.path().join("Cargo.toml");
std::fs::write(
&manifest,
r#"[package]
name = "standalone"
version = "0.1.0"
"#,
)
.expect("writing standalone Cargo.toml");
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("standalone".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
assert!(!top.contains_key("workspace"));
override_workspace_inheritance(&mut top, &manifest).unwrap_or_else(|err| {
panic!(
"standalone manifest with no ancestor workspace must NOT be rejected; \
got: {err:?} (this would indicate an R4 regression — the ancestor walk \
spuriously detected a workspace where there is none, OR the test \
environment has an unexpected `Cargo.toml` somewhere above the temp dir)"
)
});
let ws_out = top.get("workspace").and_then(|v| v.as_table()).unwrap();
assert!(
ws_out.is_empty(),
"branch 5 (standalone) must inject an empty `[workspace]`; got keys: {:?}",
ws_out.keys().collect::<Vec<_>>()
);
}
#[test]
fn detect_implicit_ancestor_workspace_returns_none_for_standalone() {
let tmp = tempfile::tempdir().expect("tempdir for ancestor-walk None negative case");
let manifest = tmp.path().join("Cargo.toml");
std::fs::write(&manifest, "[package]\nname = \"standalone\"\n")
.expect("writing standalone Cargo.toml");
let result = detect_implicit_ancestor_workspace(&manifest)
.expect("ancestor walk on a clean tempdir must not return Err");
assert!(
result.is_none(),
"ancestor walk from a standalone tempdir manifest must return None; got: {result:?}"
);
}
#[test]
fn detect_implicit_ancestor_workspace_finds_nearest_ancestor() {
let tmp = tempfile::tempdir().expect("tempdir for ancestor-walk Some positive case");
let parent_manifest = tmp.path().join("Cargo.toml");
std::fs::write(&parent_manifest, "[workspace]\nmembers = [\"sub\"]\n")
.expect("writing parent Cargo.toml");
let sub_dir = tmp.path().join("sub");
std::fs::create_dir_all(&sub_dir).expect("creating sub/");
let sub_manifest = sub_dir.join("Cargo.toml");
std::fs::write(
&sub_manifest,
"[package]\nname = \"sub\"\nversion = \"0.1.0\"\n",
)
.expect("writing sub/Cargo.toml");
let result =
detect_implicit_ancestor_workspace(&sub_manifest).expect("ancestor walk must succeed");
let found = result.expect("ancestor walk must find the parent workspace");
assert_eq!(
found, parent_manifest,
"ancestor walk must return the parent manifest path verbatim"
);
}
#[test]
fn manifest_has_inheritance_reference_returns_false_for_non_inheriting_shapes() {
let top = toml::map::Map::new();
assert!(
!manifest_has_inheritance_reference(&top),
"empty manifest has no inheritance references"
);
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("demo".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
assert!(
!manifest_has_inheritance_reference(&top),
"manifest with `[package].name` only has no inheritance references"
);
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("version".to_string(), toml::Value::String("1.0".into()));
deps.insert("foo".to_string(), toml::Value::Table(foo));
top.insert("dependencies".to_string(), toml::Value::Table(deps));
assert!(
!manifest_has_inheritance_reference(&top),
"regular dep without `workspace = true` does not count as inheritance"
);
let mut pkg2 = toml::map::Map::new();
pkg2.insert("name".to_string(), toml::Value::String("member".into()));
pkg2.insert("workspace".to_string(), toml::Value::String("../".into()));
let mut top2 = toml::map::Map::new();
top2.insert("package".to_string(), toml::Value::Table(pkg2));
assert!(
!manifest_has_inheritance_reference(&top2),
"`[package].workspace = \"...\"` is the explicit-member pointer, not inheritance"
);
}
#[test]
fn manifest_has_inheritance_reference_detects_every_family() {
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
let mut version = toml::map::Map::new();
version.insert("workspace".to_string(), toml::Value::Boolean(true));
pkg.insert("version".to_string(), toml::Value::Table(version));
top.insert("package".to_string(), toml::Value::Table(pkg));
assert!(
manifest_has_inheritance_reference(&top),
"`[package].version = {{ workspace = true }}` must be detected"
);
let mut top = toml::map::Map::new();
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("workspace".to_string(), toml::Value::Boolean(true));
deps.insert("foo".to_string(), toml::Value::Table(foo));
top.insert("dependencies".to_string(), toml::Value::Table(deps));
assert!(
manifest_has_inheritance_reference(&top),
"`[dependencies] foo = {{ workspace = true }}` must be detected"
);
let mut top = toml::map::Map::new();
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("workspace".to_string(), toml::Value::Boolean(true));
deps.insert("foo".to_string(), toml::Value::Table(foo));
top.insert("dev-dependencies".to_string(), toml::Value::Table(deps));
assert!(
manifest_has_inheritance_reference(&top),
"`[dev-dependencies] foo = {{ workspace = true }}` must be detected"
);
let mut top = toml::map::Map::new();
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("workspace".to_string(), toml::Value::Boolean(true));
deps.insert("foo".to_string(), toml::Value::Table(foo));
top.insert("build-dependencies".to_string(), toml::Value::Table(deps));
assert!(
manifest_has_inheritance_reference(&top),
"`[build-dependencies] foo = {{ workspace = true }}` must be detected"
);
let mut top = toml::map::Map::new();
let mut targets = toml::map::Map::new();
let mut cfg = toml::map::Map::new();
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("workspace".to_string(), toml::Value::Boolean(true));
deps.insert("foo".to_string(), toml::Value::Table(foo));
cfg.insert("dependencies".to_string(), toml::Value::Table(deps));
targets.insert("cfg(unix)".to_string(), toml::Value::Table(cfg));
top.insert("target".to_string(), toml::Value::Table(targets));
assert!(
manifest_has_inheritance_reference(&top),
"`[target.<cfg>.dependencies]` inheritance must be detected"
);
let mut top = toml::map::Map::new();
let mut targets = toml::map::Map::new();
let mut cfg = toml::map::Map::new();
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("workspace".to_string(), toml::Value::Boolean(true));
deps.insert("foo".to_string(), toml::Value::Table(foo));
cfg.insert("dev-dependencies".to_string(), toml::Value::Table(deps));
targets.insert("cfg(windows)".to_string(), toml::Value::Table(cfg));
top.insert("target".to_string(), toml::Value::Table(targets));
assert!(
manifest_has_inheritance_reference(&top),
"`[target.<cfg>.dev-dependencies]` inheritance must be detected"
);
let mut top = toml::map::Map::new();
let mut targets = toml::map::Map::new();
let mut cfg = toml::map::Map::new();
let mut deps = toml::map::Map::new();
let mut foo = toml::map::Map::new();
foo.insert("workspace".to_string(), toml::Value::Boolean(true));
deps.insert("foo".to_string(), toml::Value::Table(foo));
cfg.insert("build-dependencies".to_string(), toml::Value::Table(deps));
targets.insert(
"cfg(target_arch = \"wasm32\")".to_string(),
toml::Value::Table(cfg),
);
top.insert("target".to_string(), toml::Value::Table(targets));
assert!(
manifest_has_inheritance_reference(&top),
"`[target.<cfg>.build-dependencies]` inheritance must be detected"
);
let mut top = toml::map::Map::new();
let mut lints = toml::map::Map::new();
lints.insert("workspace".to_string(), toml::Value::Boolean(true));
top.insert("lints".to_string(), toml::Value::Table(lints));
assert!(
manifest_has_inheritance_reference(&top),
"`[lints] workspace = true` (top-level form) must be detected"
);
let mut top = toml::map::Map::new();
let mut lints = toml::map::Map::new();
let mut rust = toml::map::Map::new();
rust.insert("workspace".to_string(), toml::Value::Boolean(true));
lints.insert("rust".to_string(), toml::Value::Table(rust));
top.insert("lints".to_string(), toml::Value::Table(lints));
assert!(
manifest_has_inheritance_reference(&top),
"`[lints.rust] workspace = true` (forward-compat nested form) must be detected"
);
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
let mut future = toml::map::Map::new();
future.insert("workspace".to_string(), toml::Value::Boolean(true));
pkg.insert(
"future-inheritable-key".to_string(),
toml::Value::Table(future),
);
top.insert("package".to_string(), toml::Value::Table(pkg));
assert!(
manifest_has_inheritance_reference(&top),
"unknown `[package].<future-key>` inheritance must be detected (forward-compat)"
);
}
#[test]
fn override_workspace_allows_root_with_local_workspace_and_inheritance_refs() {
let mut top = toml::map::Map::new();
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("root".into()));
let mut version = toml::map::Map::new();
version.insert("workspace".to_string(), toml::Value::Boolean(true));
pkg.insert("version".to_string(), toml::Value::Table(version));
top.insert("package".to_string(), toml::Value::Table(pkg));
let mut ws = toml::map::Map::new();
let mut ws_pkg = toml::map::Map::new();
ws_pkg.insert("version".to_string(), toml::Value::String("0.1.0".into()));
ws.insert("package".to_string(), toml::Value::Table(ws_pkg));
top.insert("workspace".to_string(), toml::Value::Table(ws));
override_workspace_inheritance(&mut top, &dummy_upstream_manifest_path())
.expect("root with local [workspace] + inheritance refs must succeed (not implicit)");
let pkg_out = top.get("package").and_then(|v| v.as_table()).unwrap();
let version_out = pkg_out.get("version").and_then(|v| v.as_table()).unwrap();
assert_eq!(
version_out.get("workspace").and_then(|v| v.as_bool()),
Some(true),
"inheritance reference must pass through verbatim for workspace-root case"
);
let ws_out = top.get("workspace").and_then(|v| v.as_table()).unwrap();
assert!(
ws_out.contains_key("package"),
"workspace.package must survive for the workspace-root case"
);
}
#[test]
fn override_workspace_is_idempotent() {
let mut top = toml::map::Map::new();
let mut ws = toml::map::Map::new();
ws.insert(
"members".to_string(),
toml::Value::Array(vec![toml::Value::String("crate-a".into())]),
);
let mut ws_deps = toml::map::Map::new();
let mut shared = toml::map::Map::new();
shared.insert("path".to_string(), toml::Value::String("/abs/utils".into()));
ws_deps.insert("shared".to_string(), toml::Value::Table(shared));
ws.insert("dependencies".to_string(), toml::Value::Table(ws_deps));
top.insert("workspace".to_string(), toml::Value::Table(ws));
let mut pkg = toml::map::Map::new();
pkg.insert("name".to_string(), toml::Value::String("test".into()));
top.insert("package".to_string(), toml::Value::Table(pkg));
override_workspace_inheritance(&mut top, &dummy_upstream_manifest_path()).unwrap();
let after_first = top.clone();
override_workspace_inheritance(&mut top, &dummy_upstream_manifest_path()).unwrap();
assert_eq!(
top, after_first,
"second call must be a no-op on already-overridden output"
);
}
}