use super::*;
const MIXED_KINDS_MANIFEST: &str = r#"[package]
name = "demo"
version = "0.1.0"
[dependencies]
fmt = ">=10"
zlib = { version = ">=1.2", system = true }
openssl = { version = ">=3", system = true }
[dev-dependencies]
gtest = "^1.14"
"#;
fn dep_entry<'a>(package: &'a serde_json::Value, name: &str, kind: &str) -> &'a serde_json::Value {
package["dependencies"]
.as_array()
.expect("dependencies array")
.iter()
.find(|d| d["name"] == name && d["dependency_kind"] == kind)
.unwrap_or_else(|| panic!("dep {name:?} of kind {kind:?} not found in {package}"))
}
fn run_mixed_kinds_metadata(manifest_path: &Path) -> serde_json::Value {
let fixtures = TempDir::new().expect("fixtures tempdir");
for (name, version) in [("zlib", "1.3"), ("openssl", "3.2")] {
assert_fs::fixture::ChildPath::new(fixtures.path().join(format!("{name}.json")))
.write_str(&format!(
r#"{{ "version": "{version}", "cflags": "", "libs": "" }}"#
))
.unwrap();
}
let output = cabin()
.env(
"CABIN_PKG_CONFIG",
workspace_test_bin("cabin-system-deps-fake-pkg-config"),
)
.env("CABIN_FAKE_PKG_CONFIG_FIXTURES", fixtures.path())
.args(["metadata", "--manifest-path"])
.arg(manifest_path)
.assert()
.success()
.get_output()
.clone();
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf-8");
serde_json::from_str(&stdout)
.unwrap_or_else(|err| panic!("expected valid JSON, got error {err} for: {stdout}"))
}
#[test]
fn metadata_lists_every_dependency_kind_with_explicit_kind_field() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str(MIXED_KINDS_MANIFEST)
.unwrap();
let value = run_mixed_kinds_metadata(&dir.path().join("cabin.toml"));
let demo = package_in(&value, "demo");
for (name, kind) in [("fmt", "normal"), ("gtest", "dev")] {
let dep = dep_entry(demo, name, kind);
assert_eq!(dep["kind"], "version", "{name} should be a version source");
}
let system = demo["system_dependencies"]
.as_array()
.expect("system_dependencies array");
assert_eq!(system.len(), 2);
let by_name: std::collections::BTreeMap<&str, &serde_json::Value> = system
.iter()
.map(|sd| (sd["name"].as_str().expect("system dep name"), sd))
.collect();
assert_eq!(by_name["zlib"]["version"], ">=1.2");
assert!(
by_name["zlib"].get("required").is_none(),
"system dep metadata must not expose a `required` field: {:?}",
by_name["zlib"],
);
assert_eq!(by_name["zlib"]["dependency_kind"], "normal");
assert_eq!(by_name["openssl"]["version"], ">=3");
assert!(by_name["openssl"].get("required").is_none());
assert_eq!(by_name["openssl"]["dependency_kind"], "normal");
}
#[test]
fn metadata_dependency_listing_is_sorted_by_kind_then_name() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str(MIXED_KINDS_MANIFEST)
.unwrap();
let value = run_mixed_kinds_metadata(&dir.path().join("cabin.toml"));
let demo = package_in(&value, "demo");
let listed: Vec<(String, String)> = demo["dependencies"]
.as_array()
.unwrap()
.iter()
.map(|d| {
(
d["dependency_kind"].as_str().unwrap().to_owned(),
d["name"].as_str().unwrap().to_owned(),
)
})
.collect();
assert_eq!(
listed,
vec![
("normal".into(), "fmt".into()),
("dev".into(), "gtest".into()),
]
);
}
#[test]
fn metadata_keeps_existing_shape_for_dependencies_only_manifest() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str(
r#"[package]
name = "app"
version = "0.1.0"
[dependencies]
fmt = ">=10"
"#,
)
.unwrap();
let value = run_metadata(&dir.path().join("cabin.toml"));
let app = package_in(&value, "app");
assert!(
app["system_dependencies"].is_null(),
"system_dependencies must be omitted when empty: got {app}"
);
let deps = app["dependencies"].as_array().unwrap();
assert_eq!(deps.len(), 1);
assert_eq!(deps[0]["name"], "fmt");
assert_eq!(deps[0]["dependency_kind"], "normal");
assert_eq!(deps[0]["kind"], "version");
}
#[test]
fn resolve_excludes_dev_dependencies() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str(
r#"[package]
name = "app"
version = "0.1.0"
[dependencies]
fmt = ">=10 <11"
[dev-dependencies]
gtest = "^1.14"
"#,
)
.unwrap();
write_index_entry_no_source(&dir.path().join("index"), "fmt", "10.2.1", &"0".repeat(64));
let assertion = cabin()
.args(["resolve", "--manifest-path"])
.arg(dir.path().join("cabin.toml"))
.args(["--index-path"])
.arg(dir.path().join("index"))
.assert()
.success();
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout);
assert!(stdout.contains("fmt"), "fmt should appear: {stdout}");
assert!(
!stdout.contains("gtest"),
"dev dep gtest must not enter ordinary resolution: {stdout}"
);
}
#[test]
fn resolve_does_not_send_system_dependencies_to_resolver() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str(
r#"[package]
name = "app"
version = "0.1.0"
[dependencies]
fmt = ">=10"
zlib = { version = ">=1.2", system = true }
"#,
)
.unwrap();
write_index_entry_no_source(&dir.path().join("index"), "fmt", "10.2.1", &"0".repeat(64));
let assertion = cabin()
.args(["resolve", "--manifest-path"])
.arg(dir.path().join("cabin.toml"))
.args(["--index-path"])
.arg(dir.path().join("index"))
.assert()
.success();
let stdout = String::from_utf8_lossy(&assertion.get_output().stdout);
assert!(stdout.contains("fmt"));
assert!(
!stdout.contains("zlib"),
"system dep zlib must not appear in resolver output: {stdout}"
);
}
#[test]
fn optional_dependency_in_system_section_is_rejected_at_cli() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str(
r#"[package]
name = "app"
version = "0.1.0"
[dependencies]
zlib = { version = ">=1.2", system = true, optional = true }
"#,
)
.unwrap();
let assertion = cabin()
.args(["metadata", "--manifest-path"])
.arg(dir.path().join("cabin.toml"))
.assert()
.failure();
let stderr = String::from_utf8_lossy(&assertion.get_output().stderr);
assert!(
stderr.contains("optional"),
"expected an error mentioning the unsupported optional system dep, got: {stderr}"
);
}
#[test]
fn workspace_inheritance_per_kind_is_validated_kind_specifically() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str(
r#"[workspace]
members = ["packages/app"]
[workspace.dependencies]
fmt = ">=10"
"#,
)
.unwrap();
dir.child("packages/app/cabin.toml")
.write_str(
r#"[package]
name = "app"
version = "0.1.0"
[dev-dependencies]
fmt = { workspace = true }
"#,
)
.unwrap();
let assertion = cabin()
.args(["metadata", "--manifest-path"])
.arg(dir.path().join("cabin.toml"))
.assert()
.failure();
let stderr = String::from_utf8_lossy(&assertion.get_output().stderr);
assert!(
stderr.contains("[workspace.dev-dependencies]"),
"error should name the missing workspace table: {stderr}"
);
assert!(
stderr.contains("[dev-dependencies]"),
"error should name the declaring section: {stderr}"
);
}
#[test]
fn package_metadata_round_trips_every_dependency_kind() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str(MIXED_KINDS_MANIFEST)
.unwrap();
let out = dir.path().join("dist");
cabin()
.args(["package", "--manifest-path"])
.arg(dir.path().join("cabin.toml"))
.args(["--output-dir"])
.arg(&out)
.assert()
.success();
let metadata_path = out.join("demo-0.1.0.json");
let metadata: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&metadata_path).unwrap()).unwrap();
assert_eq!(metadata["dependencies"]["fmt"].as_str().unwrap(), ">=10");
assert_eq!(
metadata["dev-dependencies"]["gtest"].as_str().unwrap(),
"^1.14"
);
let zlib = &metadata["system-dependencies"]["zlib"];
assert_eq!(zlib["version"].as_str().unwrap(), ">=1.2");
assert!(
zlib.get("required").is_none(),
"canonical metadata must not carry `required`: {zlib:?}",
);
assert_eq!(zlib["dependency_kind"].as_str().unwrap(), "normal");
let openssl = &metadata["system-dependencies"]["openssl"];
assert_eq!(openssl["version"].as_str().unwrap(), ">=3");
assert!(openssl.get("required").is_none());
assert_eq!(openssl["dependency_kind"].as_str().unwrap(), "normal");
}