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,
writable: bool,
store: Mutex<HashMap<(Option<String>, String, String), Vec<u8>>>,
}
impl MemRepo {
fn new(name: &str, writable: bool) -> Self {
Self {
name: name.to_string(),
writable,
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 {
self.writable
}
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(),
];
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: 2,
total_uncompressed_bytes: 1234,
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 local_ui() -> UiData {
let writable: Arc<dyn RepositoryBackendTrait> = Arc::new(MemRepo::new("rust-dev", true));
let readonly: Arc<dyn RepositoryBackendTrait> = Arc::new(MemRepo::new("rust-prod", false));
let routes = FastRoutes::new(vec![
("rust-dev".to_string(), writable),
("rust-prod".to_string(), readonly),
]);
let holger: Arc<dyn HolgerObject> = Arc::new(LocalHolger::new(routes));
UiData::new(holger).expect("runtime")
}
#[test]
fn ui_data_round_trips_through_real_local_holger() {
let mut ui = local_ui();
ui.refresh_status();
assert!(ui.status.loaded, "status: {:?}", ui.status.error);
assert!(!ui.status.version.is_empty());
ui.refresh_repos();
assert_eq!(ui.repos.repos.len(), 2);
let dev = ui
.repos
.repos
.iter()
.position(|r| r.name == "rust-dev")
.expect("rust-dev present");
let prod = ui
.repos
.repos
.iter()
.position(|r| r.name == "rust-prod")
.expect("rust-prod present");
ui.select_repo(dev);
assert!(ui.repos.upload_enabled());
ui.select_repo(prod);
assert!(!ui.repos.upload_enabled());
let id = ArtifactId {
namespace: None,
name: "serde".into(),
version: "1.0.0".into(),
};
ui.put_artifact("rust-dev", &id, b"CRATE_BYTES").expect("put ok");
ui.fetch_artifact("rust-dev", id.clone());
assert!(ui.artifact.found);
assert_eq!(ui.artifact.size_bytes, b"CRATE_BYTES".len() as u64);
assert_eq!(ui.artifact.data, b"CRATE_BYTES");
ui.refresh_browse("rust-dev", None);
assert!(ui.browse.error.is_none(), "browse error: {:?}", ui.browse.error);
let row = ui
.browse
.entries
.iter()
.find(|r| r.name == "serde" && r.version == "1.0.0")
.expect("put artifact present in browse listing");
assert_eq!(row.size_bytes, b"CRATE_BYTES".len() as i64);
#[cfg_attr(not(feature = "testmatrix"), allow(unused_variables))]
let browsed_size = row.size_bytes;
ui.refresh_archive("rust-dev", None);
assert!(ui.archive.error.is_none(), "archive error: {:?}", ui.archive.error);
assert_eq!(ui.archive.repository, "rust-dev");
assert_eq!(ui.archive.file_count, 2);
assert_eq!(ui.archive.total_uncompressed_bytes, 1234);
assert_eq!(ui.archive.archive_path, "rust-dev");
assert_eq!(ui.archive.files.len(), 2);
assert!(ui
.archive
.files
.iter()
.any(|f| f == "crates/serde-1.0.0.crate"));
let err = ui.put_artifact("rust-prod", &id, b"x").unwrap_err();
assert!(
err.to_string().contains("read-only"),
"unexpected error: {err}"
);
let missing = ArtifactId {
namespace: None,
name: "does-not-exist".into(),
version: "0.0.0".into(),
};
ui.fetch_artifact("rust-dev", missing);
assert!(!ui.artifact.found);
assert!(ui.artifact.error.is_none());
#[cfg(feature = "testmatrix")]
fstatus(
"holger-ui",
"local_core_round_trip",
ui.repos.repos.len() == 2
&& browsed_size == b"CRATE_BYTES".len() as i64
&& ui.archive.file_count == 2
&& err.to_string().contains("read-only")
&& !ui.artifact.found,
&format!(
"LocalHolger: {} repos, put/fetch {} bytes, archive {} files, read-only enforced, miss=not-found",
ui.repos.repos.len(),
browsed_size,
ui.archive.file_count,
),
);
}
#[test]
fn browse_paginates_with_load_more() {
let mut ui = local_ui();
let total = 150usize;
for i in 0..total {
let id = ArtifactId {
namespace: None,
name: format!("crate-{i:03}"),
version: "1.0.0".into(),
};
ui.put_artifact("rust-dev", &id, b"x").expect("put");
}
ui.refresh_browse("rust-dev", None);
assert!(ui.browse.error.is_none(), "browse error: {:?}", ui.browse.error);
assert_eq!(ui.browse.entries.len(), 100);
assert!(!ui.browse.next_page_token.is_empty(), "expected a next-page token");
ui.load_more_browse();
assert_eq!(ui.browse.entries.len(), total);
assert!(ui.browse.next_page_token.is_empty(), "listing should be exhausted");
let mut names: Vec<&str> = ui.browse.entries.iter().map(|r| r.name.as_str()).collect();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), total, "pages overlapped or missed entries");
#[cfg_attr(not(feature = "testmatrix"), allow(unused_variables))]
let unique_names = names.len();
ui.load_more_browse();
assert_eq!(ui.browse.entries.len(), total);
#[cfg(feature = "testmatrix")]
fstatus(
"holger-ui",
"browse_paginates_with_load_more",
ui.browse.entries.len() == total && unique_names == total,
&format!("paged {total} artifacts across 2 pages; {unique_names} unique, no dups/misses"),
);
}