1use sui_compat::store_path::StorePath;
4
5pub type StoreResult<T> = Result<T, StoreError>;
7
8#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum StoreError {
12 #[error("path not found: {0}")]
14 PathNotFound(String),
15 #[error("database error: {0}")]
17 Database(String),
18 #[error("http error: {0}")]
20 Http(String),
21 #[error("narinfo parse error: {0}")]
23 NarInfo(String),
24 #[error("io error: {0}")]
26 Io(#[from] std::io::Error),
27 #[error("not supported: {0}")]
29 NotSupported(String),
30 #[error("internal error: {0}")]
32 Internal(String),
33}
34
35#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
37#[must_use]
38pub struct PathInfo {
39 pub path: String,
41 pub nar_hash: String,
43 pub nar_size: i64,
45 pub references: Vec<String>,
47 pub deriver: Option<String>,
49 pub signatures: Vec<String>,
51 pub registration_time: i64,
53 pub content_address: Option<String>,
55}
56
57impl StoreError {
58 #[must_use]
60 pub fn is_path_not_found(&self) -> bool {
61 matches!(self, Self::PathNotFound(_))
62 }
63
64 #[must_use]
66 pub fn is_not_supported(&self) -> bool {
67 matches!(self, Self::NotSupported(_))
68 }
69}
70
71impl From<crate::http::HttpError> for StoreError {
72 fn from(e: crate::http::HttpError) -> Self {
73 Self::Http(e.to_string())
74 }
75}
76
77impl PathInfo {
78 pub fn new(path: impl Into<String>, nar_hash: impl Into<String>) -> Self {
83 Self {
84 path: path.into(),
85 nar_hash: nar_hash.into(),
86 ..Self::default()
87 }
88 }
89}
90
91impl std::fmt::Display for PathInfo {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 write!(f, "{} (nar_size={})", self.path, self.nar_size)
94 }
95}
96
97impl From<&sui_compat::narinfo::NarInfo> for PathInfo {
98 fn from(info: &sui_compat::narinfo::NarInfo) -> Self {
99 let store_dir = sui_compat::store_path::DEFAULT_STORE_DIR;
106 let references = info
107 .references
108 .iter()
109 .map(|r| {
110 if r.starts_with('/') {
111 r.clone()
112 } else {
113 format!("{store_dir}/{r}")
114 }
115 })
116 .collect();
117 Self {
118 path: info.store_path.clone(),
119 nar_hash: info.nar_hash.clone(),
120 nar_size: info.nar_size as i64,
121 references,
122 deriver: info.deriver.clone(),
123 signatures: info.signatures.clone(),
124 registration_time: 0,
125 content_address: info.ca.clone(),
126 }
127 }
128}
129
130#[derive(Debug, Clone, Default, PartialEq, Eq)]
132pub struct GcOptions {
133 pub max_freed: u64,
135 pub delete_older_than: Option<u64>,
137}
138
139impl GcOptions {
140 #[must_use]
142 pub fn with_max_freed(mut self, bytes: u64) -> Self {
143 self.max_freed = bytes;
144 self
145 }
146
147 #[must_use]
149 pub fn with_delete_older_than(mut self, seconds: u64) -> Self {
150 self.delete_older_than = Some(seconds);
151 self
152 }
153}
154
155#[derive(Debug, Clone, Default, PartialEq, Eq)]
157#[must_use]
158pub struct GcResult {
159 pub paths_deleted: usize,
161 pub bytes_freed: u64,
163}
164
165impl std::fmt::Display for GcResult {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 write!(
168 f,
169 "GC: {} paths deleted, {} bytes freed",
170 self.paths_deleted, self.bytes_freed
171 )
172 }
173}
174
175#[derive(Debug, Clone, Default, PartialEq, Eq)]
177#[must_use]
178pub struct OptimiseResult {
179 pub files_linked: u64,
181 pub bytes_saved: u64,
183}
184
185impl std::fmt::Display for OptimiseResult {
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 write!(
188 f,
189 "Optimise: {} files linked, {} bytes saved",
190 self.files_linked, self.bytes_saved
191 )
192 }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
197#[must_use]
198pub struct CorruptPath {
199 pub path: String,
201 pub expected_hash: String,
203 pub actual_hash: String,
205}
206
207#[derive(Debug, Clone, Default, PartialEq, Eq)]
209#[must_use]
210pub struct VerifyResult {
211 pub total_checked: usize,
213 pub valid_count: usize,
215 pub corrupt: Vec<CorruptPath>,
217}
218
219impl std::fmt::Display for VerifyResult {
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 write!(
222 f,
223 "Verify: {} checked, {} valid, {} corrupt",
224 self.total_checked,
225 self.valid_count,
226 self.corrupt.len()
227 )
228 }
229}
230
231#[async_trait::async_trait]
236pub trait Store: Send + Sync {
237 async fn query_path_info(
239 &self,
240 path: &StorePath,
241 ) -> StoreResult<Option<PathInfo>>;
242
243 async fn is_valid_path(
245 &self,
246 path: &StorePath,
247 ) -> StoreResult<bool>;
248
249 async fn query_all_valid_paths(
251 &self,
252 ) -> StoreResult<Vec<StorePath>>;
253
254 async fn query_references(
256 &self,
257 path: &StorePath,
258 ) -> StoreResult<Vec<StorePath>> {
259 let info = self
260 .query_path_info(path)
261 .await?
262 .ok_or_else(|| StoreError::PathNotFound(path.to_absolute_path()))?;
263
264 Ok(info
265 .references
266 .iter()
267 .filter_map(|r| StorePath::from_absolute_path(r).ok())
268 .collect())
269 }
270
271 async fn compute_closure(
275 &self,
276 roots: &[StorePath],
277 ) -> StoreResult<Vec<StorePath>> {
278 let mut closure = Vec::new();
279 let mut stack: Vec<StorePath> = roots.to_vec();
280 let mut seen = std::collections::BTreeSet::new();
281
282 while let Some(path) = stack.pop() {
283 let key = path.to_absolute_path();
284 if !seen.insert(key) {
285 continue;
286 }
287
288 let refs = self.query_references(&path).await?;
289 stack.extend(refs);
290 closure.push(path);
291 }
292 Ok(closure)
293 }
294
295 async fn collect_garbage(
297 &self,
298 _options: &GcOptions,
299 ) -> StoreResult<GcResult> {
300 Err(StoreError::NotSupported(
301 "garbage collection not implemented for this backend".to_string(),
302 ))
303 }
304
305 async fn add_to_store(
307 &self,
308 _name: &str,
309 _nar_data: &[u8],
310 _references: &[String],
311 ) -> StoreResult<PathInfo> {
312 Err(StoreError::NotSupported(
313 "add_to_store not implemented for this backend".to_string(),
314 ))
315 }
316
317 async fn register_path(
319 &self,
320 _info: &PathInfo,
321 ) -> StoreResult<()> {
322 Err(StoreError::NotSupported(
323 "register_path not implemented for this backend".to_string(),
324 ))
325 }
326
327 async fn add_signatures(
329 &self,
330 _path: &StorePath,
331 _signatures: &[String],
332 ) -> StoreResult<()> {
333 Err(StoreError::NotSupported(
334 "add_signatures not implemented for this backend".to_string(),
335 ))
336 }
337
338 async fn query_referrers(
340 &self,
341 _path: &StorePath,
342 ) -> StoreResult<Vec<StorePath>> {
343 Err(StoreError::NotSupported(
344 "query_referrers not implemented for this backend".to_string(),
345 ))
346 }
347
348 async fn verify_store(&self) -> StoreResult<VerifyResult> {
350 Err(StoreError::NotSupported(
351 "verify_store not implemented for this backend".to_string(),
352 ))
353 }
354
355 async fn delete_path(&self, _path: &StorePath) -> StoreResult<u64> {
359 Err(StoreError::NotSupported(
360 "delete_path not implemented for this backend".to_string(),
361 ))
362 }
363
364 async fn optimise_store(&self, _dry_run: bool) -> StoreResult<OptimiseResult> {
370 Err(StoreError::NotSupported(
371 "optimise_store not implemented for this backend".to_string(),
372 ))
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn path_info_serialization_roundtrip() {
382 let info = PathInfo {
383 path: "/nix/store/abc-hello".to_string(),
384 nar_hash: "sha256:deadbeef".to_string(),
385 nar_size: 1024,
386 references: vec!["/nix/store/dep1".to_string()],
387 deriver: Some("/nix/store/abc.drv".to_string()),
388 signatures: vec!["key:sig".to_string()],
389 registration_time: 1234567890,
390 content_address: None,
391 };
392 let json = serde_json::to_string(&info).unwrap();
393 let parsed: PathInfo = serde_json::from_str(&json).unwrap();
394 assert_eq!(parsed.path, info.path);
395 assert_eq!(parsed.nar_hash, info.nar_hash);
396 assert_eq!(parsed.nar_size, info.nar_size);
397 assert_eq!(parsed.references, info.references);
398 assert_eq!(parsed.deriver, info.deriver);
399 assert_eq!(parsed.signatures, info.signatures);
400 assert_eq!(parsed.registration_time, info.registration_time);
401 }
402
403 #[test]
404 fn path_info_serialization_all_fields_present() {
405 let info = PathInfo {
406 path: "/nix/store/abc-hello".to_string(),
407 nar_hash: "sha256:deadbeef".to_string(),
408 nar_size: 1024,
409 references: vec!["/nix/store/dep1".to_string(), "/nix/store/dep2".to_string()],
410 deriver: Some("/nix/store/abc.drv".to_string()),
411 signatures: vec!["key1:sig1".to_string(), "key2:sig2".to_string()],
412 registration_time: 1234567890,
413 content_address: Some("fixed:out:r:sha256:cafe".to_string()),
414 };
415 let json = serde_json::to_string(&info).unwrap();
416 assert!(json.contains("\"content_address\""));
417 assert!(json.contains("fixed:out:r:sha256:cafe"));
418 let parsed: PathInfo = serde_json::from_str(&json).unwrap();
419 assert_eq!(parsed.content_address, info.content_address);
420 assert_eq!(parsed.references.len(), 2);
421 assert_eq!(parsed.signatures.len(), 2);
422 }
423
424 #[test]
425 fn path_info_serialization_none_fields() {
426 let info = PathInfo {
427 path: "/nix/store/abc-minimal".to_string(),
428 nar_hash: "sha256:000".to_string(),
429 nar_size: 0,
430 references: vec![],
431 deriver: None,
432 signatures: vec![],
433 registration_time: 0,
434 content_address: None,
435 };
436 let json = serde_json::to_string(&info).unwrap();
437 let parsed: PathInfo = serde_json::from_str(&json).unwrap();
438 assert!(parsed.deriver.is_none());
439 assert!(parsed.content_address.is_none());
440 assert!(parsed.references.is_empty());
441 assert!(parsed.signatures.is_empty());
442 assert_eq!(parsed.nar_size, 0);
443 }
444
445 #[test]
446 fn path_info_json_pretty_roundtrip() {
447 let info = PathInfo {
448 path: "/nix/store/abc-hello".to_string(),
449 nar_hash: "sha256:deadbeef".to_string(),
450 nar_size: 42,
451 references: vec![],
452 deriver: None,
453 signatures: vec![],
454 registration_time: 999,
455 content_address: None,
456 };
457 let pretty = serde_json::to_string_pretty(&info).unwrap();
458 let parsed: PathInfo = serde_json::from_str(&pretty).unwrap();
459 assert_eq!(parsed.path, info.path);
460 assert_eq!(parsed.nar_size, 42);
461 }
462
463 #[test]
464 fn path_info_deserialization_from_json_object() {
465 let json = r#"{
466 "path": "/nix/store/xyz-test",
467 "nar_hash": "sha256:abc123",
468 "nar_size": 9999,
469 "references": ["/nix/store/dep-a"],
470 "deriver": null,
471 "signatures": [],
472 "registration_time": 0,
473 "content_address": null
474 }"#;
475 let info: PathInfo = serde_json::from_str(json).unwrap();
476 assert_eq!(info.path, "/nix/store/xyz-test");
477 assert_eq!(info.nar_size, 9999);
478 assert_eq!(info.references, vec!["/nix/store/dep-a"]);
479 }
480
481 #[test]
482 fn path_info_clone_independence() {
483 let info = PathInfo {
484 path: "/nix/store/abc-hello".to_string(),
485 nar_hash: "sha256:deadbeef".to_string(),
486 nar_size: 1024,
487 references: vec!["/nix/store/dep1".to_string()],
488 deriver: Some("/nix/store/abc.drv".to_string()),
489 signatures: vec!["key:sig".to_string()],
490 registration_time: 1234567890,
491 content_address: None,
492 };
493 let mut cloned = info.clone();
494 cloned.nar_size = 9999;
495 cloned.path = "/nix/store/other".to_string();
496 assert_eq!(info.nar_size, 1024);
497 assert_eq!(info.path, "/nix/store/abc-hello");
498 }
499
500 #[test]
501 fn gc_options_default() {
502 let opts = GcOptions::default();
503 assert_eq!(opts.max_freed, 0);
504 assert!(opts.delete_older_than.is_none());
505 }
506
507 #[test]
508 fn store_error_display() {
509 let e = StoreError::PathNotFound("/nix/store/abc".to_string());
510 assert!(e.to_string().contains("/nix/store/abc"));
511
512 let e = StoreError::NotSupported("gc".to_string());
513 assert!(e.to_string().contains("gc"));
514 }
515
516 struct TestStore {
521 infos: std::collections::BTreeMap<String, PathInfo>,
522 }
523
524 impl TestStore {
525 fn new() -> Self {
526 Self {
527 infos: std::collections::BTreeMap::new(),
528 }
529 }
530
531 fn with_path(mut self, info: PathInfo) -> Self {
532 self.infos.insert(info.path.clone(), info);
533 self
534 }
535 }
536
537 #[async_trait::async_trait]
538 impl Store for TestStore {
539 async fn query_path_info(
540 &self,
541 path: &StorePath,
542 ) -> StoreResult<Option<PathInfo>> {
543 Ok(self.infos.get(&path.to_absolute_path()).cloned())
544 }
545
546 async fn is_valid_path(
547 &self,
548 path: &StorePath,
549 ) -> StoreResult<bool> {
550 Ok(self.infos.contains_key(&path.to_absolute_path()))
551 }
552
553 async fn query_all_valid_paths(&self) -> StoreResult<Vec<StorePath>> {
554 self.infos
555 .keys()
556 .map(|p| {
557 StorePath::from_absolute_path(p)
558 .map_err(|e| StoreError::Database(e.to_string()))
559 })
560 .collect()
561 }
562 }
563
564 fn hello_path() -> StorePath {
566 StorePath::from_absolute_path(
567 "/nix/store/sn5lbjwwmkbzj7cx0hfnlwf4sh16cll6-hello-2.12.1",
568 )
569 .unwrap()
570 }
571
572 fn glibc_path() -> StorePath {
573 StorePath::from_absolute_path(
574 "/nix/store/3n58xw4373jp0ljirf06d8077j15pc4j-glibc-2.37",
575 )
576 .unwrap()
577 }
578
579 fn bash_path() -> StorePath {
580 StorePath::from_absolute_path(
581 "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-bash-5.2",
582 )
583 .unwrap()
584 }
585
586 fn hello_info() -> PathInfo {
587 PathInfo {
588 path: hello_path().to_absolute_path(),
589 nar_hash: "sha256:aaa".to_string(),
590 nar_size: 5000,
591 references: vec![glibc_path().to_absolute_path()],
592 deriver: Some("/nix/store/abc.drv".to_string()),
593 signatures: vec!["key:sig".to_string()],
594 registration_time: 1000,
595 content_address: None,
596 }
597 }
598
599 fn glibc_info() -> PathInfo {
600 PathInfo {
601 path: glibc_path().to_absolute_path(),
602 nar_hash: "sha256:bbb".to_string(),
603 nar_size: 30000,
604 references: vec![bash_path().to_absolute_path()],
605 deriver: None,
606 signatures: vec![],
607 registration_time: 900,
608 content_address: None,
609 }
610 }
611
612 fn bash_info() -> PathInfo {
613 PathInfo {
614 path: bash_path().to_absolute_path(),
615 nar_hash: "sha256:ccc".to_string(),
616 nar_size: 8000,
617 references: vec![], deriver: None,
619 signatures: vec![],
620 registration_time: 800,
621 content_address: None,
622 }
623 }
624
625 #[tokio::test]
628 async fn query_references_returns_refs_from_path_info() {
629 let store = TestStore::new()
630 .with_path(hello_info())
631 .with_path(glibc_info());
632
633 let refs = store.query_references(&hello_path()).await.unwrap();
634 assert_eq!(refs.len(), 1);
635 assert_eq!(refs[0].to_absolute_path(), glibc_path().to_absolute_path());
636 }
637
638 #[tokio::test]
639 async fn query_references_returns_empty_for_leaf() {
640 let store = TestStore::new().with_path(bash_info());
641
642 let refs = store.query_references(&bash_path()).await.unwrap();
643 assert!(refs.is_empty());
644 }
645
646 #[tokio::test]
647 async fn query_references_errors_for_missing_path() {
648 let store = TestStore::new();
649
650 let result = store.query_references(&hello_path()).await;
651 assert!(result.is_err());
652 match result.unwrap_err() {
653 StoreError::PathNotFound(p) => {
654 assert!(p.contains("hello-2.12.1"));
655 }
656 other => panic!("expected PathNotFound, got {other:?}"),
657 }
658 }
659
660 #[tokio::test]
663 async fn compute_closure_walks_transitive_deps() {
664 let store = TestStore::new()
665 .with_path(hello_info())
666 .with_path(glibc_info())
667 .with_path(bash_info());
668
669 let closure = store.compute_closure(&[hello_path()]).await.unwrap();
670 assert_eq!(closure.len(), 3);
672 let paths: Vec<String> = closure.iter().map(|p| p.to_absolute_path()).collect();
673 assert!(paths.contains(&hello_path().to_absolute_path()));
674 assert!(paths.contains(&glibc_path().to_absolute_path()));
675 assert!(paths.contains(&bash_path().to_absolute_path()));
676 }
677
678 #[tokio::test]
679 async fn compute_closure_deduplicates() {
680 let store = TestStore::new()
682 .with_path(hello_info())
683 .with_path(glibc_info())
684 .with_path(bash_info());
685
686 let closure = store
687 .compute_closure(&[hello_path(), glibc_path()])
688 .await
689 .unwrap();
690 let bash_count = closure
691 .iter()
692 .filter(|p| p.to_absolute_path() == bash_path().to_absolute_path())
693 .count();
694 assert_eq!(bash_count, 1);
695 }
696
697 #[tokio::test]
698 async fn compute_closure_empty_roots() {
699 let store = TestStore::new();
700 let closure = store.compute_closure(&[]).await.unwrap();
701 assert!(closure.is_empty());
702 }
703
704 #[tokio::test]
705 async fn compute_closure_single_leaf() {
706 let store = TestStore::new().with_path(bash_info());
707
708 let closure = store.compute_closure(&[bash_path()]).await.unwrap();
709 assert_eq!(closure.len(), 1);
710 assert_eq!(closure[0].to_absolute_path(), bash_path().to_absolute_path());
711 }
712
713 #[tokio::test]
716 async fn collect_garbage_returns_not_supported() {
717 let store = TestStore::new();
718 let result = store.collect_garbage(&GcOptions::default()).await;
719 assert!(result.is_err());
720 match result.unwrap_err() {
721 StoreError::NotSupported(msg) => {
722 assert!(msg.contains("garbage collection"));
723 }
724 other => panic!("expected NotSupported, got {other:?}"),
725 }
726 }
727
728 #[tokio::test]
731 async fn add_to_store_returns_not_supported() {
732 let store = TestStore::new();
733 let result = store.add_to_store("test", b"data", &[]).await;
734 assert!(result.is_err());
735 match result.unwrap_err() {
736 StoreError::NotSupported(msg) => {
737 assert!(msg.contains("add_to_store"));
738 }
739 other => panic!("expected NotSupported, got {other:?}"),
740 }
741 }
742
743 #[tokio::test]
746 async fn register_path_returns_not_supported() {
747 let store = TestStore::new();
748 let info = hello_info();
749 let result = store.register_path(&info).await;
750 assert!(result.is_err());
751 match result.unwrap_err() {
752 StoreError::NotSupported(msg) => {
753 assert!(msg.contains("register_path"));
754 }
755 other => panic!("expected NotSupported, got {other:?}"),
756 }
757 }
758
759 #[tokio::test]
762 async fn add_signatures_returns_not_supported() {
763 let store = TestStore::new();
764 let result = store
765 .add_signatures(&hello_path(), &["sig1".to_string()])
766 .await;
767 assert!(result.is_err());
768 match result.unwrap_err() {
769 StoreError::NotSupported(msg) => {
770 assert!(msg.contains("add_signatures"));
771 }
772 other => panic!("expected NotSupported, got {other:?}"),
773 }
774 }
775
776 #[tokio::test]
779 async fn query_referrers_returns_not_supported() {
780 let store = TestStore::new();
781 let result = store.query_referrers(&hello_path()).await;
782 assert!(result.is_err());
783 match result.unwrap_err() {
784 StoreError::NotSupported(msg) => {
785 assert!(msg.contains("query_referrers"));
786 }
787 other => panic!("expected NotSupported, got {other:?}"),
788 }
789 }
790
791 #[test]
794 fn store_trait_is_object_safe() {
795 fn assert_obj_safe(_: &dyn Store) {}
796 let store = TestStore::new();
797 assert_obj_safe(&store);
798 }
799
800 #[test]
803 fn store_error_from_io() {
804 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
805 let store_err: StoreError = io_err.into();
806 assert!(store_err.to_string().contains("denied"));
807 }
808
809 #[test]
810 fn store_error_database_display() {
811 let e = StoreError::Database("connection lost".to_string());
812 assert!(e.to_string().contains("connection lost"));
813 }
814
815 #[test]
816 fn store_error_http_display() {
817 let e = StoreError::Http("timeout".to_string());
818 assert!(e.to_string().contains("timeout"));
819 assert!(e.to_string().contains("http"));
820 }
821
822 #[test]
823 fn store_error_narinfo_display() {
824 let e = StoreError::NarInfo("missing field: StorePath".to_string());
825 assert!(e.to_string().contains("missing field"));
826 assert!(e.to_string().contains("narinfo"));
827 }
828
829 #[tokio::test]
832 async fn arc_dyn_store_query_path_info() {
833 let store: std::sync::Arc<dyn Store> = std::sync::Arc::new(
834 TestStore::new().with_path(hello_info()),
835 );
836 let info = store.query_path_info(&hello_path()).await.unwrap();
837 assert!(info.is_some());
838 assert_eq!(info.unwrap().nar_hash, "sha256:aaa");
839 }
840
841 #[tokio::test]
842 async fn arc_dyn_store_is_valid_path() {
843 let store: std::sync::Arc<dyn Store> = std::sync::Arc::new(
844 TestStore::new().with_path(hello_info()),
845 );
846 assert!(store.is_valid_path(&hello_path()).await.unwrap());
847 assert!(!store.is_valid_path(&bash_path()).await.unwrap());
848 }
849
850 #[tokio::test]
851 async fn arc_dyn_store_query_all_valid_paths() {
852 let store: std::sync::Arc<dyn Store> = std::sync::Arc::new(
853 TestStore::new()
854 .with_path(hello_info())
855 .with_path(glibc_info()),
856 );
857 let paths = store.query_all_valid_paths().await.unwrap();
858 assert_eq!(paths.len(), 2);
859 }
860
861 #[tokio::test]
862 async fn arc_dyn_store_query_references() {
863 let store: std::sync::Arc<dyn Store> = std::sync::Arc::new(
864 TestStore::new()
865 .with_path(hello_info())
866 .with_path(glibc_info()),
867 );
868 let refs = store.query_references(&hello_path()).await.unwrap();
869 assert_eq!(refs.len(), 1);
870 assert_eq!(refs[0].to_absolute_path(), glibc_path().to_absolute_path());
871 }
872
873 #[tokio::test]
874 async fn arc_dyn_store_compute_closure() {
875 let store: std::sync::Arc<dyn Store> = std::sync::Arc::new(
876 TestStore::new()
877 .with_path(hello_info())
878 .with_path(glibc_info())
879 .with_path(bash_info()),
880 );
881 let closure = store.compute_closure(&[hello_path()]).await.unwrap();
882 assert_eq!(closure.len(), 3);
883 }
884
885 #[tokio::test]
886 async fn arc_dyn_store_default_methods_not_supported() {
887 let store: std::sync::Arc<dyn Store> = std::sync::Arc::new(
888 TestStore::new(),
889 );
890 assert!(store.collect_garbage(&GcOptions::default()).await.is_err());
891 assert!(store.add_to_store("x", b"data", &[]).await.is_err());
892 assert!(store.register_path(&hello_info()).await.is_err());
893 assert!(store.add_signatures(&hello_path(), &["sig".to_string()]).await.is_err());
894 assert!(store.query_referrers(&hello_path()).await.is_err());
895 }
896
897 #[tokio::test]
900 async fn box_dyn_store_query_path_info() {
901 let store: Box<dyn Store> = Box::new(
902 TestStore::new().with_path(hello_info()),
903 );
904 let info = store.query_path_info(&hello_path()).await.unwrap();
905 assert!(info.is_some());
906 }
907
908 #[tokio::test]
909 async fn box_dyn_store_is_valid_path() {
910 let store: Box<dyn Store> = Box::new(
911 TestStore::new().with_path(hello_info()),
912 );
913 assert!(store.is_valid_path(&hello_path()).await.unwrap());
914 assert!(!store.is_valid_path(&glibc_path()).await.unwrap());
915 }
916
917 #[test]
920 fn gc_result_fields() {
921 let result = GcResult {
922 paths_deleted: 42,
923 bytes_freed: 1_000_000,
924 };
925 assert_eq!(result.paths_deleted, 42);
926 assert_eq!(result.bytes_freed, 1_000_000);
927 }
928
929 #[test]
930 fn gc_options_with_values() {
931 let opts = GcOptions {
932 max_freed: 500_000,
933 delete_older_than: Some(3600),
934 };
935 assert_eq!(opts.max_freed, 500_000);
936 assert_eq!(opts.delete_older_than, Some(3600));
937 }
938
939 #[test]
940 fn gc_result_clone() {
941 let result = GcResult {
942 paths_deleted: 10,
943 bytes_freed: 5000,
944 };
945 let cloned = result.clone();
946 assert_eq!(cloned.paths_deleted, result.paths_deleted);
947 assert_eq!(cloned.bytes_freed, result.bytes_freed);
948 }
949
950 #[test]
951 fn gc_options_clone() {
952 let opts = GcOptions {
953 max_freed: 100,
954 delete_older_than: Some(60),
955 };
956 let cloned = opts.clone();
957 assert_eq!(cloned.max_freed, opts.max_freed);
958 assert_eq!(cloned.delete_older_than, opts.delete_older_than);
959 }
960
961 #[test]
964 fn store_error_debug_format() {
965 let e = StoreError::PathNotFound("/nix/store/abc".to_string());
966 let debug = format!("{e:?}");
967 assert!(debug.contains("PathNotFound"));
968 }
969
970 #[tokio::test]
973 async fn query_path_info_missing_returns_none() {
974 let store = TestStore::new();
975 let result = store.query_path_info(&hello_path()).await.unwrap();
976 assert!(result.is_none());
977 }
978
979 #[tokio::test]
982 async fn is_valid_path_false_when_missing() {
983 let store = TestStore::new();
984 assert!(!store.is_valid_path(&hello_path()).await.unwrap());
985 }
986
987 #[tokio::test]
990 async fn query_all_valid_paths_empty_store() {
991 let store = TestStore::new();
992 let paths = store.query_all_valid_paths().await.unwrap();
993 assert!(paths.is_empty());
994 }
995
996 #[test]
999 fn path_info_debug_format() {
1000 let info = hello_info();
1001 let debug = format!("{info:?}");
1002 assert!(debug.contains("hello"));
1003 assert!(debug.contains("sha256:aaa"));
1004 }
1005
1006 #[tokio::test]
1009 async fn compute_closure_handles_self_reference() {
1010 let mut info = bash_info();
1011 info.references = vec![bash_path().to_absolute_path()];
1012 let store = TestStore::new().with_path(info);
1013
1014 let closure = store.compute_closure(&[bash_path()]).await.unwrap();
1015 assert_eq!(closure.len(), 1);
1016 }
1017
1018 #[test]
1021 fn path_info_new_sets_path_and_hash() {
1022 let info = PathInfo::new("/nix/store/abc-x", "sha256:aaa");
1023 assert_eq!(info.path, "/nix/store/abc-x");
1024 assert_eq!(info.nar_hash, "sha256:aaa");
1025 assert_eq!(info.nar_size, 0);
1026 assert!(info.references.is_empty());
1027 assert!(info.deriver.is_none());
1028 assert!(info.signatures.is_empty());
1029 assert_eq!(info.registration_time, 0);
1030 assert!(info.content_address.is_none());
1031 }
1032
1033 #[test]
1034 fn path_info_new_accepts_string_owned() {
1035 let info = PathInfo::new(String::from("/nix/store/abc-x"), String::from("sha256:aaa"));
1036 assert_eq!(info.path, "/nix/store/abc-x");
1037 }
1038
1039 #[test]
1040 fn path_info_default_is_zero() {
1041 let info = PathInfo::default();
1042 assert!(info.path.is_empty());
1043 assert_eq!(info.nar_size, 0);
1044 }
1045
1046 #[test]
1049 fn path_info_display_includes_path_and_size() {
1050 let info = PathInfo {
1051 path: "/nix/store/abc-hello".to_string(),
1052 nar_hash: "sha256:aaa".to_string(),
1053 nar_size: 1024,
1054 references: vec![],
1055 deriver: None,
1056 signatures: vec![],
1057 registration_time: 0,
1058 content_address: None,
1059 };
1060 let s = info.to_string();
1061 assert!(s.contains("/nix/store/abc-hello"));
1062 assert!(s.contains("1024"));
1063 }
1064
1065 #[test]
1068 fn path_info_from_narinfo_full() {
1069 let narinfo = sui_compat::narinfo::NarInfo {
1070 store_path: "/nix/store/abc-hello".to_string(),
1071 url: "nar/abc.nar.xz".to_string(),
1072 compression: "xz".to_string(),
1073 file_hash: "sha256:fhash".to_string(),
1074 file_size: 500,
1075 nar_hash: "sha256:nhash".to_string(),
1076 nar_size: 1024,
1077 references: vec!["dep1".to_string(), "dep2".to_string()],
1078 deriver: Some("abc.drv".to_string()),
1079 signatures: vec!["k:s".to_string()],
1080 ca: Some("fixed:out:r:sha256:cafe".to_string()),
1081 };
1082 let info = PathInfo::from(&narinfo);
1083 assert_eq!(info.path, "/nix/store/abc-hello");
1084 assert_eq!(info.nar_hash, "sha256:nhash");
1085 assert_eq!(info.nar_size, 1024);
1086 assert_eq!(info.references.len(), 2);
1087 assert_eq!(info.deriver.as_deref(), Some("abc.drv"));
1088 assert_eq!(info.signatures, vec!["k:s"]);
1089 assert_eq!(info.content_address.as_deref(), Some("fixed:out:r:sha256:cafe"));
1090 assert_eq!(info.registration_time, 0);
1092 }
1093
1094 #[test]
1095 fn path_info_from_narinfo_minimal() {
1096 let narinfo = sui_compat::narinfo::NarInfo {
1097 store_path: "/nix/store/abc-leaf".to_string(),
1098 url: "nar/abc.nar".to_string(),
1099 compression: "none".to_string(),
1100 file_hash: "sha256:f".to_string(),
1101 file_size: 0,
1102 nar_hash: "sha256:n".to_string(),
1103 nar_size: 0,
1104 references: vec![],
1105 deriver: None,
1106 signatures: vec![],
1107 ca: None,
1108 };
1109 let info = PathInfo::from(&narinfo);
1110 assert!(info.references.is_empty());
1111 assert!(info.deriver.is_none());
1112 assert!(info.content_address.is_none());
1113 assert_eq!(info.nar_size, 0);
1114 }
1115
1116 #[test]
1119 fn store_error_is_path_not_found_true() {
1120 let e = StoreError::PathNotFound("/nix/store/x".to_string());
1121 assert!(e.is_path_not_found());
1122 assert!(!e.is_not_supported());
1123 }
1124
1125 #[test]
1126 fn store_error_is_path_not_found_false() {
1127 let e = StoreError::Database("x".to_string());
1128 assert!(!e.is_path_not_found());
1129 }
1130
1131 #[test]
1132 fn store_error_is_not_supported_true() {
1133 let e = StoreError::NotSupported("gc".to_string());
1134 assert!(e.is_not_supported());
1135 assert!(!e.is_path_not_found());
1136 }
1137
1138 #[test]
1139 fn store_error_is_not_supported_false_for_io() {
1140 let e = StoreError::Io(std::io::Error::new(
1141 std::io::ErrorKind::Other,
1142 "boom",
1143 ));
1144 assert!(!e.is_not_supported());
1145 assert!(!e.is_path_not_found());
1146 }
1147
1148 #[test]
1149 fn store_error_io_display_contains_message() {
1150 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing file");
1151 let e: StoreError = io_err.into();
1152 assert!(e.to_string().contains("missing file"));
1153 assert!(e.to_string().contains("io error"));
1154 }
1155
1156 #[test]
1157 fn store_error_not_supported_display() {
1158 let e = StoreError::NotSupported("gc not implemented".to_string());
1159 let s = e.to_string();
1160 assert!(s.contains("not supported"));
1161 assert!(s.contains("gc"));
1162 }
1163
1164 #[test]
1167 fn store_error_from_http_error_request() {
1168 use crate::http::HttpError;
1169 let http_err = HttpError::Request("dns failed".to_string());
1170 let store_err: StoreError = http_err.into();
1171 assert!(matches!(store_err, StoreError::Http(_)));
1172 assert!(store_err.to_string().contains("dns failed"));
1173 }
1174
1175 #[test]
1176 fn store_error_from_http_error_decode() {
1177 use crate::http::HttpError;
1178 let http_err = HttpError::Decode("bad utf-8".to_string());
1179 let store_err: StoreError = http_err.into();
1180 assert!(matches!(store_err, StoreError::Http(_)));
1181 assert!(store_err.to_string().contains("bad utf-8"));
1182 }
1183
1184 #[test]
1187 fn gc_options_with_max_freed() {
1188 let opts = GcOptions::default().with_max_freed(1024);
1189 assert_eq!(opts.max_freed, 1024);
1190 assert!(opts.delete_older_than.is_none());
1191 }
1192
1193 #[test]
1194 fn gc_options_with_delete_older_than() {
1195 let opts = GcOptions::default().with_delete_older_than(3600);
1196 assert_eq!(opts.delete_older_than, Some(3600));
1197 assert_eq!(opts.max_freed, 0);
1198 }
1199
1200 #[test]
1201 fn gc_options_chain_builder() {
1202 let opts = GcOptions::default()
1203 .with_max_freed(1_000_000)
1204 .with_delete_older_than(7200);
1205 assert_eq!(opts.max_freed, 1_000_000);
1206 assert_eq!(opts.delete_older_than, Some(7200));
1207 }
1208
1209 #[test]
1210 fn gc_options_eq() {
1211 let a = GcOptions::default().with_max_freed(100);
1212 let b = GcOptions {
1213 max_freed: 100,
1214 delete_older_than: None,
1215 };
1216 assert_eq!(a, b);
1217 }
1218
1219 #[test]
1222 fn gc_result_display_format() {
1223 let r = GcResult {
1224 paths_deleted: 5,
1225 bytes_freed: 1024,
1226 };
1227 let s = r.to_string();
1228 assert!(s.contains("5"));
1229 assert!(s.contains("1024"));
1230 assert!(s.contains("paths"));
1231 }
1232
1233 #[test]
1234 fn gc_result_default() {
1235 let r = GcResult::default();
1236 assert_eq!(r.paths_deleted, 0);
1237 assert_eq!(r.bytes_freed, 0);
1238 }
1239
1240 #[test]
1241 fn gc_result_eq() {
1242 let a = GcResult {
1243 paths_deleted: 1,
1244 bytes_freed: 100,
1245 };
1246 let b = GcResult {
1247 paths_deleted: 1,
1248 bytes_freed: 100,
1249 };
1250 let c = GcResult {
1251 paths_deleted: 1,
1252 bytes_freed: 200,
1253 };
1254 assert_eq!(a, b);
1255 assert_ne!(a, c);
1256 }
1257
1258 #[tokio::test]
1261 async fn compute_closure_dedup_with_diamond_deps() {
1262 let a_path =
1264 StorePath::from_absolute_path("/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-a")
1265 .unwrap();
1266 let b_path =
1267 StorePath::from_absolute_path("/nix/store/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-b")
1268 .unwrap();
1269 let c_path =
1270 StorePath::from_absolute_path("/nix/store/cccccccccccccccccccccccccccccccc-c")
1271 .unwrap();
1272 let d_path =
1273 StorePath::from_absolute_path("/nix/store/dddddddddddddddddddddddddddddddd-d")
1274 .unwrap();
1275
1276 let store = TestStore::new()
1277 .with_path(PathInfo {
1278 path: a_path.to_absolute_path(),
1279 nar_hash: "sha256:a".to_string(),
1280 nar_size: 1,
1281 references: vec![b_path.to_absolute_path(), c_path.to_absolute_path()],
1282 deriver: None,
1283 signatures: vec![],
1284 registration_time: 0,
1285 content_address: None,
1286 })
1287 .with_path(PathInfo {
1288 path: b_path.to_absolute_path(),
1289 nar_hash: "sha256:b".to_string(),
1290 nar_size: 1,
1291 references: vec![d_path.to_absolute_path()],
1292 deriver: None,
1293 signatures: vec![],
1294 registration_time: 0,
1295 content_address: None,
1296 })
1297 .with_path(PathInfo {
1298 path: c_path.to_absolute_path(),
1299 nar_hash: "sha256:c".to_string(),
1300 nar_size: 1,
1301 references: vec![d_path.to_absolute_path()],
1302 deriver: None,
1303 signatures: vec![],
1304 registration_time: 0,
1305 content_address: None,
1306 })
1307 .with_path(PathInfo {
1308 path: d_path.to_absolute_path(),
1309 nar_hash: "sha256:d".to_string(),
1310 nar_size: 1,
1311 references: vec![],
1312 deriver: None,
1313 signatures: vec![],
1314 registration_time: 0,
1315 content_address: None,
1316 });
1317
1318 let closure = store.compute_closure(&[a_path.clone()]).await.unwrap();
1319 assert_eq!(closure.len(), 4);
1321 let paths: Vec<String> = closure.iter().map(|p| p.to_absolute_path()).collect();
1322 assert!(paths.contains(&a_path.to_absolute_path()));
1323 assert!(paths.contains(&b_path.to_absolute_path()));
1324 assert!(paths.contains(&c_path.to_absolute_path()));
1325 assert!(paths.contains(&d_path.to_absolute_path()));
1326
1327 let d_count = paths
1329 .iter()
1330 .filter(|p| **p == d_path.to_absolute_path())
1331 .count();
1332 assert_eq!(d_count, 1);
1333 }
1334
1335 #[tokio::test]
1336 async fn compute_closure_propagates_query_error() {
1337 let store = TestStore::new().with_path(hello_info());
1341 let result = store.compute_closure(&[hello_path()]).await;
1342 assert!(result.is_err());
1343 }
1344
1345 #[test]
1348 fn path_info_eq_full_match() {
1349 let a = hello_info();
1350 let b = hello_info();
1351 assert_eq!(a, b);
1352 }
1353
1354 #[test]
1355 fn path_info_neq_when_size_differs() {
1356 let a = hello_info();
1357 let mut b = hello_info();
1358 b.nar_size = 9999;
1359 assert_ne!(a, b);
1360 }
1361
1362 #[test]
1363 fn path_info_neq_when_signatures_differ() {
1364 let a = hello_info();
1365 let mut b = hello_info();
1366 b.signatures = vec!["other:sig".to_string()];
1367 assert_ne!(a, b);
1368 }
1369
1370 #[test]
1371 fn path_info_neq_when_deriver_differs() {
1372 let a = hello_info();
1373 let mut b = hello_info();
1374 b.deriver = None;
1375 assert_ne!(a, b);
1376 }
1377
1378 struct MockStore {
1383 info: Option<PathInfo>,
1384 query_count: std::sync::atomic::AtomicUsize,
1385 valid_count: std::sync::atomic::AtomicUsize,
1386 }
1387
1388 impl MockStore {
1389 fn new(info: Option<PathInfo>) -> Self {
1390 Self {
1391 info,
1392 query_count: std::sync::atomic::AtomicUsize::new(0),
1393 valid_count: std::sync::atomic::AtomicUsize::new(0),
1394 }
1395 }
1396 fn query_count(&self) -> usize {
1397 self.query_count.load(std::sync::atomic::Ordering::Relaxed)
1398 }
1399 fn valid_count(&self) -> usize {
1400 self.valid_count.load(std::sync::atomic::Ordering::Relaxed)
1401 }
1402 }
1403
1404 #[async_trait::async_trait]
1405 impl Store for MockStore {
1406 async fn query_path_info(
1407 &self,
1408 _path: &StorePath,
1409 ) -> StoreResult<Option<PathInfo>> {
1410 self.query_count
1411 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1412 Ok(self.info.clone())
1413 }
1414 async fn is_valid_path(
1415 &self,
1416 _path: &StorePath,
1417 ) -> StoreResult<bool> {
1418 self.valid_count
1419 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1420 Ok(self.info.is_some())
1421 }
1422 async fn query_all_valid_paths(&self) -> StoreResult<Vec<StorePath>> {
1423 Ok(vec![])
1424 }
1425 }
1426
1427 #[tokio::test]
1428 async fn mock_store_counts_query_calls() {
1429 let store = MockStore::new(Some(hello_info()));
1430 let _ = store.query_path_info(&hello_path()).await.unwrap();
1431 let _ = store.query_path_info(&hello_path()).await.unwrap();
1432 let _ = store.query_path_info(&hello_path()).await.unwrap();
1433 assert_eq!(store.query_count(), 3);
1434 }
1435
1436 #[tokio::test]
1437 async fn mock_store_counts_valid_calls() {
1438 let store = MockStore::new(Some(hello_info()));
1439 let _ = store.is_valid_path(&hello_path()).await.unwrap();
1440 let _ = store.is_valid_path(&hello_path()).await.unwrap();
1441 assert_eq!(store.valid_count(), 2);
1442 }
1443
1444 #[tokio::test]
1445 async fn mock_store_via_dyn_dispatch() {
1446 let store: Box<dyn Store> = Box::new(MockStore::new(Some(hello_info())));
1447 let info = store.query_path_info(&hello_path()).await.unwrap();
1448 assert!(info.is_some());
1449 }
1450
1451 #[tokio::test]
1452 async fn mock_store_returns_none_when_empty() {
1453 let store = MockStore::new(None);
1454 let info = store.query_path_info(&hello_path()).await.unwrap();
1455 assert!(info.is_none());
1456 }
1457
1458 #[tokio::test]
1459 async fn mock_store_returns_false_when_empty() {
1460 let store = MockStore::new(None);
1461 assert!(!store.is_valid_path(&hello_path()).await.unwrap());
1462 }
1463
1464 #[tokio::test]
1465 async fn mock_store_query_all_returns_empty() {
1466 let store = MockStore::new(Some(hello_info()));
1467 let paths = store.query_all_valid_paths().await.unwrap();
1468 assert!(paths.is_empty());
1469 }
1470
1471 #[tokio::test]
1474 async fn vec_of_dyn_store_dispatch() {
1475 let stores: Vec<Box<dyn Store>> = vec![
1476 Box::new(TestStore::new().with_path(hello_info())),
1477 Box::new(MockStore::new(Some(hello_info()))),
1478 ];
1479 for store in &stores {
1480 let info = store.query_path_info(&hello_path()).await.unwrap();
1481 assert!(info.is_some());
1482 }
1483 }
1484
1485 #[tokio::test]
1488 async fn compute_closure_multiple_roots_no_overlap() {
1489 let store = TestStore::new()
1490 .with_path(hello_info())
1491 .with_path(glibc_info())
1492 .with_path(bash_info());
1493
1494 let closure = store
1495 .compute_closure(&[glibc_path(), bash_path()])
1496 .await
1497 .unwrap();
1498 assert_eq!(closure.len(), 2);
1500 }
1501
1502 #[tokio::test]
1505 async fn add_to_store_error_includes_method_name() {
1506 let store = TestStore::new();
1507 let err = store.add_to_store("x", b"d", &[]).await.unwrap_err();
1508 assert!(err.to_string().contains("add_to_store"));
1509 }
1510
1511 #[tokio::test]
1512 async fn register_path_error_includes_method_name() {
1513 let store = TestStore::new();
1514 let err = store.register_path(&hello_info()).await.unwrap_err();
1515 assert!(err.to_string().contains("register_path"));
1516 }
1517
1518 #[tokio::test]
1519 async fn add_signatures_error_includes_method_name() {
1520 let store = TestStore::new();
1521 let err = store
1522 .add_signatures(&hello_path(), &[])
1523 .await
1524 .unwrap_err();
1525 assert!(err.to_string().contains("add_signatures"));
1526 }
1527
1528 #[tokio::test]
1529 async fn query_referrers_error_includes_method_name() {
1530 let store = TestStore::new();
1531 let err = store.query_referrers(&hello_path()).await.unwrap_err();
1532 assert!(err.to_string().contains("query_referrers"));
1533 }
1534
1535 #[tokio::test]
1536 async fn collect_garbage_error_includes_method_name() {
1537 let store = TestStore::new();
1538 let err = store
1539 .collect_garbage(&GcOptions::default())
1540 .await
1541 .unwrap_err();
1542 assert!(err.to_string().contains("garbage collection"));
1543 }
1544
1545 #[test]
1548 fn store_trait_is_send_and_sync() {
1549 fn assert_send_sync<T: Send + Sync + ?Sized>() {}
1550 assert_send_sync::<dyn Store>();
1551 }
1552
1553 #[test]
1554 fn path_info_send_and_sync() {
1555 fn assert_send_sync<T: Send + Sync>() {}
1556 assert_send_sync::<PathInfo>();
1557 }
1558
1559 #[test]
1560 fn store_error_send_and_sync() {
1561 fn assert_send_sync<T: Send + Sync>() {}
1562 assert_send_sync::<StoreError>();
1563 }
1564
1565 #[test]
1566 fn store_error_internal_display() {
1567 let e = StoreError::Internal("something unexpected".to_string());
1568 let msg = e.to_string();
1569 assert!(msg.contains("internal error"));
1570 assert!(msg.contains("something unexpected"));
1571 }
1572
1573 #[test]
1576 fn corrupt_path_serialization_roundtrip() {
1577 let cp = CorruptPath {
1578 path: "/nix/store/abc-hello".to_string(),
1579 expected_hash: "sha256:aaa".to_string(),
1580 actual_hash: "sha256:bbb".to_string(),
1581 };
1582 let json = serde_json::to_string(&cp).unwrap();
1583 let cp2: CorruptPath = serde_json::from_str(&json).unwrap();
1584 assert_eq!(cp, cp2);
1585 }
1586
1587 #[test]
1588 fn verify_result_display() {
1589 let r = VerifyResult {
1590 total_checked: 10,
1591 valid_count: 8,
1592 corrupt: vec![],
1593 };
1594 assert!(r.to_string().contains("10 checked"));
1595 assert!(r.to_string().contains("8 valid"));
1596 }
1597
1598 #[test]
1599 fn gc_options_builder() {
1600 let opts = GcOptions::default()
1601 .with_max_freed(1024)
1602 .with_delete_older_than(3600);
1603 assert_eq!(opts.max_freed, 1024);
1604 assert_eq!(opts.delete_older_than, Some(3600));
1605 }
1606
1607 #[test]
1608 fn gc_result_display() {
1609 let r = GcResult {
1610 paths_deleted: 42,
1611 bytes_freed: 1_000_000,
1612 };
1613 assert!(r.to_string().contains("42 paths"));
1614 assert!(r.to_string().contains("1000000 bytes"));
1615 }
1616
1617 #[tokio::test]
1618 async fn verify_store_returns_not_supported() {
1619 let store = TestStore::new();
1620 let result = store.verify_store().await;
1621 assert!(result.is_err());
1622 match result.unwrap_err() {
1623 StoreError::NotSupported(msg) => {
1624 assert!(msg.contains("verify_store"));
1625 }
1626 other => panic!("expected NotSupported, got {other:?}"),
1627 }
1628 }
1629
1630 #[tokio::test]
1631 async fn delete_path_returns_not_supported() {
1632 let store = TestStore::new();
1633 let result = store.delete_path(&hello_path()).await;
1634 assert!(result.is_err());
1635 match result.unwrap_err() {
1636 StoreError::NotSupported(msg) => {
1637 assert!(msg.contains("delete_path"));
1638 }
1639 other => panic!("expected NotSupported, got {other:?}"),
1640 }
1641 }
1642
1643 #[test]
1646 fn optimise_result_default() {
1647 let result = OptimiseResult::default();
1648 assert_eq!(result.files_linked, 0);
1649 assert_eq!(result.bytes_saved, 0);
1650 }
1651
1652 #[test]
1653 fn optimise_result_display() {
1654 let result = OptimiseResult {
1655 files_linked: 42,
1656 bytes_saved: 1_048_576,
1657 };
1658 assert_eq!(
1659 result.to_string(),
1660 "Optimise: 42 files linked, 1048576 bytes saved"
1661 );
1662 }
1663
1664 #[test]
1665 fn optimise_result_display_zero() {
1666 let result = OptimiseResult::default();
1667 assert_eq!(
1668 result.to_string(),
1669 "Optimise: 0 files linked, 0 bytes saved"
1670 );
1671 }
1672
1673 #[test]
1674 fn optimise_result_clone_eq() {
1675 let a = OptimiseResult {
1676 files_linked: 10,
1677 bytes_saved: 5000,
1678 };
1679 let b = a.clone();
1680 assert_eq!(a, b);
1681 }
1682
1683 #[test]
1684 fn optimise_result_ne() {
1685 let a = OptimiseResult {
1686 files_linked: 10,
1687 bytes_saved: 5000,
1688 };
1689 let b = OptimiseResult {
1690 files_linked: 11,
1691 bytes_saved: 5000,
1692 };
1693 assert_ne!(a, b);
1694 }
1695
1696 #[test]
1697 fn optimise_result_debug() {
1698 let result = OptimiseResult {
1699 files_linked: 3,
1700 bytes_saved: 100,
1701 };
1702 let debug = format!("{result:?}");
1703 assert!(debug.contains("files_linked"));
1704 assert!(debug.contains("bytes_saved"));
1705 }
1706
1707 #[tokio::test]
1708 async fn optimise_store_returns_not_supported() {
1709 let store = TestStore::new();
1710 let result = store.optimise_store(false).await;
1711 assert!(result.is_err());
1712 match result.unwrap_err() {
1713 StoreError::NotSupported(msg) => {
1714 assert!(msg.contains("optimise_store"));
1715 }
1716 other => panic!("expected NotSupported, got {other:?}"),
1717 }
1718 }
1719
1720 #[tokio::test]
1721 async fn optimise_store_dry_run_returns_not_supported() {
1722 let store = TestStore::new();
1723 let result = store.optimise_store(true).await;
1724 assert!(result.is_err());
1725 match result.unwrap_err() {
1726 StoreError::NotSupported(msg) => {
1727 assert!(msg.contains("optimise_store"));
1728 }
1729 other => panic!("expected NotSupported, got {other:?}"),
1730 }
1731 }
1732}