use anyhow::{Context, Result};
use std::path::Path;
use std::time::Duration;
pub fn start_in_current_runtime(data_dir: &Path, grpc: &str, http: &str) -> Result<()> {
std::fs::create_dir_all(data_dir)
.with_context(|| format!("create holger data dir {}", data_dir.display()))?;
let cfg_path = data_dir.join("holger-dev-pair.ron");
std::fs::write(&cfg_path, holger_server_lib::dev_pair_ron(data_dir, grpc, http))
.with_context(|| format!("write holger config {}", cfg_path.display()))?;
let mut holger = holger_server_lib::read_ron_config(&cfg_path)
.with_context(|| format!("read holger config {}", cfg_path.display()))?;
holger.instantiate_backends().context("holger: instantiate backends")?;
holger_server_lib::wire_holger(&mut holger).context("holger: wire routes")?;
holger.start().context("holger: start servers")?;
Ok(())
}
pub struct EmbeddedHolger {
_rt: tokio::runtime::Runtime,
pub grpc: String,
pub http: String,
}
impl EmbeddedHolger {
pub fn start(data_dir: &Path, grpc: &str, http: &str) -> Result<Self> {
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.worker_threads(2)
.thread_name("holger-embed")
.build()
.context("build holger tokio runtime")?;
{
let _guard = rt.enter();
start_in_current_runtime(data_dir, grpc, http)?;
}
Ok(Self { _rt: rt, grpc: grpc.to_string(), http: http.to_string() })
}
pub fn wait_ready(&self, timeout: Duration) -> Result<()> {
wait_ready(&self.http, timeout)
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct RepoSummary {
pub name: String,
pub repo_type: String,
pub writable: bool,
pub crate_count: usize,
pub artifact_count: usize,
pub recent: Vec<String>,
}
pub fn read_registry(
data_dir: &Path,
grpc: &str,
http: &str,
per_repo_limit: usize,
) -> Result<Vec<RepoSummary>> {
use std::collections::BTreeSet;
std::fs::create_dir_all(data_dir)
.with_context(|| format!("create holger data dir {}", data_dir.display()))?;
let cfg_path = data_dir.join("holger-dev-pair.ron");
std::fs::write(&cfg_path, holger_server_lib::dev_pair_ron(data_dir, grpc, http))
.with_context(|| format!("write holger config {}", cfg_path.display()))?;
let mut holger = holger_server_lib::read_ron_config(&cfg_path)
.with_context(|| format!("read holger config {}", cfg_path.display()))?;
holger.instantiate_backends().context("holger: instantiate backends")?;
let mut out = Vec::with_capacity(holger.repositories.len());
for repo in &holger.repositories {
let mut summary = RepoSummary {
name: repo.ron_name.clone(),
repo_type: repo.ron_repo_type.clone(),
..Default::default()
};
if let Some(backend) = &repo.backend_repository {
summary.writable = backend.is_writable();
match backend.list(None, per_repo_limit) {
Ok(entries) => {
summary.artifact_count = entries.len();
let mut names: BTreeSet<String> = BTreeSet::new();
for e in &entries {
names.insert(e.id.name.clone());
}
summary.crate_count = names.len();
summary.recent = entries
.iter()
.rev()
.take(8)
.map(|e| format!("{} v{}", e.id.name, e.id.version))
.collect();
}
Err(e) => {
eprintln!("holger read_registry: list({}) failed: {e:#}", repo.ron_name);
}
}
}
out.push(summary);
}
Ok(out)
}
pub fn wait_ready(http_addr: &str, timeout: Duration) -> Result<()> {
let url = format!("http://{http_addr}/cache/index/config.json");
let deadline = std::time::Instant::now() + timeout;
loop {
if ureq::get(&url)
.timeout(Duration::from_millis(500))
.call()
.is_ok()
{
return Ok(());
}
if std::time::Instant::now() >= deadline {
anyhow::bail!("holger HTTP gateway never became ready at {url} (waited {timeout:?})");
}
std::thread::sleep(Duration::from_millis(150));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_registry_enumerates_dev_pair_via_functional_api() {
let dir = tempfile::tempdir().unwrap();
let data = dir.path();
let grpc = "127.0.0.1:18443";
let http = "127.0.0.1:18444";
let repos = read_registry(data, grpc, http, 1000).expect("read empty dev pair");
let names: Vec<&str> = repos.iter().map(|r| r.name.as_str()).collect();
assert_eq!(
names,
vec!["crates-io", "cache", "sparring"],
"functional API lists the dev-pair repos"
);
let sparring = repos.iter().find(|r| r.name == "sparring").unwrap();
assert!(sparring.writable, "/sparring is a writable registry");
assert_eq!(sparring.crate_count, 0, "nothing published yet");
let cfg_path = data.join("holger-dev-pair.ron");
std::fs::write(&cfg_path, holger_server_lib::dev_pair_ron(data, grpc, http)).unwrap();
let mut holger = holger_server_lib::read_ron_config(&cfg_path).unwrap();
holger.instantiate_backends().unwrap();
let id = holger_server_lib::ArtifactId {
namespace: None,
name: "demo".into(),
version: "0.1.0".into(),
};
holger.put("sparring", &id, b"crate-bytes").expect("publish to /sparring");
let repos = read_registry(data, grpc, http, 1000).expect("re-read after publish");
let sparring = repos.iter().find(|r| r.name == "sparring").unwrap();
assert_eq!(sparring.crate_count, 1, "the published crate is enumerated");
assert!(
sparring.recent.iter().any(|s| s.contains("demo")),
"recent list carries the published crate, got {:?}",
sparring.recent
);
}
}