use std::path::{Path, PathBuf};
fn crate_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn manifest() -> String {
let p = crate_root().join("Cargo.toml");
std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("read {p:?}: {e}"))
}
fn dependency_lines(manifest: &str) -> Vec<(String, String, String)> {
let mut section = String::new();
let mut out = Vec::new();
for raw in manifest.lines() {
let line = raw.trim();
if let Some(name) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) {
section = name.to_owned();
continue;
}
if line.is_empty() || line.starts_with('#') || !section.contains("dependencies") {
continue;
}
let Some((lhs, _)) = line.split_once('=') else {
continue;
};
out.push((section.clone(), lhs.trim().to_owned(), line.to_owned()));
}
out
}
fn host_dependencies(manifest: &str) -> Vec<String> {
dependency_lines(manifest)
.into_iter()
.filter(|(_, name, _)| name.starts_with("gunnar"))
.map(|(section, name, _)| format!("[{section}] {name}"))
.collect()
}
fn workspace_inherited(manifest: &str) -> Vec<String> {
dependency_lines(manifest)
.into_iter()
.filter(|(_, _, line)| line.contains("workspace") && line.contains("true"))
.map(|(section, name, _)| format!("[{section}] {name}"))
.collect()
}
fn rayon_dependencies(manifest: &str) -> Vec<String> {
dependency_lines(manifest)
.into_iter()
.filter(|(_, name, _)| name.contains("rayon"))
.map(|(section, name, _)| format!("[{section}] {name}"))
.collect()
}
fn rust_sources(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let entries = std::fs::read_dir(dir).unwrap_or_else(|e| panic!("read {dir:?}: {e}"));
for entry in entries {
let path = entry.expect("dir entry").path();
if path.is_dir() {
out.extend(rust_sources(&path));
} else if path.extension().is_some_and(|x| x == "rs") {
out.push(path);
}
}
out.sort();
out
}
#[test]
fn the_manifest_names_no_dependency_from_the_host_project() {
let found = host_dependencies(&manifest());
assert!(
found.is_empty(),
"gunnar-sendpack depends on {found:?}. D21: this crate carries no gunnar types and \
no gunnar dependency, ever. If it needs one, the design is wrong"
);
}
#[test]
fn no_dependency_inherits_from_the_workspace() {
let found = workspace_inherited(&manifest());
assert!(
found.is_empty(),
"{found:?} inherit from the workspace, which makes this manifest meaningless \
outside gunnar's tree. That is the exact coupling D21 exists to prevent"
);
}
#[test]
fn concurrency_is_not_rayon() {
let found = rayon_dependencies(&manifest());
assert!(
found.is_empty(),
"rayon appears in the manifest as {found:?}"
);
for path in rust_sources(&crate_root().join("src")) {
let text = std::fs::read_to_string(&path).expect("read source");
for (n, line) in text.lines().enumerate() {
let code = line.split("//").next().unwrap_or("");
assert!(
!code.contains("rayon"),
"{}:{} reaches for rayon: {line}",
path.display(),
n + 1
);
}
}
}
#[test]
fn no_source_file_imports_the_host_project() {
let mut checked = 0usize;
for path in rust_sources(&crate_root().join("src")) {
let text = std::fs::read_to_string(&path).expect("read source");
for (n, line) in text.lines().enumerate() {
let code = line.split("//").next().unwrap_or("");
assert!(
!code.contains("gunnar_") && !code.contains("gunnar::"),
"{}:{} reaches into the host project: {line}",
path.display(),
n + 1
);
}
checked += 1;
}
assert!(checked >= 5, "only {checked} source files were scanned");
}
#[test]
fn the_licence_matches_gitoxides() {
let m = manifest();
assert!(
m.contains(r#"license = "MIT OR Apache-2.0""#),
"the licence must match gitoxide's or the code cannot be offered to them"
);
}
#[test]
fn a_gunnar_dependency_is_detected() {
let bad = "[dependencies]\ngix-hash = \"0.26\"\ngunnar-core = { path = \"../gunnar-core\" }\n";
assert_eq!(host_dependencies(bad), vec!["[dependencies] gunnar-core"]);
assert!(host_dependencies("[dependencies]\ngix-hash = \"0.26\"\n").is_empty());
}
#[test]
fn workspace_inheritance_is_detected() {
let bad = "[package]\nversion.workspace = true\n[dependencies]\nthiserror.workspace = true\n";
assert_eq!(
workspace_inherited(bad),
vec!["[dependencies] thiserror.workspace"]
);
assert!(workspace_inherited("[dependencies]\nthiserror = \"2\"\n").is_empty());
}
#[test]
fn a_rayon_dependency_is_detected() {
let bad = "[dev-dependencies]\nrayon = \"1\"\n";
assert_eq!(rayon_dependencies(bad), vec!["[dev-dependencies] rayon"]);
assert!(rayon_dependencies("[dependencies]\ngix-features = \"0.49\"\n").is_empty());
}
#[test]
fn a_commented_out_dependency_is_not_mistaken_for_a_real_one() {
let m = "[dependencies]\n# gunnar-core = { path = \"..\" }\ngix-hash = \"0.26\"\n";
assert!(host_dependencies(m).is_empty());
}
fn unconditional_dependencies(manifest: &str) -> Vec<String> {
dependency_lines(manifest)
.into_iter()
.filter(|(section, _, line)| section == "dependencies" && !line.contains("optional = true"))
.map(|(_, name, _)| name)
.collect()
}
#[test]
fn the_grammar_half_depends_on_three_crates_and_no_framer() {
let found = unconditional_dependencies(&manifest());
assert_eq!(
found,
vec!["gix-hash", "bstr", "thiserror"],
"the always-on dependency set changed. Every name here is paid for by a \
server that wants only the four wire formats, and `gix-packetline` in \
particular must stay behind `blocking-io` — see the manifest"
);
}
#[test]
fn a_dependency_escaping_its_feature_gate_is_detected() {
let bad = "[dependencies]\ngix-hash = \"0.26\"\ngix-packetline = \"0.22\"\n";
assert_eq!(
unconditional_dependencies(bad),
vec!["gix-hash", "gix-packetline"]
);
let good = "[dependencies]\ngix-hash = \"0.26\"\n\
gix-packetline = { version = \"0.22\", optional = true }\n";
assert_eq!(unconditional_dependencies(good), vec!["gix-hash"]);
}
#[test]
fn the_io_feature_is_declared_and_gates_the_framer() {
let m = manifest();
assert!(
m.contains(r#"blocking-io = ["dep:gix-packetline"]"#),
"`blocking-io` must be the only thing that turns the framer on"
);
assert!(
m.contains(r#"default = ["blocking-io"]"#),
"a client is the majority consumer, so the I/O half is on by default"
);
}