use std::path::Path;
use crate::contract::schema::{Ecosystem, Maturity};
use crate::ports::{Fs, GitRepo};
pub use crate::protocol::facts::{CargoPublishFlag, CargoPublishPolicy};
use crate::protocol::facts::{
DistributionSurface, Facts, FactsReport, MaturitySignals, Package, RustWorkspace,
WorkspaceMember,
};
const MANIFESTS: &[(&str, Ecosystem)] = &[
("Cargo.toml", Ecosystem::Rust),
("package.json", Ecosystem::Node),
("pyproject.toml", Ecosystem::Python),
("setup.py", Ecosystem::Python),
("go.mod", Ecosystem::Go),
];
const SPIKE_LABELS: &[&str] = &[
"work in progress",
"work-in-progress",
"wip",
"experimental",
"prototype",
"pre-alpha",
"proof of concept",
"proof-of-concept",
"status: private, early",
"not production ready",
"not production-ready",
"early prototype",
"spike",
];
const CI_GLOBS: &[&str] = &[
".github/workflows",
".gitlab-ci.yml",
".circleci",
"azure-pipelines.yml",
".drone.yml",
"Jenkinsfile",
];
const MANIFEST_LIMIT: usize = 200_000;
const README_LIMIT: usize = 4_000;
const DESCRIPTION_CHARS: usize = 120;
#[must_use]
pub fn gather(repo_root: &Path, fs: &dyn Fs, git: &dyn GitRepo) -> Facts {
let is_git = git.is_work_tree();
let has_commits = is_git && git.head_commit().is_ok();
let (ecosystems, packages) = detect_manifests(repo_root, fs);
let rust_workspace = detect_rust_workspace(repo_root, fs);
let distribution_surface = detect_distribution_surface(repo_root, fs);
let (committers_total, committers_recent_year) = if has_commits {
let total = git.shortlog(None).map_or(0, count_lines);
let recent = git.shortlog(Some("1 year ago")).map_or(0, count_lines);
(total, recent)
} else {
(0, 0)
};
let tags = if has_commits {
git.tags().unwrap_or_default()
} else {
Vec::new()
};
let semver_tags: Vec<(u64, u64, u64, bool)> =
tags.iter().filter_map(|t| semver_parse(t)).collect();
let has_semver_tag = !semver_tags.is_empty();
let shipped_release_tags = semver_tags
.iter()
.filter(|&&(major, minor, _, pre)| !pre && (major >= 1 || minor >= 1))
.count();
let ge_1_0_tag = semver_tags
.iter()
.any(|&(major, _, _, pre)| major >= 1 && !pre);
let manifest_ge_1_0 = packages
.iter()
.any(|p| version_ge_1_0(p.version.as_deref()));
let has_ge_1_0_release = ge_1_0_tag || manifest_ge_1_0;
let has_ci = CI_GLOBS.iter().any(|glob| {
let path = repo_root.join(glob);
if !fs.exists(&path) {
return false;
}
if glob.ends_with("workflows") {
fs.read_dir(&path).is_ok_and(|e| !e.is_empty())
} else {
true
}
});
let dependency_bot = if fs.is_file(&repo_root.join(".github/dependabot.yml")) {
Some("dependabot".to_string())
} else if ["renovate.json", ".renovaterc", ".renovaterc.json"]
.iter()
.any(|f| fs.is_file(&repo_root.join(f)))
{
Some("renovate".to_string())
} else {
None
};
let has_issues_dir = fs.is_dir(&repo_root.join("issues"));
let readme_text = ["README.md", "README.rst", "README.txt", "README"]
.iter()
.find_map(|name| {
let text = read_text(fs, &repo_root.join(name), README_LIMIT)?;
(!text.is_empty()).then_some(text)
});
let readme_self_label = readme_text.as_deref().and_then(|text| {
let low = text.to_lowercase();
SPIKE_LABELS
.iter()
.any(|label| low.contains(label))
.then(|| "spike".to_string())
});
let description = detect_description(repo_root, fs, &packages, readme_text.as_deref());
let zerover_release_evidence = dependency_bot.is_some() && shipped_release_tags >= 2;
let release_gate = has_ge_1_0_release || zerover_release_evidence;
let production = committers_recent_year >= 2 && has_ci && release_gate;
let spike =
!has_ci && !has_semver_tag && (committers_total <= 1 || readme_self_label.is_some());
let inferred_maturity = if production {
Maturity::Production
} else if spike {
Maturity::Spike
} else {
Maturity::Mvp
};
Facts {
repo_root: repo_root.display().to_string(),
is_git,
has_commits,
ecosystems,
packages,
committers_total,
committers_recent_year,
tags,
has_semver_tag,
has_ge_1_0_release,
has_ci,
dependency_bot,
has_issues_dir,
readme_self_label,
description,
maturity_signals: MaturitySignals { production, spike },
inferred_maturity,
distribution_surface,
rust_workspace,
}
}
#[must_use]
pub fn gather_report(repo_root: &Path, fs: &dyn Fs, git: &dyn GitRepo) -> FactsReport {
FactsReport {
facts: gather(repo_root, fs, git),
cargo_publish: cargo_publish_evidence(repo_root, fs),
}
}
pub fn detect_distribution_surface(repo_root: &Path, fs: &dyn Fs) -> DistributionSurface {
let mut cargo_dist_evidence = Vec::new();
if fs.is_file(&repo_root.join("dist-workspace.toml")) {
cargo_dist_evidence.push("dist-workspace.toml".to_string());
}
if read_text(fs, &repo_root.join("Cargo.toml"), MANIFEST_LIMIT).is_some_and(|text| {
text.lines()
.any(|line| line.trim() == "[workspace.metadata.dist]")
}) {
cargo_dist_evidence.push("Cargo.toml ([workspace.metadata.dist])".to_string());
}
let workflows = repo_root.join(".github/workflows");
let mut workflow_texts = fs
.read_dir(&workflows)
.unwrap_or_default()
.into_iter()
.filter(|name| {
Path::new(name).extension().is_some_and(|extension| {
extension.eq_ignore_ascii_case("yml") || extension.eq_ignore_ascii_case("yaml")
})
})
.filter_map(|name| {
read_text(fs, &workflows.join(&name), MANIFEST_LIMIT).map(|text| (name, text))
})
.collect::<Vec<_>>();
workflow_texts.sort_by(|a, b| a.0.cmp(&b.0));
let tag_triggered_workflows = workflow_texts
.iter()
.filter(|(_, text)| workflow_pushes_tags(text))
.map(|(name, _)| name.clone())
.collect::<Vec<_>>();
let tag_triggered_cargo_publish_workflows = workflow_texts
.iter()
.filter(|(_, text)| workflow_pushes_tags(text))
.filter(|(name, _)| workflow_reaches_cargo_publish(name, &workflow_texts, &mut Vec::new()))
.map(|(name, _)| name.clone())
.collect::<Vec<_>>();
DistributionSurface {
has_cargo_dist: !cargo_dist_evidence.is_empty(),
cargo_dist_evidence,
tag_triggered_workflows,
tag_triggered_cargo_publish_workflows,
}
}
fn workflow_reaches_cargo_publish(
name: &str,
workflows: &[(String, String)],
visiting: &mut Vec<String>,
) -> bool {
if visiting.iter().any(|seen| seen == name) {
return false;
}
let Some((_, text)) = workflows.iter().find(|(candidate, _)| candidate == name) else {
return false;
};
if workflow_runs_cargo_publish(text) {
return true;
}
visiting.push(name.to_string());
let reaches_publish = local_workflow_calls(text).iter().any(|called| {
workflows
.iter()
.find(|(candidate, _)| candidate == called)
.is_some_and(|(_, callee)| {
workflow_accepts_calls(callee)
&& workflow_reaches_cargo_publish(called, workflows, visiting)
})
});
visiting.pop();
reaches_publish
}
fn workflow_runs_cargo_publish(text: &str) -> bool {
workflow_jobs(text).is_some_and(|jobs| {
jobs.values().any(|job| {
yaml_mapping(job)
.and_then(|job| job.get("steps"))
.and_then(serde_yaml::Value::as_sequence)
.is_some_and(|steps| {
steps.iter().any(|step| {
yaml_mapping(step)
.and_then(|step| step.get("run"))
.and_then(serde_yaml::Value::as_str)
.is_some_and(command_runs_cargo_publish)
})
})
})
})
}
fn command_runs_cargo_publish(command: &str) -> bool {
command.lines().any(|line| {
let uncommented = line.split('#').next().unwrap_or("");
uncommented
.split([';', '\n'])
.flat_map(|part| part.split("&&"))
.flat_map(|part| part.split("||"))
.any(command_segment_runs_cargo_publish)
})
}
fn command_segment_runs_cargo_publish(segment: &str) -> bool {
let tokens = segment.split_ascii_whitespace().collect::<Vec<_>>();
let mut index = tokens
.iter()
.position(|token| *token == "cargo")
.unwrap_or(tokens.len());
if index == tokens.len()
|| tokens[..index]
.iter()
.any(|token| !token.contains('=') || token.starts_with('-'))
{
return false;
}
index += 1;
if tokens
.get(index)
.is_some_and(|token| token.starts_with('+'))
{
index += 1;
}
if tokens.get(index) != Some(&"publish") {
return false;
}
let options = &tokens[index + 1..];
if options.iter().any(|option| {
matches!(*option, "--dry-run" | "--help" | "-h" | "--index")
|| option.starts_with("--index=")
}) {
return false;
}
for (option_index, option) in options.iter().enumerate() {
if let Some(registry) = option.strip_prefix("--registry=") {
if registry != "crates-io" {
return false;
}
} else if *option == "--registry"
&& options.get(option_index + 1).copied() != Some("crates-io")
{
return false;
}
}
true
}
fn workflow_accepts_calls(text: &str) -> bool {
let mut on_indent = None;
for line in text.lines() {
let uncommented = line.split('#').next().unwrap_or("");
if uncommented.trim().is_empty() {
continue;
}
let indent = uncommented.len() - uncommented.trim_start().len();
let key = uncommented.trim();
if key == "on:" {
on_indent = Some(indent);
continue;
}
if let Some(on) = on_indent {
if indent <= on {
return false;
}
if key == "workflow_call:" {
return true;
}
}
}
false
}
fn local_workflow_calls(text: &str) -> Vec<String> {
let Some(jobs) = workflow_jobs(text) else {
return Vec::new();
};
jobs.into_values()
.filter_map(|job| {
job.as_mapping()
.and_then(|job| job.get("uses"))
.and_then(serde_yaml::Value::as_str)
.and_then(|uses| uses.strip_prefix("./.github/workflows/"))
.map(str::to_string)
})
.collect()
}
fn workflow_jobs(text: &str) -> Option<serde_yaml::Mapping> {
let document = serde_yaml::from_str::<serde_yaml::Value>(text).ok()?;
yaml_mapping(&document)
.and_then(|root| root.get("jobs"))
.and_then(serde_yaml::Value::as_mapping)
.cloned()
}
fn yaml_mapping(value: &serde_yaml::Value) -> Option<&serde_yaml::Mapping> {
value.as_mapping()
}
fn workflow_pushes_tags(text: &str) -> bool {
let mut on_indent = None;
let mut push_indent = None;
for line in text.lines() {
let uncommented = line.split('#').next().unwrap_or("");
if uncommented.trim().is_empty() {
continue;
}
let indent = uncommented.len() - uncommented.trim_start().len();
let key = uncommented.trim();
if key == "on:" {
on_indent = Some(indent);
push_indent = None;
continue;
}
if let Some(on) = on_indent {
if indent <= on {
on_indent = None;
push_indent = None;
continue;
}
if key == "push:" {
push_indent = Some(indent);
continue;
}
}
if let Some(push) = push_indent {
if indent <= push {
push_indent = None;
} else if key == "tags:" {
return true;
}
}
}
false
}
#[must_use]
pub fn cargo_publish_evidence(repo_root: &Path, fs: &dyn Fs) -> Vec<CargoPublishFlag> {
let root_path = repo_root.join("Cargo.toml");
let Some(root_text) = read_text(fs, &root_path, MANIFEST_LIMIT) else {
return Vec::new();
};
let ws_pkg = toml_section(&root_text, "workspace.package");
let inherited = ws_pkg
.as_deref()
.map_or(CargoPublishPolicy::Unknown, |block| {
block_publish_policy(block)
});
let mut out = Vec::new();
if toml_section(&root_text, "package").is_some() {
out.push(CargoPublishFlag {
manifest: "Cargo.toml".to_string(),
package: parse_manifest("Cargo.toml", &root_text)
.unwrap_or_default()
.package,
policy: manifest_publish_policy(&root_text, inherited),
});
}
for rel in workspace_member_dirs(repo_root, fs, &root_text) {
let Some(text) = read_text(fs, &repo_root.join(&rel).join("Cargo.toml"), MANIFEST_LIMIT)
else {
continue;
};
let (package, _) = resolve_member_name_version(&text, ws_pkg.as_deref());
out.push(CargoPublishFlag {
manifest: format!("{rel}/Cargo.toml"),
package,
policy: manifest_publish_policy(&text, inherited),
});
}
out
}
fn manifest_publish_policy(text: &str, inherited: CargoPublishPolicy) -> CargoPublishPolicy {
let Some(block) = toml_section(text, "package") else {
return CargoPublishPolicy::Forbidden;
};
if inherits_workspace_publish(&block) {
return inherited;
}
block_publish_policy(&block)
}
fn block_publish_policy(block: &str) -> CargoPublishPolicy {
if let Some(registries) = toml_str_array(block, "publish") {
return if registries.iter().any(|r| r == CRATES_IO_REGISTRY_ALIAS) {
CargoPublishPolicy::Allowed
} else {
CargoPublishPolicy::Forbidden
};
}
match toml_bool_value(block, "publish") {
Some(true) => CargoPublishPolicy::Allowed,
Some(false) => CargoPublishPolicy::Forbidden,
None => {
if block_declares_key(block, "publish") {
CargoPublishPolicy::Unknown
} else {
CargoPublishPolicy::Allowed
}
}
}
}
fn inherits_workspace_publish(block: &str) -> bool {
block.lines().any(|line| {
let line = strip_toml_comment(line).trim();
line.starts_with("publish.workspace")
|| (line.starts_with("publish")
&& line.contains('{')
&& line.contains("workspace")
&& line.contains("true"))
})
}
fn block_declares_key(block: &str, key: &str) -> bool {
block.lines().any(|line| {
let line = strip_toml_comment(line).trim();
line.strip_prefix(key).is_some_and(|rest| {
rest.starts_with(|c: char| c.is_whitespace() || c == '=' || c == '.')
})
})
}
fn detect_manifests(repo_root: &Path, fs: &dyn Fs) -> (Vec<Ecosystem>, Vec<Package>) {
let mut ecosystems: Vec<Ecosystem> = Vec::new();
let mut packages: Vec<Package> = Vec::new();
for &(fname, eco) in MANIFESTS {
let path = repo_root.join(fname);
if !fs.is_file(&path) {
continue;
}
let text = read_text(fs, &path, MANIFEST_LIMIT);
let parsed = text
.as_deref()
.and_then(|t| parse_manifest(fname, t))
.unwrap_or_default();
if !ecosystems.contains(&eco) {
ecosystems.push(eco);
}
if fname == "Cargo.toml" && parsed.package.is_none() {
if let Some(text) = text.as_deref() {
if push_workspace_members(repo_root, fs, text, eco, &mut packages) {
continue;
}
}
}
if parsed.package.is_some() || fname == "Cargo.toml" || fname == "go.mod" {
packages.push(Package {
ecosystem: eco,
manifest: fname.to_string(),
package: parsed.package,
version: parsed.version,
});
}
}
if ecosystems.is_empty() {
ecosystems.push(Ecosystem::Binary);
}
(ecosystems, packages)
}
fn push_workspace_members(
repo_root: &Path,
fs: &dyn Fs,
root_text: &str,
eco: Ecosystem,
packages: &mut Vec<Package>,
) -> bool {
let ws_pkg = toml_section(root_text, "workspace.package");
let dirs = workspace_member_dirs(repo_root, fs, root_text);
let before = packages.len();
for rel in &dirs {
let manifest_path = repo_root.join(rel).join("Cargo.toml");
let Some(member_text) = read_text(fs, &manifest_path, MANIFEST_LIMIT) else {
continue;
};
let (package, version) = resolve_member_name_version(&member_text, ws_pkg.as_deref());
packages.push(Package {
ecosystem: eco,
manifest: format!("{rel}/Cargo.toml"),
package,
version,
});
}
packages.len() > before
}
fn workspace_member_dirs(repo_root: &Path, fs: &dyn Fs, root_text: &str) -> Vec<String> {
let Some(ws_block) = toml_section(root_text, "workspace") else {
return Vec::new();
};
let members = match toml_str_array(&ws_block, "members") {
Some(members) if !members.is_empty() => members,
_ => return Vec::new(),
};
let exclude: Vec<String> = toml_str_array(&ws_block, "exclude")
.unwrap_or_default()
.iter()
.map(|e| e.trim_end_matches('/').to_string())
.collect();
let mut out: Vec<String> = Vec::new();
for member in members {
let member = member.trim_end_matches('/');
if member.is_empty() || member.starts_with('/') || member.split('/').any(|c| c == "..") {
continue;
}
if let Some(prefix) = glob_parent(member) {
let dir = if prefix.is_empty() {
repo_root.to_path_buf()
} else {
repo_root.join(prefix)
};
let mut names = fs.read_dir(&dir).unwrap_or_default();
names.sort();
for name in names {
let rel = if prefix.is_empty() {
name
} else {
format!("{prefix}/{name}")
};
push_member_dir(repo_root, fs, &rel, &exclude, &mut out);
}
continue;
}
if member.contains(['*', '?']) {
continue;
}
push_member_dir(repo_root, fs, member, &exclude, &mut out);
}
out
}
fn push_member_dir(
repo_root: &Path,
fs: &dyn Fs,
rel: &str,
exclude: &[String],
out: &mut Vec<String>,
) {
let rel = rel.trim_end_matches('/');
if exclude.iter().any(|e| e == rel) || out.iter().any(|s| s == rel) {
return;
}
if !fs.is_file(&repo_root.join(rel).join("Cargo.toml")) {
return;
}
out.push(rel.to_string());
}
struct RawMember {
package: String,
version: Option<String>,
publishable: bool,
deps: Vec<(String, Option<String>)>,
pins: Vec<(String, Option<String>)>,
}
fn detect_rust_workspace(repo_root: &Path, fs: &dyn Fs) -> Option<RustWorkspace> {
let root_path = repo_root.join("Cargo.toml");
if !fs.is_file(&root_path) {
return None;
}
let root_text = read_text_full(fs, &root_path)?;
let dirs = workspace_member_dirs(repo_root, fs, &root_text);
if dirs.is_empty() {
return None;
}
let ws_pkg = toml_section(&root_text, "workspace.package");
let mut workspace_pin_reqs: std::collections::BTreeMap<String, Vec<Option<String>>> =
std::collections::BTreeMap::new();
let mut pin_parse_error = None;
match crate::release::bump::cargo_workspace_pin_declarations(&root_text) {
Ok(declarations) => {
for declaration in declarations {
workspace_pin_reqs
.entry(declaration.package)
.or_default()
.push(declaration.requirement);
}
}
Err(error) => pin_parse_error = Some(format!("Cargo.toml: {error}")),
}
let mut raw: Vec<RawMember> = Vec::new();
for rel in &dirs {
let Some(text) = read_text_full(fs, &repo_root.join(rel).join("Cargo.toml")) else {
continue;
};
let (package, version) = resolve_member_name_version(&text, ws_pkg.as_deref());
let Some(package) = package else { continue };
raw.push(RawMember {
package,
version,
publishable: member_publishable_to_crates_io(&text),
deps: member_dependency_edges(&text),
pins: match crate::release::bump::cargo_pin_declarations(&text) {
Ok(declarations) => declarations
.into_iter()
.map(|d| (d.package, d.requirement))
.collect(),
Err(error) => {
pin_parse_error.get_or_insert_with(|| format!("{rel}/Cargo.toml: {error}"));
Vec::new()
}
},
});
}
let publishable_names: std::collections::BTreeSet<&str> = raw
.iter()
.filter(|m| m.publishable)
.map(|m| m.package.as_str())
.collect();
let members: Vec<WorkspaceMember> = raw
.iter()
.filter(|m| m.publishable)
.map(|m| {
let mut dep_reqs: std::collections::BTreeMap<String, String> =
std::collections::BTreeMap::new();
for (name, req) in &m.deps {
if name.as_str() == m.package || !publishable_names.contains(name.as_str()) {
continue;
}
if let Some(req) = req {
dep_reqs.entry(name.clone()).or_insert_with(|| req.clone());
}
}
let mut workspace_deps: Vec<String> = m
.deps
.iter()
.map(|(name, _req)| name.clone())
.filter(|d| d.as_str() != m.package && publishable_names.contains(d.as_str()))
.collect();
workspace_deps.sort();
workspace_deps.dedup();
let mut pin_reqs: std::collections::BTreeMap<String, Vec<Option<String>>> =
std::collections::BTreeMap::new();
for (name, req) in &m.pins {
if name.as_str() != m.package && publishable_names.contains(name.as_str()) {
pin_reqs.entry(name.clone()).or_default().push(req.clone());
}
}
WorkspaceMember {
package: m.package.clone(),
version: m.version.clone(),
workspace_deps,
dep_reqs,
pin_reqs,
}
})
.collect();
if members.is_empty() {
return None;
}
Some(RustWorkspace {
members,
workspace_pin_reqs,
pin_parse_error,
})
}
fn member_publishable_to_crates_io(text: &str) -> bool {
let Some(block) = toml_section(text, "package") else {
return false;
};
if let Some(regs) = toml_str_array(&block, "publish") {
return regs.iter().any(|r| r == CRATES_IO_REGISTRY_ALIAS);
}
!matches!(toml_bool_value(&block, "publish"), Some(false))
}
const CRATES_IO_REGISTRY_ALIAS: &str = "crates-io";
fn toml_bool_value(block: &str, key: &str) -> Option<bool> {
for line in block.lines() {
let rest = line.trim_start();
let Some(rest) = rest.strip_prefix(key) else {
continue;
};
if !rest.starts_with(|c: char| c.is_whitespace() || c == '=') {
continue;
}
let Some(rest) = rest.trim_start().strip_prefix('=') else {
continue;
};
match strip_toml_comment(rest).trim() {
"true" => return Some(true),
"false" => return Some(false),
_ => return None,
}
}
None
}
enum DepTable {
Table,
Sub {
key: String,
package: Option<String>,
local: bool,
version: Option<String>,
},
Other,
}
fn member_dependency_edges(text: &str) -> Vec<(String, Option<String>)> {
let mut state = DepTable::Other;
let mut out: Vec<(String, Option<String>)> = Vec::new();
for line in text.lines() {
let t = strip_toml_comment(line).trim();
if let Some(header) = t.strip_prefix('[').and_then(|h| h.strip_suffix(']')) {
flush_dep_subtable(&mut state, &mut out);
state = classify_dep_table(header.trim());
continue;
}
match &mut state {
DepTable::Table => {
if let Some(edge) = dep_edge_from_line(t) {
out.push(edge);
}
}
DepTable::Sub {
package,
local,
version,
..
} => {
if let Some((k, v)) = split_key_value(t) {
match k {
"package" if package.is_none() => *package = extract_quoted(v),
"path" => *local = true,
"workspace" if v.trim_start().starts_with("true") => *local = true,
"version" if version.is_none() => *version = extract_quoted(v),
_ => {}
}
}
}
DepTable::Other => {}
}
}
flush_dep_subtable(&mut state, &mut out);
out
}
fn classify_dep_table(header: &str) -> DepTable {
if let Some(name) = dep_subtable_name(header) {
return DepTable::Sub {
key: name.trim().trim_matches(['"', '\'']).to_string(),
package: None,
local: false,
version: None,
};
}
if !header.contains("dev-dependencies")
&& (header == "dependencies"
|| header == "build-dependencies"
|| header.ends_with(".dependencies")
|| header.ends_with(".build-dependencies"))
{
return DepTable::Table;
}
DepTable::Other
}
fn dep_subtable_name(header: &str) -> Option<&str> {
if header.contains("dev-dependencies") {
return None;
}
if let Some(name) = header
.strip_prefix("dependencies.")
.or_else(|| header.strip_prefix("build-dependencies."))
{
return Some(name);
}
for infix in [".dependencies.", ".build-dependencies."] {
if let Some(idx) = header.rfind(infix) {
return Some(&header[idx + infix.len()..]);
}
}
None
}
fn dep_edge_from_line(line: &str) -> Option<(String, Option<String>)> {
let (key_full, val) = split_key_value(line)?;
let mut segs = key_full.split('.');
let crate_key = segs.next().unwrap_or("").trim().trim_matches(['"', '\'']);
if crate_key.is_empty() {
return None;
}
match segs.next().map(str::trim) {
Some("path") => Some((crate_key.to_string(), None)),
Some("workspace") if val.trim_start().starts_with("true") => {
Some((crate_key.to_string(), None))
}
Some(_) => None,
None => {
if !val.starts_with('{') || !inline_table_is_local(val) {
return None;
}
let name = inline_table_package(val).unwrap_or_else(|| crate_key.to_string());
Some((name, inline_table_version(val)))
}
}
}
fn flush_dep_subtable(state: &mut DepTable, out: &mut Vec<(String, Option<String>)>) {
if let DepTable::Sub {
key,
package,
local: true,
version,
} = state
{
out.push((
package.clone().unwrap_or_else(|| key.clone()),
version.clone(),
));
}
*state = DepTable::Other;
}
fn split_key_value(line: &str) -> Option<(&str, &str)> {
let eq = line.find('=')?;
let key = line[..eq].trim().trim_matches(['"', '\'']);
if key.is_empty() {
return None;
}
Some((key, line[eq + 1..].trim()))
}
fn inline_table_is_local(inline: &str) -> bool {
contains_toml_key(inline, "path") || toml_key_is_true(inline, "workspace")
}
fn contains_toml_key(s: &str, key: &str) -> bool {
scan_toml_key(s, key, |_after| true)
}
fn toml_key_is_true(s: &str, key: &str) -> bool {
scan_toml_key(s, key, |after| after.trim_start().starts_with("true"))
}
fn scan_toml_key(s: &str, key: &str, accept: impl Fn(&str) -> bool) -> bool {
let mut rest = s;
while let Some(pos) = rest.find(key) {
let prev_is_ident = rest[..pos]
.chars()
.next_back()
.is_some_and(|c| c.is_alphanumeric() || c == '_' || c == '-');
let after = rest[pos + key.len()..].trim_start();
if !prev_is_ident {
if let Some(value) = after.strip_prefix('=') {
if accept(value) {
return true;
}
}
}
rest = &rest[pos + key.len()..];
}
false
}
fn inline_table_package(inline: &str) -> Option<String> {
let mut rest = inline;
while let Some(pos) = rest.find("package") {
let prev_is_ident = rest[..pos]
.chars()
.next_back()
.is_some_and(|c| c.is_alphanumeric() || c == '_' || c == '-');
let after = rest[pos + "package".len()..].trim_start();
if !prev_is_ident {
if let Some(value) = after.strip_prefix('=') {
return extract_quoted(value.trim_start());
}
}
rest = &rest[pos + "package".len()..];
}
None
}
fn inline_table_version(inline: &str) -> Option<String> {
let mut rest = inline;
while let Some(pos) = rest.find("version") {
let prev_is_ident = rest[..pos]
.chars()
.next_back()
.is_some_and(|c| c.is_alphanumeric() || c == '_' || c == '-');
let after = rest[pos + "version".len()..].trim_start();
if !prev_is_ident {
if let Some(value) = after.strip_prefix('=') {
return extract_quoted(value.trim_start());
}
}
rest = &rest[pos + "version".len()..];
}
None
}
fn glob_parent(member: &str) -> Option<&str> {
if member == "*" {
return Some("");
}
member
.strip_suffix("/*")
.filter(|prefix| !prefix.contains(['*', '?']))
}
fn resolve_member_name_version(
member_text: &str,
ws_pkg: Option<&str>,
) -> (Option<String>, Option<String>) {
let parsed = parse_cargo(member_text).unwrap_or_default();
let version = parsed.version.or_else(|| {
let inherits = toml_section(member_text, "package")
.is_some_and(|block| field_inherits_workspace(&block, "version"));
if inherits {
ws_pkg.and_then(|block| toml_str_value(block, "version", false))
} else {
None
}
});
(parsed.package, version)
}
fn detect_description(
repo_root: &Path,
fs: &dyn Fs,
packages: &[Package],
readme_text: Option<&str>,
) -> Option<String> {
let manifest_desc = packages.iter().find_map(|p| {
let text = read_text(fs, &repo_root.join(&p.manifest), MANIFEST_LIMIT)?;
let desc = parse_manifest(&p.manifest, &text)?.description?;
(!desc.is_empty()).then_some(desc)
});
if let Some(desc) = manifest_desc {
return Some(truncate_chars(desc.trim(), DESCRIPTION_CHARS));
}
readme_text?.lines().find_map(|line| {
let s = line.trim();
let is_prose =
!s.is_empty() && !s.starts_with('#') && !s.starts_with('!') && !s.starts_with('>');
is_prose.then(|| truncate_chars(s, DESCRIPTION_CHARS))
})
}
#[derive(Debug, Default)]
struct ParsedManifest {
package: Option<String>,
version: Option<String>,
description: Option<String>,
}
fn parse_manifest(fname: &str, text: &str) -> Option<ParsedManifest> {
let base = Path::new(fname)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(fname);
match base {
"Cargo.toml" => parse_cargo(text),
"package.json" => parse_package_json(text),
"pyproject.toml" => Some(parse_pyproject(text)),
"go.mod" => Some(parse_gomod(text)),
_ => None, }
}
fn parse_cargo(text: &str) -> Option<ParsedManifest> {
let block = toml_section(text, "package")?;
Some(ParsedManifest {
package: toml_str_value(&block, "name", false),
version: toml_str_value(&block, "version", false),
description: toml_str_value(&block, "description", true),
})
}
fn parse_package_json(text: &str) -> Option<ParsedManifest> {
let value: serde_json::Value = serde_json::from_str(text).ok()?;
let field = |key: &str| {
value
.get(key)
.and_then(serde_json::Value::as_str)
.map(str::to_string)
};
Some(ParsedManifest {
package: field("name"),
version: field("version"),
description: field("description"),
})
}
fn parse_pyproject(text: &str) -> ParsedManifest {
let mut parsed = ParsedManifest::default();
if let Some(block) = toml_section(text, "project") {
parsed.package = toml_str_value(&block, "name", false);
parsed.version = toml_str_value(&block, "version", false);
parsed.description = toml_str_value(&block, "description", true);
}
if parsed.package.is_none() {
if let Some(block) = toml_section(text, "tool.poetry") {
parsed.package = toml_str_value(&block, "name", false);
parsed.version = toml_str_value(&block, "version", false);
parsed.description = toml_str_value(&block, "description", true);
}
}
parsed
}
fn parse_gomod(text: &str) -> ParsedManifest {
let module = text.lines().find_map(|line| {
line.strip_prefix("module")
.filter(|rest| rest.starts_with(char::is_whitespace))
.and_then(|rest| rest.split_whitespace().next())
.map(str::to_string)
});
ParsedManifest {
package: module,
version: None,
description: None,
}
}
fn toml_section(text: &str, header: &str) -> Option<String> {
let needle = format!("[{header}]");
let mut in_section = false;
let mut out = String::new();
for line in text.lines() {
if in_section {
if line.starts_with('[') {
break;
}
out.push_str(line);
out.push('\n');
} else if line.starts_with(&needle) {
in_section = true;
}
}
in_section.then_some(out)
}
fn toml_str_value(block: &str, key: &str, allow_empty: bool) -> Option<String> {
for line in block.lines() {
let rest = line.trim_start();
let Some(rest) = rest.strip_prefix(key) else {
continue;
};
if !rest.starts_with(|c: char| c.is_whitespace() || c == '=') {
continue;
}
let Some(rest) = rest.trim_start().strip_prefix('=') else {
continue;
};
let Some(value) = extract_quoted(rest.trim_start()) else {
continue;
};
if value.is_empty() && !allow_empty {
return None;
}
return Some(value);
}
None
}
fn toml_str_array(block: &str, key: &str) -> Option<Vec<String>> {
let mut acc = String::new();
let mut collecting = false;
for line in block.lines() {
let line = strip_toml_comment(line);
if collecting {
acc.push_str(line);
acc.push('\n');
if line.contains(']') {
break;
}
continue;
}
let rest = line.trim_start();
let Some(rest) = rest.strip_prefix(key) else {
continue;
};
if !rest.starts_with(|c: char| c.is_whitespace() || c == '=') {
continue;
}
let Some(rest) = rest.trim_start().strip_prefix('=') else {
continue;
};
acc.push_str(rest);
acc.push('\n');
collecting = true;
if rest.contains(']') {
break;
}
}
if !collecting {
return None;
}
let start = acc.find('[')?;
let end = acc[start..].find(']')? + start;
let mut inner = &acc[start + 1..end];
let mut out = Vec::new();
while let Some(pos) = inner.find(['"', '\'']) {
let quote = inner.as_bytes()[pos] as char;
let after = &inner[pos + 1..];
let Some(close) = after.find(quote) else {
break;
};
out.push(after[..close].to_string());
inner = &after[close + 1..];
}
Some(out)
}
fn field_inherits_workspace(block: &str, key: &str) -> bool {
let dotted = format!("{key}.workspace");
for line in block.lines() {
let t = strip_toml_comment(line).trim_start();
if let Some(rest) = t.strip_prefix(&dotted) {
let rest = rest.trim_start();
if let Some(rest) = rest.strip_prefix('=') {
if is_true_literal(rest.trim_start()) {
return true;
}
}
continue;
}
if let Some(rest) = t.strip_prefix(key) {
if !rest.starts_with(|c: char| c.is_whitespace() || c == '=') {
continue;
}
let Some(rest) = rest.trim_start().strip_prefix('=') else {
continue;
};
let v = rest.trim_start();
if v.starts_with('{') && inline_table_has_workspace_true(v) {
return true;
}
}
}
false
}
fn is_true_literal(s: &str) -> bool {
match s.strip_prefix("true") {
Some(rest) => {
let rest = rest.trim_start();
rest.is_empty() || rest.starts_with(['#', '}', ','])
}
None => false,
}
}
fn inline_table_has_workspace_true(inline: &str) -> bool {
let compact: String = inline.chars().filter(|c| !c.is_whitespace()).collect();
compact.trim_start_matches('{').split(',').any(|entry| {
entry
.strip_prefix("workspace=true")
.is_some_and(|r| r.is_empty() || r == "}")
})
}
fn strip_toml_comment(line: &str) -> &str {
let mut quote: Option<u8> = None;
for (i, &b) in line.as_bytes().iter().enumerate() {
match quote {
Some(q) => {
if b == q {
quote = None;
}
}
None => match b {
b'"' | b'\'' => quote = Some(b),
b'#' => return &line[..i],
_ => {}
},
}
}
line
}
fn extract_quoted(s: &str) -> Option<String> {
let quote = s.chars().next().filter(|&c| c == '"' || c == '\'')?;
let s = &s[1..];
let end = s.find(quote)?;
Some(s[..end].to_string())
}
fn semver_parse(tag: &str) -> Option<(u64, u64, u64, bool)> {
parse_semver_core(strip_pkg_prefix(tag))
}
fn strip_pkg_prefix(tag: &str) -> &str {
let bytes = tag.as_bytes();
for i in (0..bytes.len()).rev() {
if matches!(bytes[i], b'@' | b'/' | b'-') {
let rest = &tag[i + 1..];
if starts_with_version(rest) {
return rest;
}
}
}
tag
}
fn starts_with_version(s: &str) -> bool {
let mut rest = s.strip_prefix('v').unwrap_or(s);
for i in 0..3 {
let digits = rest.bytes().take_while(u8::is_ascii_digit).count();
if digits == 0 {
return false;
}
rest = &rest[digits..];
if i < 2 {
match rest.strip_prefix('.') {
Some(r) => rest = r,
None => return false,
}
}
}
true
}
fn parse_semver_core(core: &str) -> Option<(u64, u64, u64, bool)> {
let mut rest = core.strip_prefix('v').unwrap_or(core);
let mut nums = [0u64; 3];
for (i, slot) in nums.iter_mut().enumerate() {
let digits = rest.bytes().take_while(u8::is_ascii_digit).count();
if digits == 0 {
return None;
}
*slot = rest[..digits].parse().ok()?;
rest = &rest[digits..];
if i < 2 {
rest = rest.strip_prefix('.')?;
}
}
let pre = match rest.chars().next() {
None | Some('+') => false,
Some('-') => true,
Some(_) => return None, };
Some((nums[0], nums[1], nums[2], pre))
}
fn version_ge_1_0(version: Option<&str>) -> bool {
let Some(v) = version else {
return false;
};
let s = v.strip_prefix('v').unwrap_or(v);
let digits = s.bytes().take_while(u8::is_ascii_digit).count();
if digits == 0 || !s[digits..].starts_with('.') {
return false;
}
s[..digits].parse::<u64>().is_ok_and(|n| n >= 1)
}
fn read_text(fs: &dyn Fs, path: &Path, limit: usize) -> Option<String> {
let bytes = fs.read(path).ok()?;
Some(
String::from_utf8_lossy(&bytes)
.chars()
.take(limit)
.collect(),
)
}
fn read_text_full(fs: &dyn Fs, path: &Path) -> Option<String> {
Some(String::from_utf8_lossy(&fs.read(path).ok()?).into_owned())
}
fn count_lines(text: String) -> usize {
text.lines().filter(|l| !l.trim().is_empty()).count()
}
fn truncate_chars(s: &str, n: usize) -> String {
s.chars().take(n).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
#[derive(Default)]
struct FakeFs {
files: HashMap<PathBuf, Vec<u8>>,
dirs: HashSet<PathBuf>,
}
impl FakeFs {
fn file(mut self, path: &str, contents: &str) -> Self {
let p = PathBuf::from(path);
let mut cur = p.parent();
while let Some(dir) = cur {
if dir.as_os_str().is_empty() {
break;
}
self.dirs.insert(dir.to_path_buf());
cur = dir.parent();
}
self.files.insert(p, contents.as_bytes().to_vec());
self
}
fn dir(mut self, path: &str) -> Self {
self.dirs.insert(PathBuf::from(path));
self
}
}
impl Fs for FakeFs {
fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
self.files
.get(path)
.cloned()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))
}
fn exists(&self, path: &Path) -> bool {
self.files.contains_key(path) || self.dirs.contains(path)
}
fn is_dir(&self, path: &Path) -> bool {
self.dirs.contains(path)
}
fn is_file(&self, path: &Path) -> bool {
self.files.contains_key(path)
}
fn read_dir(&self, dir: &Path) -> std::io::Result<Vec<String>> {
if !self.dirs.contains(dir) {
return Err(std::io::Error::from(std::io::ErrorKind::NotFound));
}
let mut names: Vec<String> = self
.files
.keys()
.chain(self.dirs.iter())
.filter(|p| p.parent() == Some(dir))
.filter_map(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.collect();
names.sort();
Ok(names)
}
}
#[derive(Default)]
struct FakeGit {
work_tree: bool,
head: Option<String>,
shortlog_all: String,
shortlog_recent: String,
tags: Vec<String>,
}
impl GitRepo for FakeGit {
fn head_commit(&self) -> std::io::Result<String> {
self.head
.clone()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))
}
fn is_work_tree(&self) -> bool {
self.work_tree
}
fn shortlog(&self, since: Option<&str>) -> std::io::Result<String> {
if !self.work_tree {
return Err(std::io::Error::from(std::io::ErrorKind::Other));
}
Ok(if since.is_some() {
self.shortlog_recent.clone()
} else {
self.shortlog_all.clone()
})
}
fn tags(&self) -> std::io::Result<Vec<String>> {
if self.work_tree {
Ok(self.tags.clone())
} else {
Err(std::io::Error::from(std::io::ErrorKind::Other))
}
}
fn git_common_dir(&self) -> std::io::Result<PathBuf> {
Ok(PathBuf::from("/repo/.git"))
}
}
fn git_with(total: usize, recent: usize, tags: &[&str]) -> FakeGit {
let lines = |n: usize| {
(0..n)
.map(|i| format!(" 3\tDev {i} <dev{i}@example.com>"))
.collect::<Vec<_>>()
.join("\n")
};
FakeGit {
work_tree: true,
head: Some("deadbeef".to_string()),
shortlog_all: lines(total),
shortlog_recent: lines(recent),
tags: tags.iter().map(|t| (*t).to_string()).collect(),
}
}
fn repo() -> &'static Path {
Path::new("/repo")
}
#[test]
fn empty_repo_is_spike_binary() {
let facts = gather(repo(), &FakeFs::default(), &FakeGit::default());
assert!(!facts.is_git);
assert!(!facts.has_commits);
assert_eq!(facts.ecosystems, vec![Ecosystem::Binary]);
assert!(facts.packages.is_empty());
assert_eq!(facts.committers_total, 0);
assert_eq!(facts.committers_recent_year, 0);
assert!(facts.tags.is_empty());
assert!(!facts.has_ci);
assert_eq!(facts.dependency_bot, None);
assert_eq!(facts.description, None);
assert!(facts.maturity_signals.spike);
assert_eq!(facts.inferred_maturity, Maturity::Spike);
}
#[test]
fn unborn_repo_has_no_commits() {
let git = FakeGit {
work_tree: true,
head: None,
..FakeGit::default()
};
let facts = gather(repo(), &FakeFs::default(), &git);
assert!(facts.is_git);
assert!(!facts.has_commits);
assert_eq!(facts.committers_total, 0);
assert!(facts.tags.is_empty());
}
#[test]
fn cargo_package_name_version_description() {
let cargo = "[package]\nname = \"rg\"\nversion = \"0.3.0\"\n\
description = \"a fast grep\"\n\n[dependencies]\nserde = \"1\"\n";
let fs = FakeFs::default().file("/repo/Cargo.toml", cargo);
let facts = gather(repo(), &fs, &FakeGit::default());
assert_eq!(facts.ecosystems, vec![Ecosystem::Rust]);
assert_eq!(facts.packages.len(), 1);
let p = &facts.packages[0];
assert_eq!(p.ecosystem, Ecosystem::Rust);
assert_eq!(p.manifest, "Cargo.toml");
assert_eq!(p.package.as_deref(), Some("rg"));
assert_eq!(p.version.as_deref(), Some("0.3.0"));
assert_eq!(facts.description.as_deref(), Some("a fast grep"));
}
#[test]
fn cargo_virtual_workspace_with_no_resolvable_member_keeps_null_entry() {
let fs = FakeFs::default().file("/repo/Cargo.toml", "[workspace]\nmembers = [\"a\"]\n");
let facts = gather(repo(), &fs, &FakeGit::default());
assert_eq!(facts.ecosystems, vec![Ecosystem::Rust]);
assert_eq!(facts.packages.len(), 1);
assert_eq!(facts.packages[0].manifest, "Cargo.toml");
assert_eq!(facts.packages[0].package, None);
assert_eq!(facts.packages[0].version, None);
}
#[test]
fn cargo_virtual_workspace_enumerates_members() {
let root = "[workspace]\nresolver = \"2\"\n\
members = [\"crates/core\", \"crates/cli\"]\n\n\
[workspace.package]\nversion = \"0.1.0\"\nedition = \"2021\"\n";
let core = "[package]\nname = \"acme-core\"\nversion.workspace = true\n\
edition.workspace = true\ndescription = \"the core lib\"\n";
let cli = "[package]\nname = \"acme-cli\"\nversion = \"2.3.4\"\n\
description = \"the cli\"\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/crates/core/Cargo.toml", core)
.file("/repo/crates/cli/Cargo.toml", cli);
let facts = gather(repo(), &fs, &FakeGit::default());
assert_eq!(facts.ecosystems, vec![Ecosystem::Rust]);
assert_eq!(facts.packages.len(), 2);
let core_pkg = &facts.packages[0];
assert_eq!(core_pkg.ecosystem, Ecosystem::Rust);
assert_eq!(core_pkg.manifest, "crates/core/Cargo.toml");
assert_eq!(core_pkg.package.as_deref(), Some("acme-core"));
assert_eq!(core_pkg.version.as_deref(), Some("0.1.0"));
let cli_pkg = &facts.packages[1];
assert_eq!(cli_pkg.manifest, "crates/cli/Cargo.toml");
assert_eq!(cli_pkg.package.as_deref(), Some("acme-cli"));
assert_eq!(cli_pkg.version.as_deref(), Some("2.3.4"));
assert_eq!(facts.description.as_deref(), Some("the core lib"));
}
#[test]
fn rust_workspace_graph_captures_lib_bin_edge() {
let root = "[workspace]\nmembers = [\"crates/core\", \"crates/cli\"]\n\n\
[workspace.package]\nversion = \"0.1.6\"\n";
let core = "[package]\nname = \"octl-core\"\nversion.workspace = true\n\
publish = true\n";
let cli = "[package]\nname = \"orchestratectl\"\nversion.workspace = true\n\
publish = true\n\n[dependencies]\n\
octl-core = { path = \"../core\", version = \"=0.1.6\" }\n\
serde = \"1\"\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/crates/core/Cargo.toml", core)
.file("/repo/crates/cli/Cargo.toml", cli);
let ws = detect_rust_workspace(repo(), &fs).expect("a members-bearing workspace");
let names: Vec<_> = ws.members.iter().map(|m| m.package.as_str()).collect();
assert_eq!(names, vec!["octl-core", "orchestratectl"]);
assert!(ws.members[0].workspace_deps.is_empty());
assert_eq!(ws.members[1].workspace_deps, vec!["octl-core".to_string()]);
assert_eq!(ws.members[0].version.as_deref(), Some("0.1.6"));
}
#[test]
fn rust_workspace_graph_drops_unpublishable_members() {
let root = "[workspace]\nmembers = [\"a\", \"b\", \"c\"]\n";
let a = "[package]\nname = \"a\"\nversion = \"1.0.0\"\n"; let b = "[package]\nname = \"b\"\nversion = \"1.0.0\"\npublish = false\n";
let c = "[package]\nname = \"c\"\nversion = \"1.0.0\"\n\
publish = [\"my-registry\"]\n\n[dependencies]\n\
b = { path = \"../b\" }\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/a/Cargo.toml", a)
.file("/repo/b/Cargo.toml", b)
.file("/repo/c/Cargo.toml", c);
let ws = detect_rust_workspace(repo(), &fs).expect("at least `a` is publishable");
let names: Vec<_> = ws.members.iter().map(|m| m.package.as_str()).collect();
assert_eq!(names, vec!["a"], "only the publishable member survives");
}
#[test]
fn rust_workspace_graph_honors_crates_io_allow_list() {
let root = "[workspace]\nmembers = [\"a\"]\n";
let a = "[package]\nname = \"a\"\nversion = \"1.0.0\"\n\
publish = [\"crates-io\"]\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/a/Cargo.toml", a);
let ws = detect_rust_workspace(repo(), &fs).expect("crates-io allow-listed");
assert_eq!(ws.members.len(), 1);
}
#[test]
fn cargo_publish_evidence_reads_the_root_package() {
let fs = FakeFs::default().file(
"/repo/Cargo.toml",
"[package]\nname = \"solo\"\nversion = \"0.1.0\"\npublish = false\n",
);
assert_eq!(
cargo_publish_evidence(repo(), &fs),
vec![CargoPublishFlag {
manifest: "Cargo.toml".to_string(),
package: Some("solo".to_string()),
policy: CargoPublishPolicy::Forbidden,
}]
);
}
#[test]
fn cargo_publish_evidence_reads_every_workspace_member() {
let fs = FakeFs::default()
.file(
"/repo/Cargo.toml",
"[workspace]\nmembers = [\"a\", \"b\"]\n",
)
.file(
"/repo/a/Cargo.toml",
"[package]\nname = \"a\"\nversion = \"1.0.0\"\n",
)
.file(
"/repo/b/Cargo.toml",
"[package]\nname = \"b\"\nversion = \"1.0.0\"\npublish = false\n",
);
assert_eq!(
policies(&fs),
vec![
("a/Cargo.toml".to_string(), CargoPublishPolicy::Allowed),
("b/Cargo.toml".to_string(), CargoPublishPolicy::Forbidden),
]
);
}
#[test]
fn cargo_publish_evidence_reads_a_hybrid_root_and_its_members() {
let fs = FakeFs::default()
.file(
"/repo/Cargo.toml",
"[package]\nname = \"root\"\nversion = \"1.0.0\"\n\n\
[workspace]\nmembers = [\"crates/private\"]\n",
)
.file(
"/repo/crates/private/Cargo.toml",
"[package]\nname = \"private\"\nversion = \"1.0.0\"\npublish = false\n",
);
assert_eq!(
policies(&fs),
vec![
("Cargo.toml".to_string(), CargoPublishPolicy::Allowed),
(
"crates/private/Cargo.toml".to_string(),
CargoPublishPolicy::Forbidden
),
]
);
}
#[test]
fn cargo_publish_evidence_covers_every_publish_value_shape() {
let cases = [
("", CargoPublishPolicy::Allowed), ("publish = true\n", CargoPublishPolicy::Allowed),
("publish = false\n", CargoPublishPolicy::Forbidden),
("publish=false\n", CargoPublishPolicy::Forbidden), ("publish = false # private\n", CargoPublishPolicy::Forbidden), ("publish = []\n", CargoPublishPolicy::Forbidden), ("publish = [\"crates-io\"]\n", CargoPublishPolicy::Allowed),
(
"publish = [\"my-registry\"]\n",
CargoPublishPolicy::Forbidden,
),
(
"publish = { workspace = false }\n",
CargoPublishPolicy::Unknown,
),
];
for (line, expected) in cases {
let fs = FakeFs::default().file(
"/repo/Cargo.toml",
&format!("[package]\nname = \"solo\"\nversion = \"0.1.0\"\n{line}"),
);
assert_eq!(
cargo_publish_evidence(repo(), &fs)[0].policy,
expected,
"publish line {line:?}"
);
}
}
#[test]
fn cargo_publish_evidence_ignores_a_publish_key_outside_the_package_table() {
let fs = FakeFs::default().file(
"/repo/Cargo.toml",
"[package]\nname = \"solo\"\nversion = \"0.1.0\"\n\n\
[package.metadata.release]\npublish = false\n\n\
[dependencies]\npublish = \"1.0\"\n",
);
assert_eq!(
cargo_publish_evidence(repo(), &fs)[0].policy,
CargoPublishPolicy::Allowed
);
}
#[test]
fn cargo_publish_evidence_resolves_workspace_inheritance() {
let member = "[package]\nname = \"a\"\nversion = \"1.0.0\"\npublish.workspace = true\n";
for (ws_publish, expected) in [
("publish = false\n", CargoPublishPolicy::Forbidden),
("publish = true\n", CargoPublishPolicy::Allowed),
("", CargoPublishPolicy::Allowed), ] {
let fs = FakeFs::default()
.file(
"/repo/Cargo.toml",
&format!(
"[workspace]\nmembers = [\"a\"]\n\n[workspace.package]\nversion = \"1.0.0\"\n{ws_publish}"
),
)
.file("/repo/a/Cargo.toml", member);
assert_eq!(
cargo_publish_evidence(repo(), &fs)[0].policy,
expected,
"[workspace.package] {ws_publish:?}"
);
}
let fs = FakeFs::default()
.file("/repo/Cargo.toml", "[workspace]\nmembers = [\"a\"]\n")
.file("/repo/a/Cargo.toml", member);
assert_eq!(
cargo_publish_evidence(repo(), &fs)[0].policy,
CargoPublishPolicy::Unknown
);
}
#[test]
fn facts_report_exposes_the_normalizers_exact_tri_state_evidence() {
let fs = FakeFs::default()
.file(
"/repo/Cargo.toml",
"[workspace]\nmembers = [\"allowed\", \"inherited\", \"unknown\"]\n\
\n[workspace.package]\npublish = false\n",
)
.file(
"/repo/allowed/Cargo.toml",
"[package]\nname = \"allowed\"\npublish = true\n",
)
.file(
"/repo/inherited/Cargo.toml",
"[package]\nname = \"inherited\"\npublish.workspace = true\n",
)
.file(
"/repo/unknown/Cargo.toml",
"[package]\nname = \"unknown\"\npublish = { workspace = false }\n",
);
let report = gather_report(repo(), &fs, &FakeGit::default());
assert_eq!(report.cargo_publish, cargo_publish_evidence(repo(), &fs));
assert_eq!(
report
.cargo_publish
.iter()
.map(|e| e.policy)
.collect::<Vec<_>>(),
vec![
CargoPublishPolicy::Allowed,
CargoPublishPolicy::Forbidden,
CargoPublishPolicy::Unknown,
]
);
let json = serde_json::to_value(&report).expect("facts report serializes");
assert_eq!(json["cargo_publish"][0]["manifest"], "allowed/Cargo.toml");
assert_eq!(json["cargo_publish"][0]["policy"], "allowed");
assert_eq!(json["cargo_publish"][1]["policy"], "forbidden");
assert_eq!(json["cargo_publish"][2]["policy"], "unknown");
}
#[test]
fn cargo_publish_evidence_is_empty_without_a_manifest() {
assert!(cargo_publish_evidence(repo(), &FakeFs::default()).is_empty());
}
fn policies(fs: &FakeFs) -> Vec<(String, CargoPublishPolicy)> {
cargo_publish_evidence(repo(), fs)
.into_iter()
.map(|m| (m.manifest, m.policy))
.collect()
}
#[test]
fn rust_workspace_graph_none_for_single_crate_repo() {
let cargo = "[package]\nname = \"solo\"\nversion = \"0.1.0\"\n";
let fs = FakeFs::default().file("/repo/Cargo.toml", cargo);
assert!(detect_rust_workspace(repo(), &fs).is_none());
}
#[test]
fn rust_workspace_graph_dep_rename_via_inline_package_key() {
let root = "[workspace]\nmembers = [\"core\", \"cli\"]\n";
let core = "[package]\nname = \"real-core\"\nversion = \"1.0.0\"\n";
let cli = "[package]\nname = \"cli\"\nversion = \"1.0.0\"\n\n[dependencies]\n\
alias = { package = \"real-core\", path = \"../core\" }\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/core/Cargo.toml", core)
.file("/repo/cli/Cargo.toml", cli);
let ws = detect_rust_workspace(repo(), &fs).expect("workspace");
let cli = ws
.members
.iter()
.find(|m| m.package == "cli")
.expect("cli member");
assert_eq!(cli.workspace_deps, vec!["real-core".to_string()]);
}
#[test]
fn rust_workspace_graph_ignores_registry_dep_sharing_a_member_name() {
let root = "[workspace]\nmembers = [\"shared\", \"cli\"]\n";
let shared = "[package]\nname = \"shared\"\nversion = \"1.0.0\"\n\n[dependencies]\n\
cli = { path = \"../cli\" }\n"; let cli = "[package]\nname = \"cli\"\nversion = \"1.0.0\"\n\n[dependencies]\n\
shared = \"1\"\n"; let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/shared/Cargo.toml", shared)
.file("/repo/cli/Cargo.toml", cli);
let ws = detect_rust_workspace(repo(), &fs).expect("workspace");
let cli = ws.members.iter().find(|m| m.package == "cli").unwrap();
let shared = ws.members.iter().find(|m| m.package == "shared").unwrap();
assert!(
cli.workspace_deps.is_empty(),
"a registry dep sharing a member name is not an edge"
);
assert_eq!(
shared.workspace_deps,
vec!["cli".to_string()],
"the real path edge is still detected"
);
}
#[test]
fn rust_workspace_graph_ignores_registry_inline_table_without_path() {
let root = "[workspace]\nmembers = [\"a\", \"b\"]\n";
let a = "[package]\nname = \"a\"\nversion = \"1.0.0\"\n";
let b = "[package]\nname = \"b\"\nversion = \"1.0.0\"\n\n[dependencies]\n\
a = { version = \"1\", features = [\"x\"] }\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/a/Cargo.toml", a)
.file("/repo/b/Cargo.toml", b);
let ws = detect_rust_workspace(repo(), &fs).expect("workspace");
let b = ws.members.iter().find(|m| m.package == "b").unwrap();
assert!(
b.workspace_deps.is_empty(),
"an inline registry dep (no path/workspace) is not an edge"
);
}
#[test]
fn rust_workspace_graph_records_the_lockstep_pin_requirement() {
let root = "[workspace]\nmembers = [\"core\", \"cli\"]\n";
let core = "[package]\nname = \"octl-core\"\nversion = \"0.4.0\"\n";
let cli = "[package]\nname = \"cli\"\nversion = \"0.4.0\"\n\n[dependencies]\n\
octl-core = { path = \"../core\", version = \"=0.4.0\" }\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/core/Cargo.toml", core)
.file("/repo/cli/Cargo.toml", cli);
let ws = detect_rust_workspace(repo(), &fs).expect("workspace");
let cli = ws.members.iter().find(|m| m.package == "cli").unwrap();
assert_eq!(cli.workspace_deps, vec!["octl-core".to_string()]);
assert_eq!(
cli.dep_reqs.get("octl-core").map(String::as_str),
Some("=0.4.0")
);
assert_eq!(
cli.pin_reqs.get("octl-core"),
Some(&vec![Some("=0.4.0".to_string())])
);
}
#[test]
fn rust_workspace_records_inherited_root_exact_pin() {
let root = "[workspace]\nmembers = [\"core\", \"cli\"]\n\
[workspace.package]\nversion = \"0.4.0\"\n\
[workspace.dependencies]\ncore = {\npath = \"core\",\nversion = \"=0.4.0\"\n}\n";
let core = "[package]\nname = \"core\"\nversion.workspace = true\n";
let cli = "[package]\nname = \"cli\"\nversion.workspace = true\n\n[dependencies]\ncore.workspace = true\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/core/Cargo.toml", core)
.file("/repo/cli/Cargo.toml", cli);
let ws = detect_rust_workspace(repo(), &fs).expect("workspace");
assert_eq!(
ws.workspace_pin_reqs.get("core"),
Some(&vec![Some("=0.4.0".to_string())])
);
let cli = ws
.members
.iter()
.find(|member| member.package == "cli")
.unwrap();
assert_eq!(cli.workspace_deps, vec!["core".to_string()]);
}
#[test]
fn rust_workspace_pin_requirements_preserve_all_dependency_tables() {
let root = "[workspace]\nmembers = [\"core\", \"cli\"]\n";
let core = "[package]\nname = \"core\"\nversion = \"0.4.0\"\n";
let cli = "[package]\nname = \"cli\"\nversion = \"0.4.0\"\n\n[dependencies]\ncore = { path = \"../core\", version = \"=0.4.0\" }\n[dev-dependencies]\ncore = { path = \"../core\", version = \"=0.4.0\" }\n[build-dependencies]\ncore = { path = \"../core\", version = \"=0.4.0\" }\n[target.'cfg(unix)'.dependencies]\ncore = { path = \"../core\", version = \"=0.4.0\" }\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/core/Cargo.toml", core)
.file("/repo/cli/Cargo.toml", cli);
let ws = detect_rust_workspace(repo(), &fs).expect("workspace");
let cli = ws.members.iter().find(|m| m.package == "cli").unwrap();
assert_eq!(cli.workspace_deps, vec!["core".to_string()]);
assert_eq!(
cli.pin_reqs.get("core"),
Some(&vec![Some("=0.4.0".to_string()); 4])
);
}
#[test]
fn rust_workspace_pin_discovery_reads_the_complete_manifest() {
let root = "[workspace]\nmembers = [\"core\", \"cli\"]\n";
let core = "[package]\nname = \"core\"\nversion = \"0.4.0\"\n";
let mut cli = "[package]\nname = \"cli\"\nversion = \"0.4.0\"\n\n[dependencies]\ncore = { path = \"../core\", version = \"=0.4.0\" }\n".to_string();
cli.push('#');
cli.push_str(&"x".repeat(MANIFEST_LIMIT));
cli.push_str("\n[dev-dependencies]\ncore = { path = \"../core\", version = \"^0.4\" }\n");
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/core/Cargo.toml", core)
.file("/repo/cli/Cargo.toml", &cli);
let ws = detect_rust_workspace(repo(), &fs).expect("workspace");
let cli = ws.members.iter().find(|m| m.package == "cli").unwrap();
assert_eq!(
cli.pin_reqs.get("core"),
Some(&vec![Some("=0.4.0".to_string()), Some("^0.4".to_string())])
);
}
#[test]
fn rust_workspace_graph_omits_req_for_a_pathonly_edge() {
let root = "[workspace]\nmembers = [\"core\", \"cli\"]\n";
let core = "[package]\nname = \"octl-core\"\nversion = \"0.4.0\"\n";
let cli = "[package]\nname = \"cli\"\nversion = \"0.4.0\"\n\n[dependencies]\n\
octl-core = { path = \"../core\" }\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/core/Cargo.toml", core)
.file("/repo/cli/Cargo.toml", cli);
let ws = detect_rust_workspace(repo(), &fs).expect("workspace");
let cli = ws.members.iter().find(|m| m.package == "cli").unwrap();
assert_eq!(cli.workspace_deps, vec!["octl-core".to_string()]);
assert!(!cli.dep_reqs.contains_key("octl-core"));
}
#[test]
fn rust_workspace_graph_reads_target_specific_dependency_edges() {
let root = "[workspace]\nmembers = [\"core\", \"cli\"]\n";
let core = "[package]\nname = \"octl-core\"\nversion = \"1.0.0\"\n";
let cli = "[package]\nname = \"cli\"\nversion = \"1.0.0\"\n\n\
[target.'cfg(unix)'.dependencies]\n\
octl-core = { path = \"../core\", version = \"=1.0.0\" }\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/core/Cargo.toml", core)
.file("/repo/cli/Cargo.toml", cli);
let ws = detect_rust_workspace(repo(), &fs).expect("workspace");
let cli = ws.members.iter().find(|m| m.package == "cli").unwrap();
assert_eq!(cli.workspace_deps, vec!["octl-core".to_string()]);
}
#[test]
fn rust_workspace_graph_reads_subtable_path_edge_and_rename() {
let root = "[workspace]\nmembers = [\"core\", \"cli\"]\n";
let core = "[package]\nname = \"real-core\"\nversion = \"1.0.0\"\n";
let cli = "[package]\nname = \"cli\"\nversion = \"1.0.0\"\n\n\
[dependencies.alias]\npackage = \"real-core\"\npath = \"../core\"\n\
version = \"=1.0.0\"\n\n\
[dependencies.serde]\nversion = \"1\"\n"; let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/core/Cargo.toml", core)
.file("/repo/cli/Cargo.toml", cli);
let ws = detect_rust_workspace(repo(), &fs).expect("workspace");
let cli = ws.members.iter().find(|m| m.package == "cli").unwrap();
assert_eq!(
cli.workspace_deps,
vec!["real-core".to_string()],
"the renamed path sub-table is the only edge; the version-only one is not"
);
}
#[test]
fn rust_workspace_graph_reads_dotted_key_path_edge() {
let root = "[workspace]\nmembers = [\"core\", \"cli\"]\n";
let core = "[package]\nname = \"core\"\nversion = \"1.0.0\"\n";
let cli = "[package]\nname = \"cli\"\nversion = \"1.0.0\"\n\n[dependencies]\n\
core.path = \"../core\"\ncore.version = \"=1.0.0\"\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/core/Cargo.toml", core)
.file("/repo/cli/Cargo.toml", cli);
let ws = detect_rust_workspace(repo(), &fs).expect("workspace");
let cli = ws.members.iter().find(|m| m.package == "cli").unwrap();
assert_eq!(cli.workspace_deps, vec!["core".to_string()]);
}
#[test]
fn rust_workspace_graph_dep_rename_ignores_package_substring_in_a_path() {
let root = "[workspace]\nmembers = [\"core\", \"cli\"]\n";
let core = "[package]\nname = \"octl-core\"\nversion = \"1.0.0\"\n";
let cli = "[package]\nname = \"cli\"\nversion = \"1.0.0\"\n\n[dependencies]\n\
octl-core = { path = \"../my-package-core\", version = \"1\" }\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/core/Cargo.toml", core)
.file("/repo/cli/Cargo.toml", cli);
let ws = detect_rust_workspace(repo(), &fs).expect("workspace");
let cli = ws.members.iter().find(|m| m.package == "cli").unwrap();
assert_eq!(cli.workspace_deps, vec!["octl-core".to_string()]);
}
#[test]
fn rust_workspace_graph_reads_build_dependency_edges() {
let root = "[workspace]\nmembers = [\"gen\", \"app\"]\n";
let gen = "[package]\nname = \"gen\"\nversion = \"1.0.0\"\n";
let app = "[package]\nname = \"app\"\nversion = \"1.0.0\"\n\n\
[build-dependencies]\ngen = { path = \"../gen\" }\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/gen/Cargo.toml", gen)
.file("/repo/app/Cargo.toml", app);
let ws = detect_rust_workspace(repo(), &fs).expect("workspace");
let app = ws.members.iter().find(|m| m.package == "app").unwrap();
assert_eq!(app.workspace_deps, vec!["gen".to_string()]);
}
#[test]
fn rust_workspace_graph_excludes_dev_dependency_edges() {
let root = "[workspace]\nmembers = [\"lib\", \"cli\"]\n";
let lib = "[package]\nname = \"lib\"\nversion = \"1.0.0\"\n\n\
[dev-dependencies]\ncli = { path = \"../cli\" }\n";
let cli = "[package]\nname = \"cli\"\nversion = \"1.0.0\"\n\n[dependencies]\n\
lib = { path = \"../lib\" }\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/lib/Cargo.toml", lib)
.file("/repo/cli/Cargo.toml", cli);
let ws = detect_rust_workspace(repo(), &fs).expect("workspace");
let lib = ws.members.iter().find(|m| m.package == "lib").unwrap();
let cli = ws.members.iter().find(|m| m.package == "cli").unwrap();
assert!(
lib.workspace_deps.is_empty(),
"dev-dependency edge is not an ordering edge"
);
assert_eq!(cli.workspace_deps, vec!["lib".to_string()]);
}
#[test]
fn cargo_workspace_inline_version_inheritance() {
let root = "[workspace]\nmembers = [\"m\"]\n\n\
[workspace.package]\nversion = \"1.5.0\"\n";
let member = "[package]\nname = \"m\"\nversion = { workspace = true }\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/m/Cargo.toml", member);
let facts = gather(repo(), &fs, &FakeGit::default());
assert_eq!(facts.packages.len(), 1);
assert_eq!(facts.packages[0].package.as_deref(), Some("m"));
assert_eq!(facts.packages[0].version.as_deref(), Some("1.5.0"));
assert!(facts.has_ge_1_0_release);
}
#[test]
fn cargo_workspace_multiline_members_array() {
let root = "[workspace]\nmembers = [\n \"a\",\n \"b\",\n]\n\n\
[workspace.package]\nversion = \"0.2.0\"\n";
let a = "[package]\nname = \"a\"\nversion.workspace = true\n";
let b = "[package]\nname = \"b\"\nversion.workspace = true\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/a/Cargo.toml", a)
.file("/repo/b/Cargo.toml", b);
let facts = gather(repo(), &fs, &FakeGit::default());
let names: Vec<_> = facts
.packages
.iter()
.map(|p| p.package.as_deref())
.collect();
assert_eq!(names, vec![Some("a"), Some("b")]);
assert!(facts
.packages
.iter()
.all(|p| p.version.as_deref() == Some("0.2.0")));
}
#[test]
fn cargo_workspace_member_without_workspace_package_table() {
let root = "[workspace]\nmembers = [\"m\"]\n";
let member = "[package]\nname = \"m\"\nversion.workspace = true\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/m/Cargo.toml", member);
let facts = gather(repo(), &fs, &FakeGit::default());
assert_eq!(facts.packages.len(), 1);
assert_eq!(facts.packages[0].package.as_deref(), Some("m"));
assert_eq!(facts.packages[0].version, None);
}
#[test]
fn cargo_workspace_glob_members_are_expanded() {
let root = "[workspace]\nmembers = [\"crates/*\"]\n\n\
[workspace.package]\nversion = \"0.4.0\"\n";
let a = "[package]\nname = \"za\"\nversion.workspace = true\n";
let b = "[package]\nname = \"mb\"\nversion.workspace = true\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/crates/za/Cargo.toml", a)
.file("/repo/crates/mb/Cargo.toml", b)
.file("/repo/crates/README.md", "not a crate\n");
let facts = gather(repo(), &fs, &FakeGit::default());
let names: Vec<_> = facts
.packages
.iter()
.map(|p| p.package.as_deref())
.collect();
assert_eq!(names, vec![Some("mb"), Some("za")]);
assert_eq!(facts.packages[0].manifest, "crates/mb/Cargo.toml");
assert!(facts
.packages
.iter()
.all(|p| p.version.as_deref() == Some("0.4.0")));
}
#[test]
fn cargo_workspace_exclude_and_dedup() {
let root = "[workspace]\nmembers = [\"crates/*\", \"crates/a\"]\n\
exclude = [\"crates/b\"]\n\n\
[workspace.package]\nversion = \"0.1.0\"\n";
let a = "[package]\nname = \"a\"\nversion.workspace = true\n";
let b = "[package]\nname = \"b\"\nversion.workspace = true\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/crates/a/Cargo.toml", a)
.file("/repo/crates/b/Cargo.toml", b);
let facts = gather(repo(), &fs, &FakeGit::default());
let names: Vec<_> = facts
.packages
.iter()
.map(|p| p.package.as_deref())
.collect();
assert_eq!(names, vec![Some("a")]);
}
#[test]
fn cargo_workspace_commented_out_member_is_ignored() {
let root = "[workspace]\nmembers = [\n \"a\",\n # \"b\",\n]\n\n\
[workspace.package]\nversion = \"0.1.0\"\n";
let a = "[package]\nname = \"a\"\nversion.workspace = true\n";
let b = "[package]\nname = \"b\"\nversion.workspace = true\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/a/Cargo.toml", a)
.file("/repo/b/Cargo.toml", b);
let facts = gather(repo(), &fs, &FakeGit::default());
let names: Vec<_> = facts
.packages
.iter()
.map(|p| p.package.as_deref())
.collect();
assert_eq!(names, vec![Some("a")]);
}
#[test]
fn cargo_workspace_rejects_escaping_member_paths() {
let root = "[workspace]\nmembers = [\"../outside\", \"/abs\"]\n";
let outside = "[package]\nname = \"outside\"\nversion = \"9.9.9\"\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/outside/Cargo.toml", outside)
.file("/abs/Cargo.toml", outside);
let facts = gather(repo(), &fs, &FakeGit::default());
assert_eq!(facts.packages.len(), 1);
assert_eq!(facts.packages[0].manifest, "Cargo.toml");
assert_eq!(facts.packages[0].package, None);
}
#[test]
fn cargo_workspace_inheritance_scoped_and_boolean_strict() {
let root = "[workspace]\nmembers = [\"m\", \"n\"]\n\n\
[workspace.package]\nversion = \"7.7.7\"\n";
let m = "[package]\nname = \"m\"\n\n\
[package.metadata.tool]\nversion.workspace = true\n";
let n = "[package]\nname = \"n\"\nversion.workspace = trueish\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/m/Cargo.toml", m)
.file("/repo/n/Cargo.toml", n);
let facts = gather(repo(), &fs, &FakeGit::default());
assert_eq!(facts.packages[0].package.as_deref(), Some("m"));
assert_eq!(facts.packages[0].version, None);
assert_eq!(facts.packages[1].package.as_deref(), Some("n"));
assert_eq!(facts.packages[1].version, None);
}
#[test]
fn cargo_workspace_inline_inheritance_rejects_false_positive() {
let root = "[workspace]\nmembers = [\"yes\", \"no\"]\n\n\
[workspace.package]\nversion = \"3.0.0\"\n";
let yes = "[package]\nname = \"yes\"\nversion = { workspace = true }\n";
let no = "[package]\nname = \"no\"\nversion = { workspace = false, path = \"x\" }\n";
let fs = FakeFs::default()
.file("/repo/Cargo.toml", root)
.file("/repo/yes/Cargo.toml", yes)
.file("/repo/no/Cargo.toml", no);
let facts = gather(repo(), &fs, &FakeGit::default());
assert_eq!(facts.packages[0].version.as_deref(), Some("3.0.0"));
assert_eq!(facts.packages[1].version, None);
}
#[test]
fn package_json_parsed_and_go_mod_module() {
let pkg = r#"{"name": "@acme/cli", "version": "2.0.0", "description": "cli"}"#;
let fs = FakeFs::default()
.file("/repo/package.json", pkg)
.file("/repo/go.mod", "module github.com/acme/tool\n\ngo 1.22\n");
let facts = gather(repo(), &fs, &FakeGit::default());
assert_eq!(facts.ecosystems, vec![Ecosystem::Node, Ecosystem::Go]);
let node = facts
.packages
.iter()
.find(|p| p.ecosystem == Ecosystem::Node)
.unwrap();
assert_eq!(node.package.as_deref(), Some("@acme/cli"));
assert_eq!(node.version.as_deref(), Some("2.0.0"));
let go = facts
.packages
.iter()
.find(|p| p.ecosystem == Ecosystem::Go)
.unwrap();
assert_eq!(go.package.as_deref(), Some("github.com/acme/tool"));
assert_eq!(facts.description.as_deref(), Some("cli"));
}
#[test]
fn pyproject_project_then_poetry_fallback() {
let project = "[project]\nname = \"widget\"\nversion = \"1.4.0\"\n\
description = \"a widget\"\n";
let facts = gather(
repo(),
&FakeFs::default().file("/repo/pyproject.toml", project),
&FakeGit::default(),
);
assert_eq!(facts.packages[0].package.as_deref(), Some("widget"));
assert_eq!(facts.packages[0].version.as_deref(), Some("1.4.0"));
let poetry = "[tool.poetry]\nname = \"legacy\"\nversion = \"0.1.0\"\n";
let facts = gather(
repo(),
&FakeFs::default().file("/repo/pyproject.toml", poetry),
&FakeGit::default(),
);
assert_eq!(facts.packages[0].package.as_deref(), Some("legacy"));
}
#[test]
fn pyproject_single_quoted_strings_parse() {
let project = "[project]\nname = 'widget'\nversion = '1.2.0'\n\
description = 'a widget'\n";
let facts = gather(
repo(),
&FakeFs::default().file("/repo/pyproject.toml", project),
&FakeGit::default(),
);
assert_eq!(facts.packages[0].package.as_deref(), Some("widget"));
assert_eq!(facts.packages[0].version.as_deref(), Some("1.2.0"));
assert!(facts.has_ge_1_0_release);
assert_eq!(facts.description.as_deref(), Some("a widget"));
}
#[test]
fn toml_key_matches_whole_token_not_prefix() {
let cargo = "[package]\nnamespace = \"nope\"\nversion-code = \"9\"\n\
name = \"real\"\nversion = \"0.2.0\"\n";
let facts = gather(
repo(),
&FakeFs::default().file("/repo/Cargo.toml", cargo),
&FakeGit::default(),
);
assert_eq!(facts.packages[0].package.as_deref(), Some("real"));
assert_eq!(facts.packages[0].version.as_deref(), Some("0.2.0"));
}
#[test]
fn setup_py_marks_python_but_adds_no_package() {
let fs = FakeFs::default().file("/repo/setup.py", "from setuptools import setup\n");
let facts = gather(repo(), &fs, &FakeGit::default());
assert_eq!(facts.ecosystems, vec![Ecosystem::Python]);
assert!(facts.packages.is_empty());
}
#[test]
fn ecosystems_emit_in_canonical_order() {
let fs = FakeFs::default().file("/repo/go.mod", "module x\n").file(
"/repo/Cargo.toml",
"[package]\nname = \"a\"\nversion = \"0.1.0\"\n",
);
let facts = gather(repo(), &fs, &FakeGit::default());
assert_eq!(facts.ecosystems, vec![Ecosystem::Rust, Ecosystem::Go]);
}
#[test]
fn workflows_dir_counts_only_when_non_empty() {
let empty = FakeFs::default().dir("/repo/.github/workflows").file(
"/repo/Cargo.toml",
"[package]\nname=\"a\"\nversion=\"0.1.0\"\n",
);
assert!(!gather(repo(), &empty, &FakeGit::default()).has_ci);
let with_wf = FakeFs::default()
.file("/repo/.github/workflows/ci.yml", "on: push\n")
.file(
"/repo/Cargo.toml",
"[package]\nname=\"a\"\nversion=\"0.1.0\"\n",
);
assert!(gather(repo(), &with_wf, &FakeGit::default()).has_ci);
}
#[test]
fn single_file_ci_configs_count_on_existence() {
for name in [
".gitlab-ci.yml",
"azure-pipelines.yml",
".drone.yml",
"Jenkinsfile",
] {
let fs = FakeFs::default().file(&format!("/repo/{name}"), "ci\n");
assert!(
gather(repo(), &fs, &FakeGit::default()).has_ci,
"{name} should count as CI"
);
}
}
#[test]
fn distribution_surface_detects_cargo_dist_and_only_tag_push_workflows() {
let fs = FakeFs::default()
.file("/repo/dist-workspace.toml", "[dist]\nci = \"github\"\n")
.file("/repo/Cargo.toml", "[workspace.metadata.dist]\ndist = true\n")
.file(
"/repo/.github/workflows/release.yml",
"on:\n push:\n tags:\n - 'v*'\n",
)
.file(
"/repo/.github/workflows/ci.yml",
"on:\n push:\n branches: [main]\n pull_request:\n tags: [never-a-trigger]\n",
);
let surface = gather(repo(), &fs, &FakeGit::default()).distribution_surface;
assert!(surface.has_cargo_dist);
assert_eq!(
surface.cargo_dist_evidence,
vec![
"dist-workspace.toml".to_string(),
"Cargo.toml ([workspace.metadata.dist])".to_string()
]
);
assert_eq!(surface.tag_triggered_workflows, vec!["release.yml"]);
assert!(surface.tag_triggered_cargo_publish_workflows.is_empty());
}
#[test]
fn cargo_publish_workflow_detection_is_empty_when_workflow_is_missing() {
let fs = FakeFs::default().file(
"/repo/Cargo.toml",
"[package]\nname = \"missing-workflow\"\n",
);
let surface = gather(repo(), &fs, &FakeGit::default()).distribution_surface;
assert!(surface.tag_triggered_workflows.is_empty());
assert!(surface.tag_triggered_cargo_publish_workflows.is_empty());
}
#[test]
fn dispatch_only_cargo_publish_workflow_is_not_tag_triggered_evidence() {
let fs = FakeFs::default().file(
"/repo/.github/workflows/publish.yml",
"on:\n workflow_dispatch:\njobs:\n publish:\n runs-on: ubuntu-latest\n steps:\n - run: cargo publish\n",
);
let surface = gather(repo(), &fs, &FakeGit::default()).distribution_surface;
assert!(surface.tag_triggered_workflows.is_empty());
assert!(surface.tag_triggered_cargo_publish_workflows.is_empty());
}
#[test]
fn direct_tag_triggered_cargo_publish_workflow_is_detected() {
let fs = FakeFs::default().file(
"/repo/.github/workflows/publish.yaml",
"on:\n push:\n tags:\n - 'v*'\njobs:\n publish:\n runs-on: ubuntu-latest\n steps:\n - run: cargo publish --locked\n",
);
let surface = gather(repo(), &fs, &FakeGit::default()).distribution_surface;
assert_eq!(surface.tag_triggered_workflows, vec!["publish.yaml"]);
assert_eq!(
surface.tag_triggered_cargo_publish_workflows,
vec!["publish.yaml"]
);
}
#[test]
fn cargo_publish_command_requires_a_real_crates_io_publish() {
for command in [
"cargo publish --dry-run",
"cargo publish --help",
"cargo publish --registry internal",
"cargo publish --index https://example.invalid",
"echo cargo publish",
"# cargo publish",
] {
assert!(
!command_runs_cargo_publish(command),
"{command:?} is not credible crates.io publish evidence"
);
}
for command in [
"cargo publish --locked",
"cargo +stable publish --locked",
"cd crates/demo && cargo publish --registry crates-io",
"CARGO_TERM_COLOR=always cargo publish; echo done",
] {
assert!(
command_runs_cargo_publish(command),
"{command:?} should be credible crates.io publish evidence"
);
}
}
#[test]
fn tag_triggered_local_reusable_cargo_publish_workflow_is_detected() {
let fs = FakeFs::default()
.file(
"/repo/.github/workflows/release.yml",
"on:\n push:\n tags:\n - 'v*'\njobs:\n crates:\n uses: ./.github/workflows/publish.yml\n",
)
.file(
"/repo/.github/workflows/publish.yml",
"on:\n workflow_call:\njobs:\n publish:\n runs-on: ubuntu-latest\n steps:\n - run: cargo publish --locked\n",
);
let surface = gather(repo(), &fs, &FakeGit::default()).distribution_surface;
assert_eq!(surface.tag_triggered_workflows, vec!["release.yml"]);
assert_eq!(
surface.tag_triggered_cargo_publish_workflows,
vec!["release.yml"]
);
}
#[test]
fn local_workflow_without_workflow_call_is_not_credible_publish_evidence() {
let fs = FakeFs::default()
.file(
"/repo/.github/workflows/release.yml",
"on:\n push:\n tags:\n - 'v*'\njobs:\n crates:\n uses: ./.github/workflows/publish.yml\n",
)
.file(
"/repo/.github/workflows/publish.yml",
"on:\n workflow_dispatch:\njobs:\n publish:\n runs-on: ubuntu-latest\n steps:\n - run: cargo publish --locked\n",
);
let surface = gather(repo(), &fs, &FakeGit::default()).distribution_surface;
assert_eq!(surface.tag_triggered_workflows, vec!["release.yml"]);
assert!(surface.tag_triggered_cargo_publish_workflows.is_empty());
}
#[test]
fn distribution_surface_is_empty_without_dist_or_tag_push() {
let fs = FakeFs::default()
.file("/repo/Cargo.toml", "[package]\nname = \"plain\"\n")
.file(
"/repo/.github/workflows/ci.yaml",
"on:\n push:\n branches: [main]\n",
);
let surface = gather(repo(), &fs, &FakeGit::default()).distribution_surface;
assert!(!surface.has_cargo_dist);
assert!(surface.cargo_dist_evidence.is_empty());
assert!(surface.tag_triggered_workflows.is_empty());
assert!(surface.tag_triggered_cargo_publish_workflows.is_empty());
}
#[test]
fn dependency_bot_and_issues_dir() {
let dependabot = FakeFs::default().file("/repo/.github/dependabot.yml", "version: 2\n");
assert_eq!(
gather(repo(), &dependabot, &FakeGit::default()).dependency_bot,
Some("dependabot".to_string())
);
let renovate = FakeFs::default().file("/repo/renovate.json", "{}\n");
assert_eq!(
gather(repo(), &renovate, &FakeGit::default()).dependency_bot,
Some("renovate".to_string())
);
let both = FakeFs::default()
.file("/repo/.github/dependabot.yml", "version: 2\n")
.file("/repo/renovate.json", "{}\n");
assert_eq!(
gather(repo(), &both, &FakeGit::default()).dependency_bot,
Some("dependabot".to_string())
);
let issues = FakeFs::default().dir("/repo/issues");
assert!(gather(repo(), &issues, &FakeGit::default()).has_issues_dir);
}
#[test]
fn readme_self_label_and_prose_description() {
let readme = "# My Tool\n\n> a quote\n\nStatus: private, early. Not much yet.\n";
let fs = FakeFs::default().file("/repo/README.md", readme);
let facts = gather(repo(), &fs, &FakeGit::default());
assert_eq!(facts.readme_self_label.as_deref(), Some("spike"));
assert_eq!(
facts.description.as_deref(),
Some("Status: private, early. Not much yet.")
);
}
#[test]
fn description_truncates_to_120_chars() {
let long = "x".repeat(200);
let fs = FakeFs::default().file("/repo/README.md", &format!("intro\n{long}\n"));
let facts = gather(repo(), &fs, &FakeGit::default());
let cargo = format!("[package]\nname=\"a\"\nversion=\"0.1.0\"\ndescription=\"{long}\"\n");
let fs2 = FakeFs::default().file("/repo/Cargo.toml", &cargo);
let facts2 = gather(repo(), &fs2, &FakeGit::default());
assert_eq!(facts.description.as_deref(), Some("intro"));
assert_eq!(
facts2.description.as_deref().map(|d| d.chars().count()),
Some(120)
);
}
#[test]
fn read_limit_counts_chars_not_bytes() {
let desc = "く".repeat(60); let cargo = format!("[package]\nname=\"a\"\nversion=\"0.1.0\"\ndescription=\"{desc}\"\n");
let fs = FakeFs::default().file("/repo/Cargo.toml", &cargo);
let facts = gather(repo(), &fs, &FakeGit::default());
assert_eq!(facts.description.as_deref(), Some(desc.as_str()));
assert!(!facts.description.as_deref().unwrap().contains('\u{FFFD}'));
}
#[test]
fn semver_parse_plain_prefixed_and_prerelease() {
assert_eq!(semver_parse("v1.2.3"), Some((1, 2, 3, false)));
assert_eq!(semver_parse("1.2.3"), Some((1, 2, 3, false)));
assert_eq!(semver_parse("core-v1.2.3"), Some((1, 2, 3, false)));
assert_eq!(semver_parse("@acme/cli@2.0.0"), Some((2, 0, 0, false)));
assert_eq!(semver_parse("1.2.3-rc1"), Some((1, 2, 3, true)));
assert_eq!(semver_parse("1.2.3+build"), Some((1, 2, 3, false)));
assert_eq!(semver_parse("nightly"), None);
assert_eq!(semver_parse("1.2"), None);
assert_eq!(semver_parse("1.2.3.4"), None);
}
#[test]
fn ge_1_0_release_from_tag_but_not_from_prerelease() {
let fs = FakeFs::default().file(
"/repo/Cargo.toml",
"[package]\nname=\"a\"\nversion=\"0.9.0\"\n",
);
let facts = gather(repo(), &fs, &git_with(1, 1, &["v0.9.0", "v1.0.0"]));
assert!(facts.has_semver_tag);
assert!(facts.has_ge_1_0_release);
let fs2 = FakeFs::default().file(
"/repo/Cargo.toml",
"[package]\nname=\"a\"\nversion=\"0.9.0\"\n",
);
let facts2 = gather(repo(), &fs2, &git_with(1, 1, &["v1.0.0-rc1"]));
assert!(facts2.has_semver_tag);
assert!(!facts2.has_ge_1_0_release);
}
#[test]
fn ge_1_0_release_from_manifest_version() {
let fs = FakeFs::default().file(
"/repo/Cargo.toml",
"[package]\nname=\"a\"\nversion=\"1.4.0\"\n",
);
let facts = gather(repo(), &fs, &FakeGit::default());
assert!(facts.has_ge_1_0_release);
}
#[test]
fn version_ge_1_0_requires_dot_after_major() {
assert!(version_ge_1_0(Some("1.0.0")));
assert!(version_ge_1_0(Some("v2.3")));
assert!(version_ge_1_0(Some("2024.1")));
assert!(!version_ge_1_0(Some("0.9.9")));
assert!(!version_ge_1_0(Some("1"))); assert!(!version_ge_1_0(None));
}
#[test]
fn production_needs_two_recent_committers_ge_1_0_and_ci() {
let fs = FakeFs::default()
.file(
"/repo/Cargo.toml",
"[package]\nname=\"a\"\nversion=\"1.2.0\"\n",
)
.file("/repo/.github/workflows/ci.yml", "on: push\n");
let facts = gather(repo(), &fs, &git_with(4, 3, &["v1.2.0"]));
assert!(facts.has_ci);
assert!(facts.has_ge_1_0_release);
assert!(facts.maturity_signals.production);
assert_eq!(facts.inferred_maturity, Maturity::Production);
}
#[test]
fn mvp_when_ci_present_but_not_production_grade() {
let fs = FakeFs::default()
.file(
"/repo/Cargo.toml",
"[package]\nname=\"a\"\nversion=\"0.3.0\"\n",
)
.file("/repo/.github/workflows/ci.yml", "on: push\n");
let facts = gather(repo(), &fs, &git_with(1, 1, &["v0.3.0"]));
assert!(!facts.maturity_signals.production);
assert!(!facts.maturity_signals.spike);
assert_eq!(facts.inferred_maturity, Maturity::Mvp);
}
fn zerover_fs(bot: &str) -> FakeFs {
FakeFs::default()
.file(
"/repo/Cargo.toml",
"[package]\nname=\"a\"\nversion=\"0.6.0\"\n",
)
.file("/repo/.github/workflows/ci.yml", "on: push\n")
.file(&format!("/repo/{bot}"), "version: 2\n")
}
#[test]
fn pre_1_0_with_full_release_infra_is_production() {
let fs = zerover_fs(".github/dependabot.yml");
let facts = gather(repo(), &fs, &git_with(3, 3, &["v0.5.0", "v0.6.0"]));
assert!(facts.has_ci);
assert!(!facts.has_ge_1_0_release);
assert_eq!(facts.dependency_bot.as_deref(), Some("dependabot"));
assert!(facts.maturity_signals.production);
assert_eq!(facts.inferred_maturity, Maturity::Production);
}
#[test]
fn renovate_unlocks_the_zerover_release_path() {
let fs = zerover_fs("renovate.json");
let facts = gather(repo(), &fs, &git_with(2, 2, &["v0.1.0", "v0.2.0"]));
assert_eq!(facts.dependency_bot.as_deref(), Some("renovate"));
assert!(facts.maturity_signals.production);
assert_eq!(facts.inferred_maturity, Maturity::Production);
}
#[test]
fn bare_0x_with_only_a_tag_is_not_production() {
let fs = FakeFs::default().file(
"/repo/Cargo.toml",
"[package]\nname=\"a\"\nversion=\"0.6.0\"\n",
);
let facts = gather(repo(), &fs, &git_with(3, 3, &["v0.6.0"]));
assert!(!facts.has_ci);
assert!(!facts.has_ge_1_0_release);
assert_eq!(facts.dependency_bot, None);
assert!(!facts.maturity_signals.production);
assert_eq!(facts.inferred_maturity, Maturity::Mvp);
}
#[test]
fn zerover_v0_0_x_tag_is_not_a_shipped_release() {
let fs = zerover_fs(".github/dependabot.yml");
let facts = gather(repo(), &fs, &git_with(3, 3, &["v0.0.1", "v0.0.2"]));
assert!(facts.has_ci);
assert_eq!(facts.dependency_bot.as_deref(), Some("dependabot"));
assert!(!facts.maturity_signals.production);
assert_eq!(facts.inferred_maturity, Maturity::Mvp);
}
#[test]
fn pre_1_0_release_infra_requires_release_cadence() {
let fs = zerover_fs(".github/dependabot.yml");
let facts = gather(repo(), &fs, &git_with(3, 3, &["v0.1.0"]));
assert!(!facts.maturity_signals.production);
assert_eq!(facts.inferred_maturity, Maturity::Mvp);
}
#[test]
fn pre_1_0_release_infra_requires_dependency_bot() {
let fs = FakeFs::default()
.file(
"/repo/Cargo.toml",
"[package]\nname=\"a\"\nversion=\"0.6.0\"\n",
)
.file("/repo/.github/workflows/ci.yml", "on: push\n");
let facts = gather(repo(), &fs, &git_with(3, 3, &["v0.5.0", "v0.6.0"]));
assert_eq!(facts.dependency_bot, None);
assert!(!facts.maturity_signals.production);
assert_eq!(facts.inferred_maturity, Maturity::Mvp);
}
#[test]
fn pre_1_0_release_infra_ignores_prerelease_tags_for_cadence() {
let fs = zerover_fs(".github/dependabot.yml");
let facts = gather(
repo(),
&fs,
&git_with(3, 3, &["v0.5.0", "v0.6.0-rc1", "v0.7.0-rc1"]),
);
assert!(facts.has_semver_tag);
assert!(!facts.maturity_signals.production);
assert_eq!(facts.inferred_maturity, Maturity::Mvp);
}
#[test]
fn pre_1_0_release_infra_requires_two_recent_committers() {
let fs = zerover_fs(".github/dependabot.yml");
let facts = gather(repo(), &fs, &git_with(1, 1, &["v0.5.0", "v0.6.0"]));
assert!(!facts.maturity_signals.production);
assert_eq!(facts.inferred_maturity, Maturity::Mvp);
}
#[test]
fn ge_1_0_release_reaches_production_without_a_dependency_bot() {
let fs = FakeFs::default()
.file(
"/repo/Cargo.toml",
"[package]\nname=\"a\"\nversion=\"1.2.0\"\n",
)
.file("/repo/.github/workflows/ci.yml", "on: push\n");
let facts = gather(repo(), &fs, &git_with(2, 2, &["v1.2.0"]));
assert!(facts.has_ge_1_0_release);
assert_eq!(facts.dependency_bot, None);
assert!(facts.maturity_signals.production);
assert_eq!(facts.inferred_maturity, Maturity::Production);
}
#[test]
fn spike_forced_by_readme_label_even_with_multiple_committers() {
let fs = FakeFs::default()
.file(
"/repo/Cargo.toml",
"[package]\nname=\"a\"\nversion=\"0.1.0\"\n",
)
.file(
"/repo/README.md",
"# X\n\nThis is an experimental prototype.\n",
);
let facts = gather(repo(), &fs, &git_with(3, 3, &[]));
assert_eq!(facts.readme_self_label.as_deref(), Some("spike"));
assert!(facts.maturity_signals.spike);
assert_eq!(facts.inferred_maturity, Maturity::Spike);
}
#[test]
fn multi_committer_no_ci_no_label_is_mvp_not_spike() {
let fs = FakeFs::default().file(
"/repo/Cargo.toml",
"[package]\nname=\"a\"\nversion=\"0.1.0\"\n",
);
let facts = gather(repo(), &fs, &git_with(3, 2, &[]));
assert!(!facts.maturity_signals.spike);
assert_eq!(facts.inferred_maturity, Maturity::Mvp);
}
#[test]
fn same_repo_same_facts() {
let build = || {
FakeFs::default()
.file(
"/repo/Cargo.toml",
"[package]\nname=\"a\"\nversion=\"0.3.0\"\n",
)
.file("/repo/.github/workflows/ci.yml", "on: push\n")
};
let a = gather(repo(), &build(), &git_with(2, 2, &["v0.3.0"]));
let b = gather(repo(), &build(), &git_with(2, 2, &["v0.3.0"]));
assert_eq!(
serde_json::to_string(&a).unwrap(),
serde_json::to_string(&b).unwrap()
);
}
}