use std::path::{Path, PathBuf};
use std::process::Command;
use chtypes::fetch::{
self, Action, EnsureOptions, LockFile, RELEASE_PUBLIC_KEY_HEX, lock_key, sha256_file,
};
use chtypes::{Error, Registry, RegistryOptions};
use serde_json::Value as Json;
fn foreign() -> &'static str {
if chtypes::host_platform() == "linux-amd64" {
"darwin-arm64"
} else {
"linux-amd64"
}
}
fn announce(message: &str) {
use std::io::Write;
let _ = std::io::stderr().write_all(message.as_bytes());
let _ = std::io::stderr().flush();
}
fn fixtures_dir() -> Option<PathBuf> {
let dir = std::env::var_os("CHTYPES_FETCH_FIXTURES")
.map(PathBuf::from)
.unwrap_or_else(|| {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../tests/fixtures/fetch")
});
let dir = dir.canonicalize().unwrap_or(dir);
if !dir.join("expected.json").is_file() {
announce(&format!(
"\nSKIP: no fetch fixtures at {} (tests/fixtures/fetch, generated by the core \
repository; set $CHTYPES_FETCH_FIXTURES). This test is skipped.\n",
dir.display()
));
return None;
}
let fp = foreign();
if chtypes::cache_dir_for(fp).is_dir()
|| chtypes::SYSTEM_ARTIFACT_ROOTS
.iter()
.any(|r| Path::new(r).join(fp).is_dir())
{
announce(&format!(
"\nSKIP: this host has a {fp} artifact directory on the §1 search path, which \
would shadow the fixture installs. This test is skipped.\n"
));
return None;
}
Some(dir)
}
macro_rules! fixtures {
() => {
match fixtures_dir() {
Some(d) => d,
None => return,
}
};
}
fn expected(fx: &Path) -> Json {
let doc: Json = serde_json::from_slice(&std::fs::read(fx.join("expected.json")).unwrap())
.expect("expected.json parses");
assert_eq!(doc["schema"], 1, "this suite reads expected.json schema 1");
doc
}
fn test_key(fx: &Path) -> String {
std::fs::read_to_string(fx.join("test-key/public.hex"))
.expect("test-key/public.hex")
.trim()
.to_string()
}
fn file_url(fx: &Path, fixture: &str) -> String {
format!("file://{}", fx.join(fixture).display())
}
fn tmp(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("chtypes-rs-fetch-{}-{name}", std::process::id()));
std::fs::remove_dir_all(&dir).ok();
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn opts(fx: &Path, fixture: &str, dest: &Path) -> EnsureOptions {
EnsureOptions {
dest: Some(dest.to_path_buf()),
platform: Some(foreign().into()),
url: Some(file_url(fx, fixture)),
trusted_keys: Some(vec![test_key(fx)]),
allow_unsigned: Some(false),
..Default::default()
}
}
fn index_row(index: &Json, platform: &str, minor: &str) -> Json {
let (os, arch) = platform.split_once('-').unwrap();
index["artifacts"]
.as_array()
.unwrap()
.iter()
.find(|r| r["os"] == os && r["arch"] == arch && r["clickhouse_minor"] == minor)
.cloned()
.unwrap_or_else(|| panic!("no index row for {platform}/{minor}"))
}
fn signed_index(fx: &Path) -> Json {
serde_json::from_slice(&std::fs::read(fx.join("signed/index.json")).unwrap()).unwrap()
}
#[test]
fn every_fixture_verdict_matches_expected_json_through_ensure() {
let fx = fixtures!();
let doc = expected(&fx);
let verdicts = doc["verdicts"].as_array().unwrap();
assert!(!verdicts.is_empty(), "expected.json lists no verdicts");
let mut checked = 0;
for v in verdicts {
let fixture = v["fixture"].as_str().unwrap();
let dest = tmp(&format!("ensure-{fixture}-{}", checked));
let mut o = opts(&fx, fixture, &dest);
o.trusted_keys = Some(vec![match v["trusted_keys"].as_str().unwrap() {
"test" => test_key(&fx),
"release" => RELEASE_PUBLIC_KEY_HEX.to_string(),
other => panic!("unknown trusted_keys spelling {other:?}"),
}]);
o.allow_unsigned = Some(v["allow_unsigned"].as_bool().unwrap());
let ctx = format!("{fixture} ({})", v["why"]);
match (fetch::ensure("25.8", &o), v["code"].as_str()) {
(Ok(installed), None) => {
assert_eq!(installed.action, Action::Installed, "{ctx}");
assert_eq!(installed.line, "25.8", "{ctx}");
assert_eq!(installed.dir, dest.join("25.8"), "{ctx}");
let row = index_row(&signed_index(&fx), foreign(), "25.8");
assert_eq!(installed.version, row["clickhouse_version"], "{ctx}");
assert_eq!(
installed.library,
dest.join("25.8").join(row["library"].as_str().unwrap()),
"{ctx}"
);
assert_eq!(
sha256_file(&installed.library).unwrap(),
row["library_sha256"].as_str().unwrap(),
"{ctx}: the installed library must hash as the index says"
);
assert_eq!(installed.library_sha256, row["library_sha256"], "{ctx}");
assert_eq!(
installed
.asset
.as_ref()
.map(|(f, s)| (f.as_str(), s.as_str())),
Some((
row["file"].as_str().unwrap(),
row["sha256"].as_str().unwrap()
)),
"{ctx}"
);
for extra in ["manifest.json", "CH_VERSION", "unsafe_families.txt"] {
assert!(
dest.join("25.8").join(extra).is_file(),
"{ctx}: {extra} missing"
);
}
let leftovers: Vec<String> = std::fs::read_dir(&dest)
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n != "25.8")
.collect();
assert!(leftovers.is_empty(), "{ctx}: leftovers {leftovers:?}");
}
(Err(err), Some(code)) => {
assert_eq!(err.artifact_code(), Some(code), "{ctx}: got {err}");
assert!(
!dest.join("25.8").exists(),
"{ctx}: a refused fetch must install nothing"
);
assert!(
std::fs::read_dir(&dest).map(|d| d.count()).unwrap_or(0) == 0,
"{ctx}: a refused fetch must leave nothing behind"
);
}
(Ok(_), Some(code)) => panic!("{ctx}: installed, but the spec says {code}"),
(Err(err), None) => panic!("{ctx}: the spec says installs, got {err}"),
}
checked += 1;
std::fs::remove_dir_all(&dest).ok();
}
assert_eq!(checked, verdicts.len());
}
#[test]
fn installed_and_verified_downloads_nothing_and_offline_reads_no_source() {
let fx = fixtures!();
let dest = tmp("idempotent");
let first = fetch::ensure("25.8", &opts(&fx, "signed", &dest)).unwrap();
assert_eq!(first.action, Action::Installed);
let again = fetch::ensure("25.8", &opts(&fx, "signed", &dest)).unwrap();
assert_eq!(again.action, Action::AlreadyInstalled);
assert_eq!(again.dir, first.dir);
assert_eq!(again.library_sha256, first.library_sha256);
assert!(again.asset.is_some(), "the release was consulted");
let dead = EnsureOptions {
url: Some("http://127.0.0.1:1/never".into()),
..opts(&fx, "signed", &dest)
};
let err = fetch::ensure("25.8", &dead).unwrap_err();
assert_eq!(
err.artifact_code(),
Some("CHTYPES_SOURCE_UNREACHABLE"),
"{err}"
);
let offline = EnsureOptions {
offline: true,
..dead.clone()
};
let local = fetch::ensure("25.8", &offline).unwrap();
assert_eq!(local.action, Action::AlreadyInstalled);
assert_eq!(local.asset, None, "no release was consulted");
assert_eq!(
fetch::ensure("25.8.28.1-lts", &offline).unwrap().action,
Action::AlreadyInstalled
);
let other = fetch::ensure("25.8.30.16-lts", &offline).unwrap_err();
assert_eq!(
other.artifact_code(),
Some("CHTYPES_SOURCE_UNREACHABLE"),
"{other}"
);
let forced = EnsureOptions {
force: true,
..dead.clone()
};
let err = fetch::ensure("25.8", &forced).unwrap_err();
assert_eq!(
err.artifact_code(),
Some("CHTYPES_SOURCE_UNREACHABLE"),
"{err}"
);
assert!(
err.to_string().contains("127.0.0.1:1"),
"the message names the source: {err}"
);
assert_eq!(sha256_file(&first.library).unwrap(), first.library_sha256);
let forced_ok = EnsureOptions {
force: true,
..opts(&fx, "signed", &dest)
};
assert_eq!(
fetch::ensure("25.8", &forced_ok).unwrap().action,
Action::Replaced
);
std::fs::remove_dir_all(&dest).ok();
}
#[test]
fn a_corrupt_install_is_replaced_or_reported_never_trusted() {
let fx = fixtures!();
let dest = tmp("corrupt");
let first = fetch::ensure("25.8", &opts(&fx, "signed", &dest)).unwrap();
std::fs::write(&first.library, b"not the library any more").unwrap();
let offline = EnsureOptions {
offline: true,
..opts(&fx, "signed", &dest)
};
let err = fetch::ensure("25.8", &offline).unwrap_err();
assert_eq!(
err.artifact_code(),
Some("CHTYPES_ARTIFACT_CORRUPT"),
"{err}"
);
let bad: Vec<_> = fetch::verify_installed(&dest)
.into_iter()
.filter(|v| v.result.is_err())
.map(|v| v.line)
.collect();
assert_eq!(
bad,
vec!["25.8".to_string()],
"verify must name the corrupt line"
);
let replaced = fetch::ensure("25.8", &opts(&fx, "signed", &dest)).unwrap();
assert_eq!(replaced.action, Action::Replaced);
assert_eq!(
sha256_file(&replaced.library).unwrap(),
first.library_sha256
);
assert!(
fetch::verify_installed(&dest)
.iter()
.all(|v| v.result.is_ok())
);
std::fs::remove_dir_all(&dest).ok();
}
#[test]
fn unpublished_lines_platforms_and_patches_are_refused() {
let fx = fixtures!();
let doc = expected(&fx);
let u = &doc["unpublished"];
let code = u["code"].as_str().unwrap();
let dest = tmp("unpublished");
let base = opts(&fx, "signed", &dest);
for (what, o) in [
("line", fetch::ensure(u["line"].as_str().unwrap(), &base)),
("patch", fetch::ensure(u["patch"].as_str().unwrap(), &base)),
(
"platform",
fetch::ensure(
"25.8",
&EnsureOptions {
platform: Some(u["platform"].as_str().unwrap().into()),
..base.clone()
},
),
),
] {
let err = o.unwrap_err();
assert_eq!(err.artifact_code(), Some(code), "{what}: {err}");
assert!(
matches!(err, Error::ArtifactUnpublished { .. }),
"{what}: {err:?}"
);
}
let row = index_row(&signed_index(&fx), foreign(), "25.8");
let exact = row["clickhouse_version"].as_str().unwrap();
assert_eq!(fetch::ensure(exact, &base).unwrap().version, exact);
let bare = exact.rsplit_once('-').map(|(b, _)| b).unwrap_or(exact);
assert_eq!(fetch::ensure(bare, &base).unwrap().version, exact);
assert_eq!(
fetch::ensure(&format!("v{exact}"), &base).unwrap().version,
exact
);
assert!(fetch::ensure("latest", &base).is_err());
std::fs::remove_dir_all(&dest).ok();
}
#[test]
fn the_lock_file_records_and_frozen_refuses_drift() {
let fx = fixtures!();
let dest = tmp("lock");
let lock_path = dest.join("new.lock");
let o = EnsureOptions {
lock: Some(lock_path.clone()),
..opts(&fx, "signed", &dest)
};
let installed = fetch::ensure("25.8", &o).unwrap();
let lock = LockFile::load(&lock_path, true).unwrap();
let key = lock_key(foreign(), "25.8");
let (file, sha) = installed.asset.clone().unwrap();
assert_eq!(lock.artifacts[&key].file, file);
assert_eq!(lock.artifacts[&key].sha256, sha);
assert_eq!(lock.schema, 1);
let spec_lock = LockFile::load(&fx.join("chtypes.lock"), true).unwrap();
assert_eq!(spec_lock.artifacts[&key], lock.artifacts[&key]);
let frozen = EnsureOptions {
lock: Some(fx.join("chtypes.lock")),
frozen: true,
..opts(&fx, "signed", &dest)
};
assert_eq!(
fetch::ensure("25.8", &frozen).unwrap().action,
Action::AlreadyInstalled
);
let fresh = tmp("lock-fresh");
let frozen_fresh = EnsureOptions {
dest: Some(fresh.clone()),
..frozen.clone()
};
assert_eq!(
fetch::ensure("26.7", &frozen_fresh).unwrap().action,
Action::Installed
);
let drifted = dest.join("drift.lock");
let text = std::fs::read_to_string(fx.join("chtypes.lock")).unwrap();
assert!(text.contains(&sha), "the spec lock pins this row");
std::fs::write(&drifted, text.replace(&sha, &"0".repeat(64))).unwrap();
let scratch = tmp("lock-drift");
let err = fetch::ensure(
"25.8",
&EnsureOptions {
dest: Some(scratch.clone()),
lock: Some(drifted.clone()),
frozen: true,
..opts(&fx, "signed", &dest)
},
)
.unwrap_err();
assert_eq!(
err.artifact_code(),
Some("CHTYPES_ARTIFACT_PINNED"),
"{err}"
);
assert!(!scratch.join("25.8").exists());
let sparse = dest.join("sparse.lock");
std::fs::write(&sparse, r#"{"schema":1,"artifacts":{}}"#).unwrap();
let err = fetch::ensure(
"25.8",
&EnsureOptions {
lock: Some(sparse),
frozen: true,
..opts(&fx, "signed", &dest)
},
)
.unwrap_err();
assert_eq!(
err.artifact_code(),
Some("CHTYPES_ARTIFACT_PINNED"),
"{err}"
);
let err = fetch::ensure(
"25.8",
&EnsureOptions {
lock: Some(dest.join("absent.lock")),
frozen: true,
..opts(&fx, "signed", &dest)
},
)
.unwrap_err();
assert_eq!(
err.artifact_code(),
Some("CHTYPES_ARTIFACT_PINNED"),
"no lock file pins nothing: {err}"
);
for d in [dest, fresh, scratch] {
std::fs::remove_dir_all(&d).ok();
}
}
#[test]
fn offline_is_source_unreachable_without_touching_the_network() {
let dest = tmp("offline");
for url in [Some("http://127.0.0.1:1/never".to_string()), None] {
let err = fetch::ensure(
"25.8",
&EnsureOptions {
dest: Some(dest.clone()),
platform: Some(foreign().into()),
url,
offline: true,
..Default::default()
},
)
.unwrap_err();
assert_eq!(
err.artifact_code(),
Some("CHTYPES_SOURCE_UNREACHABLE"),
"{err}"
);
assert!(matches!(err, Error::SourceUnreachable { .. }));
assert!(err.to_string().contains("offline"), "{err}");
assert!(!err.to_string().contains("refused"), "{err}");
}
std::fs::remove_dir_all(&dest).ok();
}
#[test]
fn ensure_all_installs_every_published_line() {
let fx = fixtures!();
let doc = expected(&fx);
let dest = tmp("all");
let installed = fetch::ensure_all(&opts(&fx, "signed", &dest)).unwrap();
let mut lines: Vec<String> = installed.iter().map(|i| i.line.clone()).collect();
lines.sort();
let mut want: Vec<String> = doc["lines"].as_object().unwrap().keys().cloned().collect();
want.sort();
assert_eq!(lines, want);
for i in &installed {
assert_eq!(i.action, Action::Installed);
assert_eq!(i.version, doc["lines"][&i.line]);
assert_eq!(sha256_file(&i.library).unwrap(), i.library_sha256);
}
let again = fetch::ensure_all(&opts(&fx, "signed", &dest)).unwrap();
assert!(again.iter().all(|i| i.action == Action::AlreadyInstalled));
let info = fetch::release_info(&opts(&fx, "signed", &dest)).unwrap();
assert_eq!(info.signed_by.as_deref(), doc["key_id"].as_str());
assert_eq!(info.license, "Elastic-2.0");
assert_eq!(
info.artifacts.len(),
signed_index(&fx)["artifacts"].as_array().unwrap().len()
);
std::fs::remove_dir_all(&dest).ok();
}
#[test]
fn a_search_path_registry_reports_a_missing_line_with_the_spec_message() {
let dest = tmp("missing");
let reg = Registry::from_search_path_with(RegistryOptions {
dir: Some(dest.clone()),
autofetch: Some(false),
..Default::default()
});
assert!(!reg.autofetch());
assert_eq!(reg.search_path()[0], dest);
assert_eq!(reg.dir(), dest, "fetch writes to the explicit directory");
assert!(
reg.libraries().is_empty(),
"nothing is loaded until asked for"
);
let err = reg.for_version("99.9").unwrap_err();
let Error::ArtifactMissing {
line,
platform,
looked_in,
} = &err
else {
panic!("expected ArtifactMissing, got {err:?}");
};
assert_eq!(line, "99.9");
assert_eq!(platform, &chtypes::host_platform());
assert_eq!(looked_in, reg.search_path());
assert_eq!(err.artifact_code(), Some("CHTYPES_ARTIFACT_MISSING"));
let dirs: Vec<String> = looked_in.iter().map(|d| d.display().to_string()).collect();
assert_eq!(
err.to_string(),
format!(
"chtypes: no artifact for ClickHouse 99.9 ({platform}). Looked in: {}.\n\
Install it: cargo install chtypes && chtypes fetch 99.9\n\
or set CHTYPES_AUTOFETCH=1 to fetch on first use.",
dirs.join(", ")
)
);
let err = reg.for_version("99.9.1.1-lts").unwrap_err();
assert!(matches!(err, Error::ArtifactMissing { ref line, .. } if line == "99.9"));
std::fs::remove_dir_all(&dest).ok();
}
#[test]
fn the_search_path_is_the_spec_s_order() {
let explicit = PathBuf::from("/explicit/reg");
let path = chtypes::registry_search_path(Some(&explicit));
let host = chtypes::host_platform();
assert_eq!(path[0], explicit);
let tail: Vec<PathBuf> = path[path.len() - 2..].to_vec();
assert_eq!(
tail,
vec![
PathBuf::from("/usr/local/share/chtypes/artifacts").join(&host),
PathBuf::from("/opt/chtypes/artifacts").join(&host),
]
);
assert!(path.contains(&chtypes::default_registry_dir()));
assert_eq!(chtypes::install_dir(Some(&explicit)), explicit);
let fpath = chtypes::search_path_for(foreign(), None);
assert_eq!(fpath[0], chtypes::cache_dir_for(foreign()));
assert_eq!(
chtypes::install_dir_for(foreign(), None),
chtypes::cache_dir_for(foreign())
);
assert!(fpath.iter().all(|p| p.ends_with(foreign())));
}
fn bin() -> Command {
let mut c = Command::new(env!("CARGO_BIN_EXE_chtypes"));
c.env_remove("CHTYPES_REGISTRY")
.env_remove("CHTYPES_TRUSTED_KEYS")
.env_remove("CHTYPES_ALLOW_UNSIGNED")
.env_remove("CHTYPES_ARTIFACTS_URL")
.env_remove("CHTYPES_AUTOFETCH")
.env_remove("CHTYPES_DOWNLOAD_TOKEN");
c
}
struct Run {
code: i32,
stdout: String,
stderr: String,
}
fn run(mut c: Command) -> Run {
let out = c.output().expect("the chtypes binary runs");
Run {
code: out.status.code().expect("an exit code, not a signal"),
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
}
}
#[test]
fn the_binary_matches_every_verdict_and_exit_code() {
let fx = fixtures!();
let doc = expected(&fx);
let cache = tmp("bin-cache");
let platform = doc["platforms"][0].as_str().unwrap();
let mut checked = 0;
for v in doc["verdicts"].as_array().unwrap() {
let fixture = v["fixture"].as_str().unwrap();
let dest = tmp(&format!("bin-{fixture}-{checked}"));
let mut c = bin();
c.args(["fetch", "25.8", "--platform", platform, "--dest"])
.arg(&dest)
.arg("--url")
.arg(file_url(&fx, fixture))
.env("XDG_CACHE_HOME", &cache);
match v["trusted_keys"].as_str().unwrap() {
"test" => {
c.env("CHTYPES_TRUSTED_KEYS", test_key(&fx));
}
"release" => {} other => panic!("unknown trusted_keys spelling {other:?}"),
}
if v["allow_unsigned"].as_bool().unwrap() {
c.env("CHTYPES_ALLOW_UNSIGNED", "1");
}
let r = run(c);
let ctx = format!("{fixture} ({}): stderr:\n{}", v["why"], r.stderr);
assert_eq!(r.code, v["exit"].as_i64().unwrap() as i32, "{ctx}");
match v["code"].as_str() {
None => {
assert_eq!(
r.stdout.trim_end(),
dest.join("25.8").display().to_string(),
"{ctx}: stdout is the installed directory alone"
);
let row = index_row(&signed_index(&fx), platform, "25.8");
assert_eq!(
sha256_file(&dest.join("25.8").join(row["library"].as_str().unwrap())).unwrap(),
row["library_sha256"].as_str().unwrap(),
"{ctx}"
);
if v["allow_unsigned"].as_bool().unwrap() {
let warnings: Vec<&str> =
r.stderr.lines().filter(|l| l.contains("WARNING")).collect();
assert_eq!(warnings.len(), 1, "{ctx}: exactly one loud warning");
assert!(
warnings[0].contains(&file_url(&fx, fixture)),
"{ctx}: the warning names the source"
);
} else {
assert!(!r.stderr.contains("WARNING"), "{ctx}");
assert!(
r.stderr.contains(doc["key_id"].as_str().unwrap()),
"{ctx}: progress names the key that verified"
);
}
}
Some(code) => {
assert!(
r.stdout.is_empty(),
"{ctx}: nothing on stdout: {:?}",
r.stdout
);
assert!(r.stderr.contains(code), "{ctx}: stderr carries {code}");
assert!(!dest.join("25.8").exists(), "{ctx}: nothing installed");
}
}
checked += 1;
std::fs::remove_dir_all(&dest).ok();
}
assert_eq!(checked, doc["verdicts"].as_array().unwrap().len());
std::fs::remove_dir_all(&cache).ok();
}
#[test]
fn the_binary_s_other_exit_codes_and_commands() {
let fx = fixtures!();
let doc = expected(&fx);
let cache = tmp("bin2-cache");
let dest = tmp("bin2-dest");
let platform = doc["platforms"][0].as_str().unwrap();
let key = test_key(&fx);
let signed = file_url(&fx, "signed");
let u = &doc["unpublished"];
for args in [
vec!["fetch", u["line"].as_str().unwrap(), "--platform", platform],
vec![
"fetch",
u["patch"].as_str().unwrap(),
"--platform",
platform,
],
vec![
"fetch",
"25.8",
"--platform",
u["platform"].as_str().unwrap(),
],
] {
let mut c = bin();
c.args(&args)
.arg("--dest")
.arg(&dest)
.arg("--url")
.arg(&signed);
c.env("CHTYPES_TRUSTED_KEYS", &key)
.env("XDG_CACHE_HOME", &cache);
let r = run(c);
assert_eq!(
r.code,
u["exit"].as_i64().unwrap() as i32,
"{args:?}: {}",
r.stderr
);
assert!(
r.stderr.contains(u["code"].as_str().unwrap()),
"{args:?}: {}",
r.stderr
);
assert!(r.stdout.is_empty());
}
for extra in [vec!["--offline"], vec!["--url", "http://127.0.0.1:1/never"]] {
let mut c = bin();
c.args(["fetch", "25.8", "--platform", platform, "--dest"])
.arg(&dest)
.args(&extra)
.env("XDG_CACHE_HOME", &cache);
let r = run(c);
assert_eq!(r.code, 3, "{extra:?}: {}", r.stderr);
assert!(
r.stderr.contains("CHTYPES_SOURCE_UNREACHABLE"),
"{}",
r.stderr
);
}
for args in [
vec![],
vec!["fetch"],
vec!["fetch", "latest"],
vec!["fetch", "25.8", "--bogus"],
vec!["fetch", "25.8", "--tag", "v1", "--url", "x"],
vec!["fetch", "25.8", "--all"],
vec!["fetch", "25.8", "--platform", "plan9-mips"],
vec!["fetch", "25.8", "--dest"],
vec!["frobnicate"],
] {
let mut c = bin();
c.args(&args).env("XDG_CACHE_HOME", &cache);
let r = run(c);
assert_eq!(r.code, 2, "{args:?}: {}", r.stderr);
assert!(r.stdout.is_empty(), "{args:?}");
}
let mut c = bin();
c.args(["fetch", "25.8", "nope", "--platform", platform, "--dest"])
.arg(&dest)
.arg("--url")
.arg(&signed)
.env("CHTYPES_TRUSTED_KEYS", &key)
.env("XDG_CACHE_HOME", &cache);
let r = run(c);
assert_eq!(r.code, 2, "{}", r.stderr);
assert!(
!dest.join("25.8").exists(),
"nothing was fetched before the usage error"
);
let first = |s: String| s.lines().next().unwrap_or_default().to_string();
let mut c = bin();
c.args(["where", "--dest"]).arg(&dest);
assert_eq!(first(run(c).stdout), dest.display().to_string());
let mut c = bin();
c.arg("where")
.env("CHTYPES_REGISTRY", "/some/registry")
.env("XDG_CACHE_HOME", &cache);
assert_eq!(first(run(c).stdout), "/some/registry");
let mut c = bin();
c.arg("where").env("XDG_CACHE_HOME", &cache);
assert_eq!(
first(run(c).stdout),
cache
.join("chtypes/artifacts")
.join(chtypes::host_platform())
.display()
.to_string()
);
let mut c = bin();
c.args(["fetch", "26.7", "--platform", platform, "--url"])
.arg(&signed)
.env("CHTYPES_TRUSTED_KEYS", &key)
.env("XDG_CACHE_HOME", &cache);
let r = run(c);
assert_eq!(r.code, 0, "{}", r.stderr);
let in_cache = cache.join("chtypes/artifacts").join(platform).join("26.7");
assert_eq!(r.stdout.trim_end(), in_cache.display().to_string());
assert!(in_cache.join("manifest.json").is_file());
let mut c = bin();
c.args(["fetch", "26.7", "--platform", platform, "--url"])
.arg(&signed)
.env("CHTYPES_TRUSTED_KEYS", &key)
.env("XDG_CACHE_HOME", &cache);
let r = run(c);
assert_eq!(
r.code, 0,
"installed in the cache satisfies a plain fetch: {}",
r.stderr
);
assert!(r.stderr.contains("already installed"), "{}", r.stderr);
let mut c = bin();
c.args([
"fetch",
"26.7",
"--platform",
platform,
"--offline",
"--url",
])
.arg("http://127.0.0.1:1/never")
.env("XDG_CACHE_HOME", &cache);
let r = run(c);
assert_eq!(r.code, 0, "offline, installed in the cache: {}", r.stderr);
assert!(r.stderr.contains("already installed"), "{}", r.stderr);
let elsewhere = tmp("bin2-elsewhere");
let mut c = bin();
c.args(["fetch", "26.7", "--platform", platform, "--dest"])
.arg(&elsewhere)
.arg("--url")
.arg(&signed)
.env("CHTYPES_TRUSTED_KEYS", &key)
.env("XDG_CACHE_HOME", &cache);
let r = run(c);
assert_eq!(r.code, 0, "{}", r.stderr);
assert_eq!(
r.stdout.trim_end(),
elsewhere.join("26.7").display().to_string(),
"--dest is where the line must be, whatever the cache holds"
);
assert!(elsewhere.join("26.7/manifest.json").is_file());
assert!(!r.stderr.contains("already installed"), "{}", r.stderr);
std::fs::remove_dir_all(&elsewhere).ok();
let lock = dest.join("chtypes.lock");
let mut c = bin();
c.args(["fetch", "25.8", "26.7", "--platform", platform, "--dest"])
.arg(&dest)
.arg("--url")
.arg(&signed)
.arg("--lock")
.arg(&lock)
.env("CHTYPES_TRUSTED_KEYS", &key)
.env("XDG_CACHE_HOME", &cache);
let r = run(c);
assert_eq!(r.code, 0, "{}", r.stderr);
assert_eq!(
r.stdout.lines().collect::<Vec<_>>(),
vec![
dest.join("25.8").display().to_string(),
dest.join("26.7").display().to_string()
]
);
let written = LockFile::load(&lock, true).unwrap();
let spec_lock = LockFile::load(&fx.join("chtypes.lock"), true).unwrap();
for minor in ["25.8", "26.7"] {
let k = lock_key(platform, minor);
assert_eq!(written.artifacts[&k], spec_lock.artifacts[&k], "{k}");
}
let mut c = bin();
c.args(["fetch", "--all", "--platform", platform, "--dest"])
.arg(&dest)
.arg("--url")
.arg(&signed)
.arg("--lock")
.arg(fx.join("chtypes.lock"))
.arg("--frozen")
.env("CHTYPES_TRUSTED_KEYS", &key)
.env("XDG_CACHE_HOME", &cache);
let r = run(c);
assert_eq!(r.code, 0, "{}", r.stderr);
assert_eq!(r.stdout.lines().count(), 2);
let drifted = dest.join("drift.lock");
let text = std::fs::read_to_string(fx.join("chtypes.lock")).unwrap();
let row = index_row(&signed_index(&fx), platform, "25.8");
std::fs::write(
&drifted,
text.replace(row["sha256"].as_str().unwrap(), &"0".repeat(64)),
)
.unwrap();
let scratch = tmp("bin2-drift");
let mut c = bin();
c.args(["fetch", "25.8", "--platform", platform, "--dest"])
.arg(&scratch)
.arg("--url")
.arg(&signed)
.arg("--lock")
.arg(&drifted)
.arg("--frozen")
.env("CHTYPES_TRUSTED_KEYS", &key)
.env("XDG_CACHE_HOME", &cache);
let r = run(c);
assert_eq!(r.code, 1, "{}", r.stderr);
assert!(r.stderr.contains("CHTYPES_ARTIFACT_PINNED"), "{}", r.stderr);
assert!(r.stdout.is_empty());
let mut c = bin();
c.args(["verify", "--platform", platform, "--dest"])
.arg(&dest)
.env("XDG_CACHE_HOME", &cache);
let r = run(c);
assert_eq!(r.code, 0, "{}", r.stderr);
assert_eq!(
r.stdout.lines().filter(|l| l.starts_with("ok ")).count(),
2,
"{}",
r.stdout
);
std::fs::write(
dest.join("26.7").join(row["library"].as_str().unwrap()),
b"flipped",
)
.unwrap();
let mut c = bin();
c.args(["verify", "--platform", platform, "--dest"])
.arg(&dest)
.env("XDG_CACHE_HOME", &cache);
let r = run(c);
assert_eq!(r.code, 1, "{}", r.stderr);
assert_eq!(
r.stdout
.lines()
.filter(|l| l.starts_with("CORRUPT "))
.count(),
1,
"{}",
r.stdout
);
assert!(r.stdout.contains("26.7"));
let mut c = bin();
c.args(["list", "--platform", platform, "--dest"])
.arg(&dest)
.arg("--url")
.arg(&signed)
.env("CHTYPES_TRUSTED_KEYS", &key)
.env("XDG_CACHE_HOME", &cache);
let r = run(c);
assert_eq!(r.code, 0, "{}", r.stderr);
assert!(r.stdout.contains("installed"), "{}", r.stdout);
assert!(
r.stdout.contains(doc["key_id"].as_str().unwrap()),
"{}",
r.stdout
);
assert!(r.stdout.contains("(installed)"), "{}", r.stdout);
let mut c = bin();
c.args(["list", "--platform", platform, "--dest"])
.arg(&dest)
.arg("--url")
.arg(file_url(&fx, "bad-signature"))
.env("CHTYPES_TRUSTED_KEYS", &key)
.env("XDG_CACHE_HOME", &cache);
let r = run(c);
assert_eq!(r.code, 1, "{}", r.stderr);
assert!(
r.stderr.contains("CHTYPES_ARTIFACT_UNTRUSTED"),
"{}",
r.stderr
);
for d in [cache, dest, scratch] {
std::fs::remove_dir_all(&d).ok();
}
}
fn rerun(inner: &str, envs: &[(&str, &str)]) -> Run {
let mut c = Command::new(std::env::current_exe().unwrap());
c.args(["--exact", inner, "--nocapture", "--test-threads=1"]);
c.env_remove("CHTYPES_REGISTRY")
.env_remove("CHTYPES_TRUSTED_KEYS")
.env_remove("CHTYPES_ALLOW_UNSIGNED")
.env_remove("CHTYPES_ARTIFACTS_URL")
.env_remove("CHTYPES_AUTOFETCH")
.env("CHTYPES_TEST_INNER", "1");
for (k, v) in envs {
c.env(k, v);
}
run(c)
}
fn inner() -> bool {
std::env::var_os("CHTYPES_TEST_INNER").is_some()
}
#[test]
fn inner_autofetch_runs_ensure_once_per_line() {
if !inner() {
return;
}
let fx = PathBuf::from(std::env::var("CHTYPES_FETCH_FIXTURES").unwrap());
let dest = PathBuf::from(std::env::var("CHTYPES_TEST_DEST").unwrap());
let key = test_key(&fx);
let reg = Registry::from_search_path_with(RegistryOptions {
dir: Some(dest.clone()),
..Default::default()
});
assert!(reg.autofetch(), "CHTYPES_AUTOFETCH=1 turns it on");
assert!(!dest.join("25.8").exists());
let err = reg.for_version("25.8").unwrap_err();
assert!(
matches!(err, Error::Load { .. }),
"the fixture was fetched and then failed to dlopen (it is text): {err:?}"
);
assert!(
dest.join("25.8/manifest.json").is_file(),
"autofetch installed the line"
);
let installed_at = std::fs::metadata(dest.join("25.8/manifest.json"))
.unwrap()
.modified()
.unwrap();
let err = reg.for_version("25.8").unwrap_err();
assert!(matches!(err, Error::Load { .. }), "{err:?}");
assert_eq!(
std::fs::metadata(dest.join("25.8/manifest.json"))
.unwrap()
.modified()
.unwrap(),
installed_at,
"no re-fetch of an installed line"
);
let reg = Registry::from_search_path_with(RegistryOptions {
dir: Some(dest.clone()),
autofetch: Some(true),
fetch: EnsureOptions {
url: Some(file_url(&fx, "unsigned")),
trusted_keys: Some(vec![key.clone()]),
allow_unsigned: Some(false),
..Default::default()
},
..Default::default()
});
let err = reg.for_version("26.7").unwrap_err();
assert_eq!(
err.artifact_code(),
Some("CHTYPES_ARTIFACT_UNTRUSTED"),
"{err}"
);
assert!(!dest.join("26.7").exists());
let err = reg.for_version("26.7").unwrap_err();
assert_eq!(
err.artifact_code(),
Some("CHTYPES_ARTIFACT_MISSING"),
"once per process per line: the second open does not fetch again: {err}"
);
let reg = Registry::from_search_path_with(RegistryOptions {
dir: Some(dest.clone()),
autofetch: Some(false),
..Default::default()
});
let err = reg.for_version("99.9").unwrap_err();
assert_eq!(err.artifact_code(), Some("CHTYPES_ARTIFACT_MISSING"));
println!("INNER-OK autofetch");
}
#[test]
fn autofetch_runs_ensure_once_per_line() {
let fx = fixtures!();
let cache = tmp("autofetch-cache");
let dest = tmp("autofetch-dest");
let host = tmp("autofetch-host");
std::os::unix::fs::symlink(fx.join("signed"), host.join("artifacts")).unwrap();
let r = rerun(
"inner_autofetch_runs_ensure_once_per_line",
&[
("XDG_CACHE_HOME", cache.to_str().unwrap()),
("CHTYPES_AUTOFETCH", "1"),
(
"CHTYPES_ARTIFACTS_URL",
&format!("file://{}", host.display()),
),
("CHTYPES_TRUSTED_KEYS", &test_key(&fx)),
("CHTYPES_FETCH_FIXTURES", fx.to_str().unwrap()),
("CHTYPES_TEST_DEST", dest.to_str().unwrap()),
],
);
assert_eq!(r.code, 0, "inner run failed:\n{}\n{}", r.stdout, r.stderr);
assert!(r.stdout.contains("INNER-OK autofetch"), "{}", r.stdout);
assert!(
r.stdout.contains("test result: ok. 1 passed"),
"{}",
r.stdout
);
for d in [cache, dest, host] {
std::fs::remove_dir_all(&d).ok();
}
}
#[test]
fn a_search_path_registry_opens_one_line_lazily() {
let dir = match std::env::var_os(chtypes::REGISTRY_ENV) {
Some(d) => PathBuf::from(d),
None => chtypes::default_registry_dir(),
};
let lines = chtypes::installed_lines(std::slice::from_ref(&dir));
let Some((line, _)) = lines.first() else {
announce(&format!(
"\nSKIP a_search_path_registry_opens_one_line_lazily: no installed artifact under {} \
— fetch one with scripts/fetch.sh 25.8 (docs/guides/fetch.md)\n",
dir.display()
));
return;
};
let reg = Registry::from_search_path_with(RegistryOptions {
dir: Some(dir.clone()),
autofetch: Some(false),
..Default::default()
});
assert!(reg.libraries().is_empty());
assert!(
reg.versions().contains(line),
"versions() lists what is installed: {:?}",
reg.versions()
);
let lib = reg.for_version(line).unwrap();
assert_eq!(lib.minor(), line);
assert_eq!(
reg.libraries().len(),
1,
"exactly the line asked for is loaded"
);
let again = reg.for_version(lib.version()).unwrap();
assert!(
std::sync::Arc::ptr_eq(&lib, &again),
"the exact patch resolves to the same load"
);
assert_eq!(reg.libraries().len(), 1);
}
#[test]
fn a_rebuild_installs_the_highest_build() {
let fx = fixtures!();
let doc = expected(&fx);
let cases = doc["builds"]["cases"]
.as_array()
.cloned()
.unwrap_or_default();
assert!(
!cases.is_empty(),
"expected.json carries no builds.cases — regenerate the fixtures \
from the release pipeline"
);
let index: Json = serde_json::from_slice(
&std::fs::read(fx.join("two-builds").join("index.json")).expect("two-builds/index.json"),
)
.expect("two-builds/index.json parses");
let row_for = |file: &str| -> Json {
index["artifacts"]
.as_array()
.unwrap()
.iter()
.find(|a| a["file"] == file)
.unwrap_or_else(|| panic!("two-builds/index.json has no row for {file}"))
.clone()
};
for (i, c) in cases.iter().enumerate() {
let platform = c["platform"].as_str().unwrap();
let line = c["line"].as_str().unwrap();
let ctx = format!("{platform} {line}");
let want = row_for(c["install"]["file"].as_str().unwrap());
let other = row_for(c["superseded"]["file"].as_str().unwrap());
assert_eq!(
want["clickhouse_version"], other["clickhouse_version"],
"{ctx}: the two rows are different versions, so this proves nothing about builds"
);
let (wb, ob) = (
want["build"].as_u64().unwrap_or(0),
other["build"].as_u64().unwrap_or(0),
);
assert!(wb > ob, "{ctx}: the fixture's own case is upside down");
let dest = tmp(&format!("two-builds-{i}"));
let mut o = opts(&fx, "two-builds", &dest);
o.platform = Some(platform.to_string());
let installed = fetch::ensure(line, &o).unwrap_or_else(|e| panic!("{ctx}: {e}"));
let on_disk = sha256_file(&installed.library).expect("hash the installed library");
assert_eq!(
on_disk,
want["library_sha256"].as_str().unwrap(),
"{ctx}: installed library is not build {wb}"
);
assert_ne!(
on_disk,
other["library_sha256"].as_str().unwrap(),
"{ctx}: the SUPERSEDED build {ob} is what landed"
);
}
}