use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use holger_ui::data::UiData;
use server_lib::exposed::fast_routes::FastRoutes;
use server_lib::LocalHolger;
use traits::{ArtifactFormat, ArtifactId, HolgerObject, RepositoryBackendTrait};
#[cfg(feature = "testmatrix")]
fn fstatus(component: &str, check: &str, ok: bool, detail: &str) {
nornir_testmatrix::functional_status(component, check, ok, detail);
}
struct MemRepo {
name: String,
store: Mutex<HashMap<(Option<String>, String, String), Vec<u8>>>,
}
impl MemRepo {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
store: Mutex::new(HashMap::new()),
}
}
fn key(id: &ArtifactId) -> (Option<String>, String, String) {
(id.namespace.clone(), id.name.clone(), id.version.clone())
}
}
impl RepositoryBackendTrait for MemRepo {
fn name(&self) -> &str {
&self.name
}
fn format(&self) -> ArtifactFormat {
ArtifactFormat::Rust
}
fn is_writable(&self) -> bool {
true
}
fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>> {
Ok(self.store.lock().unwrap().get(&Self::key(id)).cloned())
}
fn put(&self, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()> {
self.store.lock().unwrap().insert(Self::key(id), data.to_vec());
Ok(())
}
fn list(
&self,
name_filter: Option<&str>,
limit: usize,
) -> anyhow::Result<Vec<traits::ArtifactEntry>> {
let store = self.store.lock().unwrap();
let mut out = Vec::new();
for ((namespace, name, version), bytes) in store.iter() {
if let Some(f) = name_filter {
if !name.contains(f) {
continue;
}
}
out.push(traits::ArtifactEntry {
id: ArtifactId {
namespace: namespace.clone(),
name: name.clone(),
version: version.clone(),
},
size_bytes: bytes.len() as i64,
content_type: "application/octet-stream".into(),
});
if limit != 0 && out.len() >= limit {
break;
}
}
Ok(out)
}
fn archive_files(&self, prefix: Option<&str>) -> anyhow::Result<Vec<String>> {
let files = vec![
"crates/serde-1.0.0.crate".to_string(),
"crates/tokio-1.2.0.crate".to_string(),
"crates/anyhow-1.0.0.crate".to_string(),
];
Ok(match prefix {
Some(p) => files.into_iter().filter(|f| f.starts_with(p)).collect(),
None => files,
})
}
fn archive_info(&self) -> anyhow::Result<traits::ArchiveInfo> {
Ok(traits::ArchiveInfo {
file_count: 3,
total_uncompressed_bytes: 4242,
archive_path: self.name.clone(),
})
}
fn handle_http2_request(
&self,
_method: &str,
_suburl: &str,
_body: &[u8],
) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)> {
Ok((404, Vec::new(), b"not found".to_vec()))
}
}
fn seeded_ui() -> UiData {
let repo: Arc<dyn RepositoryBackendTrait> = Arc::new(MemRepo::new("rust-dev"));
let routes = FastRoutes::new(vec![("rust-dev".to_string(), repo)]);
let holger: Arc<dyn HolgerObject> = Arc::new(LocalHolger::new(routes));
let ui = UiData::new(holger).expect("runtime");
for (name, bytes) in [("serde", &b"SERDE_BYTES"[..]), ("tokio", &b"TOKIO"[..])] {
let id = ArtifactId {
namespace: None,
name: name.into(),
version: "1.0.0".into(),
};
ui.put_artifact("rust-dev", &id, bytes).expect("seed put");
}
ui
}
#[test]
fn browse_view_state_json_lists_seeded_artifacts() {
let mut ui = seeded_ui();
ui.refresh_browse("rust-dev", None);
let s = ui.browse.state_json();
assert_eq!(s["repository"], "rust-dev", "state_json carries the active repo: {s}");
assert!(s["error"].is_null(), "no error in a healthy listing: {s}");
let entries = s["entries"].as_array().expect("state_json has an entries array");
assert_eq!(entries.len(), 2, "two seeded artifacts in the Browse listing: {s}");
let serde = entries
.iter()
.find(|e| e["name"] == "serde")
.expect("serde row present in Browse state_json");
assert_eq!(serde["version"], "1.0.0");
assert_eq!(serde["size_bytes"].as_i64(), Some(b"SERDE_BYTES".len() as i64));
assert!(
entries.iter().any(|e| e["name"] == "tokio"),
"tokio row present in Browse state_json: {s}"
);
#[cfg(feature = "testmatrix")]
fstatus(
"holger-ui",
"browse_view_state_json",
s["repository"] == "rust-dev"
&& entries.len() == 2
&& entries.iter().any(|e| e["name"] == "serde")
&& entries.iter().any(|e| e["name"] == "tokio"),
&format!("Browse state_json: repo={} {} entries (serde+tokio)", s["repository"], entries.len()),
);
}
#[test]
fn archive_view_state_json_exposes_tree_and_stats() {
let mut ui = seeded_ui();
ui.refresh_archive("rust-dev", None);
let s = ui.archive.state_json();
assert!(s["error"].is_null(), "no error browsing the archive: {s}");
assert_eq!(s["repository"], "rust-dev");
assert_eq!(s["file_count"].as_u64(), Some(3), "stats file_count: {s}");
assert_eq!(
s["total_uncompressed_bytes"].as_u64(),
Some(4242),
"stats total bytes: {s}"
);
let files = s["files"].as_array().expect("state_json has a files tree");
assert_eq!(files.len(), 3, "three archive entries in the tree: {s}");
assert!(
files.iter().any(|f| f == "crates/serde-1.0.0.crate"),
"archive tree carries the real entries: {s}"
);
#[cfg(feature = "testmatrix")]
fstatus(
"holger-ui",
"archive_view_state_json",
s["file_count"].as_u64() == Some(3)
&& s["total_uncompressed_bytes"].as_u64() == Some(4242)
&& files.len() == 3
&& files.iter().any(|f| f == "crates/serde-1.0.0.crate"),
&format!("Archive state_json: file_count={} bytes={} tree_len={}", s["file_count"], s["total_uncompressed_bytes"], files.len()),
);
}