use super::*;
use std::path::PathBuf;
fn stage_fmt_index(root: &Path) -> PathBuf {
let index = root.join("index");
assert_fs::fixture::ChildPath::new(index.join("config.json"))
.write_str("{\"schema\":1,\"kind\":\"file-registry\",\"packages\":\"packages\",\"artifacts\":\"artifacts\"}\n")
.unwrap();
let archive = index.join("artifacts/fmt/fmt-10.2.1.tar.gz");
let manifest = "[package]\nname = \"fmt\"\nversion = \"10.2.1\"\ncxx-standard = \"c++17\"\n\n[target.fmt]\ntype = \"library\"\nsources = [\"src/fmt.cc\"]\ninclude-dirs = [\"include\"]\n";
let header = "#pragma once\nint fmt_value();\n";
let body = "#include \"fmt.h\"\nint fmt_value() { return 42; }\n";
let checksum = make_archive(
&archive,
&[
("cabin.toml", manifest),
("include/fmt.h", header),
("src/fmt.cc", body),
],
);
let entry = format!(
"{{\n \"schema\": 1,\n \"name\": \"fmt\",\n \"versions\": {{\n \"10.2.1\": {{\n \"dependencies\": {{}},\n \"yanked\": false,\n \"checksum\": \"sha256:{checksum}\",\n \"source\": {{\"type\": \"archive\", \"path\": \"../artifacts/fmt/fmt-10.2.1.tar.gz\", \"format\": \"tar.gz\"}}\n }}\n }}\n}}\n",
);
assert_fs::fixture::ChildPath::new(index.join("packages/fmt.json"))
.write_str(&entry)
.unwrap();
index
}
fn stage_consumer_project(root: &Path) {
assert_fs::fixture::ChildPath::new(root.join("cabin.toml"))
.write_str(
r#"[package]
name = "app"
version = "0.1.0"
cxx-standard = "c++17"
[dependencies]
fmt = "10.2.1"
[target.app]
type = "executable"
sources = ["src/main.cc"]
deps = ["fmt"]
"#,
)
.unwrap();
assert_fs::fixture::ChildPath::new(root.join("src/main.cc"))
.write_str("extern int fmt_value();\nint main() { return fmt_value() == 42 ? 0 : 1; }\n")
.unwrap();
}
#[test]
fn vendor_writes_deterministic_file_registry() {
let dir = TempDir::new().unwrap();
let index = stage_fmt_index(dir.path());
stage_consumer_project(&dir.path().join("proj"));
cabin()
.args(["vendor", "--manifest-path"])
.arg(dir.path().join("proj/cabin.toml"))
.arg("--vendor-dir")
.arg(dir.path().join("proj/vendor"))
.arg("--index-path")
.arg(&index)
.arg("--cache-dir")
.arg(dir.path().join("cache"))
.assert()
.success();
let vendor = dir.path().join("proj/vendor");
assert!(vendor.join("config.json").is_file());
assert!(vendor.join("packages/fmt.json").is_file());
assert!(vendor.join("artifacts/fmt/fmt-10.2.1.tar.gz").is_file());
assert!(vendor.join("cabin-vendor.json").is_file());
let body = fs::read_to_string(vendor.join("packages/fmt.json")).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&body).unwrap();
let path = parsed["versions"]["10.2.1"]["source"]["path"]
.as_str()
.unwrap();
assert_eq!(path, "../artifacts/fmt/fmt-10.2.1.tar.gz");
let summary = fs::read(vendor.join("cabin-vendor.json")).unwrap();
cabin()
.args(["vendor", "--manifest-path"])
.arg(dir.path().join("proj/cabin.toml"))
.arg("--vendor-dir")
.arg(&vendor)
.arg("--index-path")
.arg(&index)
.arg("--cache-dir")
.arg(dir.path().join("cache"))
.assert()
.success();
let summary_again = fs::read(vendor.join("cabin-vendor.json")).unwrap();
assert_eq!(summary, summary_again);
}
#[test]
fn vendor_then_offline_build_links_against_the_vendored_dependency() {
require_cxx_build_tools();
let dir = TempDir::new().unwrap();
let index = stage_fmt_index(dir.path());
stage_consumer_project(&dir.path().join("proj"));
cabin()
.args(["vendor", "--manifest-path"])
.arg(dir.path().join("proj/cabin.toml"))
.arg("--vendor-dir")
.arg(dir.path().join("proj/vendor"))
.arg("--index-path")
.arg(&index)
.arg("--cache-dir")
.arg(dir.path().join("cache"))
.assert()
.success();
fs::remove_dir_all(&index).unwrap();
cabin()
.args(["build", "--offline", "--manifest-path"])
.arg(dir.path().join("proj/cabin.toml"))
.arg("--build-dir")
.arg(dir.path().join("proj/build"))
.arg("--index-path")
.arg(dir.path().join("proj/vendor"))
.arg("--cache-dir")
.arg(dir.path().join("vendor-cache"))
.assert()
.success();
let exe = dir
.path()
.join("proj/build/dev/packages/app")
.join(host_exe("app"));
assert!(exe.is_file(), "offline build must link the executable");
}
#[test]
fn offline_rejects_index_url() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str(
r#"[package]
name = "lone"
version = "0.1.0"
"#,
)
.unwrap();
let assertion = cabin()
.args(["build", "--offline", "--manifest-path"])
.arg(dir.path().join("cabin.toml"))
.arg("--index-url")
.arg("https://example.com/index")
.arg("--build-dir")
.arg(dir.path().join("build"))
.assert()
.failure();
let stderr = String::from_utf8_lossy(&assertion.get_output().stderr);
assert!(
stderr.contains("--offline forbids network access"),
"expected offline rejection, got: {stderr}"
);
assert!(
stderr.contains("https://example.com/index"),
"diagnostic should name the rejected URL, got: {stderr}"
);
}
#[test]
fn vendor_with_no_versioned_deps_writes_skeleton_only() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str(
r#"[package]
name = "lone"
version = "0.1.0"
cxx-standard = "c++17"
[target.lone]
type = "library"
sources = ["src/lib.cc"]
"#,
)
.unwrap();
dir.child("src/lib.cc")
.write_str("int lone_value() { return 0; }\n")
.unwrap();
let vendor = dir.path().join("vendor");
cabin()
.args(["vendor", "--manifest-path"])
.arg(dir.path().join("cabin.toml"))
.arg("--vendor-dir")
.arg(&vendor)
.assert()
.success();
assert!(vendor.join("config.json").is_file());
assert!(vendor.join("cabin-vendor.json").is_file());
assert!(vendor.join("packages").is_dir());
assert!(vendor.join("artifacts").is_dir());
}
#[test]
fn vendor_locked_succeeds_when_lockfile_is_current() {
let dir = TempDir::new().unwrap();
let index = stage_fmt_index(dir.path());
stage_consumer_project(&dir.path().join("proj"));
cabin()
.args(["vendor", "--manifest-path"])
.arg(dir.path().join("proj/cabin.toml"))
.arg("--vendor-dir")
.arg(dir.path().join("proj/vendor"))
.arg("--index-path")
.arg(&index)
.arg("--cache-dir")
.arg(dir.path().join("cache"))
.assert()
.success();
let lock_before = fs::read_to_string(dir.path().join("proj/cabin.lock")).unwrap();
cabin()
.args(["vendor", "--locked", "--manifest-path"])
.arg(dir.path().join("proj/cabin.toml"))
.arg("--vendor-dir")
.arg(dir.path().join("proj/vendor"))
.arg("--index-path")
.arg(&index)
.arg("--cache-dir")
.arg(dir.path().join("cache"))
.assert()
.success();
let lock_after = fs::read_to_string(dir.path().join("proj/cabin.lock")).unwrap();
assert_eq!(lock_before, lock_after);
}