use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::Arc; use std::sync::atomic::{AtomicU32, Ordering}; use async_trait::async_trait; use gitbaby::GitBaby; use gitbaby::archive::{ArchiveFormat, BundleRequest, GetArchiveRequest}; use gitbaby::error::BabyError; use gitbaby::repo::RepositoryFacade; static COUNTER: AtomicU32 = AtomicU32::new(0); struct TestFacade { dir: PathBuf, } #[async_trait] impl RepositoryFacade for TestFacade { async fn git_repo_dir(&self) -> Result { Ok(self.dir.clone()) } async fn git_alternate_object_directories(&self) -> Result, BabyError> { Ok(Vec::new()) } async fn gix_repo(&self) -> Result { gix::open(&self.dir).map_err(|e| BabyError::Custom(e.to_string())) } } fn temp_repo() -> PathBuf { let n = COUNTER.fetch_add(1, Ordering::SeqCst); let dir = std::env::temp_dir().join(format!("gitbaby-archive-test-{}-{n}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).expect("mkdir repo"); let out = Command::new("git") .args(["init", "-q"]) .current_dir(&dir) .output() .expect("git init failed"); assert!(out.status.success(), "git init: {:?}", out); dir } fn cleanup(repo: &Path) { let _ = std::fs::remove_dir_all(repo); } fn git_commit(repo: &Path, msg: &str) { let out = Command::new("git") .args([ "-c", "user.name=T", "-c", "user.email=t@e", "commit", "-q", "-m", msg, ]) .current_dir(repo) .output() .expect("git commit failed"); assert!(out.status.success(), "git commit `{msg}`: {:?}", out); } fn write_file(repo: &Path, rel: &str, content: &str) { let p = repo.join(rel); std::fs::create_dir_all(p.parent().expect("parent")).expect("mkdir"); std::fs::write(p, content).expect("write"); } fn index_commit(repo: &Path, msg: &str) { let out = Command::new("git") .args(["add", "-A"]) .current_dir(repo) .output() .expect("git add failed"); assert!(out.status.success(), "git add: {:?}", out); git_commit(repo, msg); } async fn baby_of(repo: PathBuf) -> GitBaby { GitBaby::new(Arc::new(TestFacade { dir: repo })) } fn archive_dir(repo: &Path) -> PathBuf { repo.join("archive") } fn archive_files(repo: &Path) -> Vec { let mut files: Vec = std::fs::read_dir(archive_dir(repo)) .expect("read archive dir") .filter_map(|e| e.ok()) .map(|e| e.path()) .filter(|p| p.is_file()) .collect(); files.sort(); files } #[tokio::test] async fn get_archive_persists_and_hits_cache() { let repo = temp_repo(); write_file(&repo, "a.txt", "one"); write_file(&repo, "b.txt", "bee"); index_commit(&repo, "first"); let baby = baby_of(repo.clone()).await; let req = || GetArchiveRequest { commit: "HEAD".to_string(), format: ArchiveFormat::Tar, prefix: None, path: None, exclude: Vec::new(), }; let first_bytes = baby .get_archive(req()) .await .expect("get_archive") .read_to_end() .await .expect("read archive body"); let files = archive_files(&repo); assert_eq!(files.len(), 1, "one cached archive expected"); assert_eq!( files[0].extension().and_then(|e| e.to_str()), Some("tar"), "cached file: {:?}", files[0] ); let on_disk = std::fs::read(&files[0]).expect("read cached file"); assert_eq!(on_disk, first_bytes, "cached bytes match streamed bytes"); let direct = Command::new("git") .args(["archive", "--format=tar", "HEAD"]) .current_dir(&repo) .output() .expect("git archive direct"); assert!(direct.status.success(), "git archive direct: {:?}", direct); assert_eq!(direct.stdout, first_bytes, "matches raw git archive output"); let second_bytes = baby .get_archive(req()) .await .expect("get_archive cached") .read_to_end() .await .expect("read archive body"); assert_eq!(second_bytes, first_bytes, "cache hit returns same bytes"); assert_eq!(archive_files(&repo).len(), 1); cleanup(&repo); } #[tokio::test] async fn get_archive_keys_differ_by_options() { let repo = temp_repo(); write_file(&repo, "a.txt", "one"); index_commit(&repo, "first"); let baby = baby_of(repo.clone()).await; let base = GetArchiveRequest { commit: "HEAD".to_string(), format: ArchiveFormat::Tar, prefix: None, path: None, exclude: Vec::new(), }; let plain = baby .get_archive(base.clone()) .await .expect("plain") .read_to_end() .await .expect("plain body"); let prefixed = baby .get_archive(GetArchiveRequest { prefix: Some("p/".to_string()), ..base.clone() }) .await .expect("prefixed") .read_to_end() .await .expect("prefixed body"); let excluded = baby .get_archive(GetArchiveRequest { exclude: vec!["a.txt".to_string()], ..base.clone() }) .await .expect("excluded") .read_to_end() .await .expect("excluded body"); assert_ne!(plain, prefixed); assert_ne!(plain, excluded); assert_ne!(prefixed, excluded); let files = archive_files(&repo); assert_eq!(files.len(), 3, "three distinct keys cached"); let zipped = baby .get_archive(GetArchiveRequest { format: ArchiveFormat::Zip, ..base }) .await .expect("zip") .read_to_end() .await .expect("zip body"); let files = archive_files(&repo); assert_eq!(files.len(), 4); assert!( files .iter() .any(|p| p.extension().and_then(|e| e.to_str()) == Some("zip")), "zip file present: {:?}", files ); assert!(zipped.starts_with(b"PK"), "zip magic"); assert_ne!(zipped, plain); cleanup(&repo); } #[tokio::test] async fn get_archive_new_commit_new_key() { let repo = temp_repo(); write_file(&repo, "a.txt", "one"); index_commit(&repo, "first"); let baby = baby_of(repo.clone()).await; let req = || GetArchiveRequest { commit: "HEAD".to_string(), format: ArchiveFormat::Tar, prefix: None, path: None, exclude: Vec::new(), }; let _ = baby .get_archive(req()) .await .expect("first revision") .read_to_end() .await .expect("body"); assert_eq!(archive_files(&repo).len(), 1); write_file(&repo, "a.txt", "two"); index_commit(&repo, "second"); let _ = baby .get_archive(req()) .await .expect("second revision") .read_to_end() .await .expect("body"); let files = archive_files(&repo); assert_eq!(files.len(), 2, "new commit produces new cached key"); cleanup(&repo); } #[tokio::test] async fn create_bundle_persists_and_hits_cache() { let repo = temp_repo(); write_file(&repo, "a.txt", "one"); index_commit(&repo, "first"); let baby = baby_of(repo.clone()).await; let req = || BundleRequest { refs: vec!["HEAD".to_string()], }; let first = baby .create_bundle(req()) .await .expect("create_bundle") .read_to_end() .await .expect("bundle body"); let files = archive_files(&repo); assert_eq!(files.len(), 1, "one cached bundle expected"); assert_eq!( files[0].extension().and_then(|e| e.to_str()), Some("bundle"), "cached file: {:?}", files[0] ); let on_disk = std::fs::read(&files[0]).expect("read cached file"); assert_eq!(on_disk, first, "cached bundle bytes match streamed bytes"); let second = baby .create_bundle(req()) .await .expect("create_bundle cached") .read_to_end() .await .expect("bundle body"); assert_eq!(second, first, "cache hit returns same bundle bytes"); assert_eq!(archive_files(&repo).len(), 1); cleanup(&repo); }