use std::fs;
use std::path::{Path, PathBuf};
use toml::{Table, Value};
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("crates/<name> always has two ancestors")
.to_path_buf()
}
fn manifest(path: &Path) -> Table {
let text = fs::read_to_string(path).unwrap_or_else(|e| panic!("read {path:?}: {e}"));
text.parse::<Table>()
.unwrap_or_else(|e| panic!("parse {path:?}: {e}"))
}
const DEP_TABLES: [&str; 3] = ["dependencies", "dev-dependencies", "build-dependencies"];
fn is_publishable(manifest: &Table) -> bool {
match manifest.get("package").and_then(|p| p.get("publish")) {
None => true,
Some(Value::Boolean(allowed)) => *allowed,
Some(Value::Array(registries)) => !registries.is_empty(),
Some(_) => true,
}
}
fn member_dirs(root: &Path, members: &[Value]) -> Vec<String> {
let mut out = Vec::new();
for member in members {
let member = member.as_str().expect("a member entry is a string");
let Some(parent) = member.strip_suffix("/*") else {
out.push(member.to_string());
continue;
};
let listing = fs::read_dir(root.join(parent))
.unwrap_or_else(|e| panic!("read {parent}/ to expand `{member}`: {e}"));
for entry in listing.flatten() {
if entry.path().join("Cargo.toml").is_file() {
out.push(format!("{parent}/{}", entry.file_name().to_string_lossy()));
}
}
}
out
}
#[test]
fn internal_pins_match_the_workspace_version() {
let root = workspace_root();
let root_manifest = manifest(&root.join("Cargo.toml"));
let workspace = root_manifest
.get("workspace")
.expect("the root manifest has a [workspace] table");
let version = workspace
.get("package")
.and_then(|p| p.get("version"))
.and_then(Value::as_str)
.expect("[workspace.package] version is set");
let deps = workspace
.get("dependencies")
.and_then(Value::as_table)
.expect("[workspace.dependencies] exists");
let mut checked = 0;
for (name, dep) in deps {
let Some(table) = dep.as_table() else {
continue;
};
let Some(path) = table.get("path").and_then(Value::as_str) else {
continue;
};
match table.get("version").and_then(Value::as_str) {
Some(pinned) => {
assert_eq!(
pinned, version,
"\n {name} is pinned at {pinned} while the workspace is {version}.\n\
A release bumps `[workspace.package] version`; this does not follow it, \
so it has to be bumped in the same commit.\n"
);
checked += 1;
}
None => assert!(
!is_publishable(&manifest(&root.join(path).join("Cargo.toml"))),
"{name} is published but depended on by path alone. crates.io needs \
a requirement it can resolve for somebody who is not building from \
this checkout, so it needs `version = \"{version}\"` too."
),
}
}
assert!(
checked >= 1,
"expected at least one internal pin to check and found none — have the \
workspace's own crates left `[workspace.dependencies]`?"
);
}
#[test]
fn members_inherit_the_workspace_version() {
let root = workspace_root();
let root_manifest = manifest(&root.join("Cargo.toml"));
let members = root_manifest
.get("workspace")
.and_then(|w| w.get("members"))
.and_then(Value::as_array)
.expect("[workspace] members is set");
assert!(!members.is_empty(), "no workspace members to check");
for member in member_dirs(&root, members) {
let path = root.join(&member).join("Cargo.toml");
let parsed = manifest(&path);
let package = parsed
.get("package")
.unwrap_or_else(|| panic!("{member} has no [package] table"));
let inherits = package
.get("version")
.and_then(Value::as_table)
.and_then(|t| t.get("workspace"))
.and_then(Value::as_bool)
== Some(true);
assert!(
inherits,
"{member} sets its own version ({:?}) instead of inheriting it with \
`version.workspace = true`",
package.get("version")
);
for table in DEP_TABLES {
let Some(deps) = parsed.get(table).and_then(Value::as_table) else {
continue;
};
for (name, dep) in deps {
let has_path = dep.as_table().is_some_and(|t| t.contains_key("path"));
assert!(
!has_path,
"{member} declares {name} by path under [{table}]; put it in \
`[workspace.dependencies]` and use `{name}.workspace = true`, \
so the version behind it stays in one place"
);
}
}
}
}