1use crate::error::{Result, TdbError};
7use sha2::{Digest, Sha256};
8use std::fs;
9use std::path::PathBuf;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::time::SystemTime;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
17pub enum BackupFormat {
18 NQuads,
20 Turtle,
22 JsonLd,
24 Binary,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
30pub enum BackupCompression {
31 None,
33 Zstd,
35 Gzip,
37}
38
39#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
43pub struct BackupManifest {
44 pub backup_id: String,
46 pub created_at_ms: i64,
48 pub dataset_name: String,
50 pub triple_count: usize,
52 pub format: BackupFormat,
54 pub compression: BackupCompression,
56 pub checksum: String,
58 pub size_bytes: usize,
60}
61
62pub struct BackupEngine {
70 backup_dir: PathBuf,
72 max_backups: usize,
74}
75
76impl BackupEngine {
77 pub fn new(backup_dir: PathBuf, max_backups: usize) -> Self {
79 Self {
80 backup_dir,
81 max_backups,
82 }
83 }
84
85 pub fn create_backup(
87 &self,
88 triples: &[(String, String, String)],
89 dataset_name: &str,
90 format: BackupFormat,
91 compression: BackupCompression,
92 ) -> Result<BackupManifest> {
93 fs::create_dir_all(&self.backup_dir).map_err(TdbError::Io)?;
94
95 let backup_id = Self::new_id();
96 let raw = Self::serialize(triples, format)?;
97 let data = Self::compress(raw, compression)?;
98 let checksum = Self::sha256_hex(&data);
99 let size_bytes = data.len();
100
101 let data_path = self.backup_dir.join(format!("{}.dat", backup_id));
102 fs::write(&data_path, &data).map_err(TdbError::Io)?;
103
104 let manifest = BackupManifest {
105 backup_id: backup_id.clone(),
106 created_at_ms: Self::now_ms(),
107 dataset_name: dataset_name.to_string(),
108 triple_count: triples.len(),
109 format,
110 compression,
111 checksum,
112 size_bytes,
113 };
114
115 let manifest_path = self.backup_dir.join(format!("{}.json", backup_id));
116 let json = serde_json::to_string_pretty(&manifest)
117 .map_err(|e| TdbError::Serialization(e.to_string()))?;
118 fs::write(&manifest_path, json).map_err(TdbError::Io)?;
119
120 Ok(manifest)
121 }
122
123 pub fn list_backups(&self) -> Vec<BackupManifest> {
125 let mut manifests = Vec::new();
126 let entries = match fs::read_dir(&self.backup_dir) {
127 Ok(e) => e,
128 Err(_) => return manifests,
129 };
130 for entry in entries.flatten() {
131 let path = entry.path();
132 if path.extension().and_then(|e| e.to_str()) == Some("json") {
133 if let Ok(json) = fs::read_to_string(&path) {
134 if let Ok(m) = serde_json::from_str::<BackupManifest>(&json) {
135 manifests.push(m);
136 }
137 }
138 }
139 }
140 manifests.sort_by(|a, b| b.created_at_ms.cmp(&a.created_at_ms));
141 manifests
142 }
143
144 pub fn restore(&self, backup_id: &str) -> Result<Vec<(String, String, String)>> {
146 let manifest = self.load_manifest(backup_id)?;
147 let data_path = self.backup_dir.join(format!("{}.dat", backup_id));
148 let data = fs::read(&data_path).map_err(TdbError::Io)?;
149 let raw = Self::decompress(data, manifest.compression)?;
150 Self::deserialize(&raw, manifest.format)
151 }
152
153 pub fn verify(&self, backup_id: &str) -> Result<bool> {
157 let manifest = self.load_manifest(backup_id)?;
158 let data_path = self.backup_dir.join(format!("{}.dat", backup_id));
159 let data = fs::read(&data_path).map_err(TdbError::Io)?;
160 Ok(Self::sha256_hex(&data) == manifest.checksum)
161 }
162
163 pub fn delete_backup(&self, backup_id: &str) -> Result<()> {
165 let data_path = self.backup_dir.join(format!("{}.dat", backup_id));
166 let manifest_path = self.backup_dir.join(format!("{}.json", backup_id));
167 if data_path.exists() {
168 fs::remove_file(&data_path).map_err(TdbError::Io)?;
169 }
170 if manifest_path.exists() {
171 fs::remove_file(&manifest_path).map_err(TdbError::Io)?;
172 }
173 Ok(())
174 }
175
176 pub fn prune_old(&self, keep_count: usize) -> usize {
180 let manifests = self.list_backups(); if manifests.len() <= keep_count {
182 return 0;
183 }
184 let mut deleted = 0usize;
185 for m in &manifests[keep_count..] {
186 if self.delete_backup(&m.backup_id).is_ok() {
187 deleted += 1;
188 }
189 }
190 deleted
191 }
192
193 fn load_manifest(&self, backup_id: &str) -> Result<BackupManifest> {
196 let path = self.backup_dir.join(format!("{}.json", backup_id));
197 let json = fs::read_to_string(&path).map_err(TdbError::Io)?;
198 serde_json::from_str(&json).map_err(|e| TdbError::Deserialization(e.to_string()))
199 }
200
201 fn new_id() -> String {
202 static COUNTER: AtomicU64 = AtomicU64::new(0);
203 let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
204 let t = SystemTime::now()
205 .duration_since(SystemTime::UNIX_EPOCH)
206 .unwrap_or_default();
207 format!("bkp_{}_{}_{}", t.as_millis(), t.subsec_nanos(), seq)
208 }
209
210 fn now_ms() -> i64 {
211 SystemTime::now()
212 .duration_since(SystemTime::UNIX_EPOCH)
213 .map(|d| d.as_millis() as i64)
214 .unwrap_or(0)
215 }
216
217 fn sha256_hex(data: &[u8]) -> String {
218 let mut hasher = Sha256::new();
219 hasher.update(data);
220 format!("{:x}", hasher.finalize())
221 }
222
223 fn serialize(triples: &[(String, String, String)], format: BackupFormat) -> Result<Vec<u8>> {
224 match format {
225 BackupFormat::NQuads | BackupFormat::Turtle => {
226 let mut out = String::new();
227 for (s, p, o) in triples {
228 out.push_str(&format!("<{}> <{}> <{}> .\n", s, p, o));
229 }
230 Ok(out.into_bytes())
231 }
232 BackupFormat::JsonLd => {
233 let mut items = Vec::new();
234 for (s, p, o) in triples {
235 items.push(serde_json::json!({
236 "@id": s,
237 p: [{ "@id": o }]
238 }));
239 }
240 serde_json::to_vec(&items).map_err(|e| TdbError::Serialization(e.to_string()))
241 }
242 BackupFormat::Binary => {
243 let mut buf = Vec::new();
245 buf.extend_from_slice(&(triples.len() as u64).to_le_bytes());
246 for (s, p, o) in triples {
247 for part in &[s, p, o] {
248 let bytes = part.as_bytes();
249 buf.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
250 buf.extend_from_slice(bytes);
251 }
252 }
253 Ok(buf)
254 }
255 }
256 }
257
258 fn deserialize(data: &[u8], format: BackupFormat) -> Result<Vec<(String, String, String)>> {
259 match format {
260 BackupFormat::NQuads | BackupFormat::Turtle => {
261 let text = std::str::from_utf8(data)
262 .map_err(|e| TdbError::Deserialization(e.to_string()))?;
263 let mut triples = Vec::new();
264 for line in text.lines() {
265 let line = line.trim();
266 if line.is_empty() || line.starts_with('#') {
267 continue;
268 }
269 let parts: Vec<&str> = line.splitn(4, "> <").collect();
271 if parts.len() < 3 {
272 continue;
273 }
274 let s = parts[0].trim_start_matches('<').to_string();
275 let p = parts[1].to_string();
276 let o = parts[2]
277 .trim_end_matches("> .")
278 .trim_end_matches(" .")
279 .trim_end_matches('>')
280 .to_string();
281 triples.push((s, p, o));
282 }
283 Ok(triples)
284 }
285 BackupFormat::JsonLd => {
286 let items: Vec<serde_json::Value> = serde_json::from_slice(data)
287 .map_err(|e| TdbError::Deserialization(e.to_string()))?;
288 let mut triples = Vec::new();
289 for item in items {
290 let s = item["@id"].as_str().unwrap_or("").to_string();
291 if let Some(obj) = item.as_object() {
292 for (key, val) in obj {
293 if key == "@id" {
294 continue;
295 }
296 if let Some(arr) = val.as_array() {
297 for v in arr {
298 let o = v["@id"].as_str().unwrap_or("").to_string();
299 triples.push((s.clone(), key.clone(), o));
300 }
301 }
302 }
303 }
304 }
305 Ok(triples)
306 }
307 BackupFormat::Binary => {
308 if data.len() < 8 {
309 return Err(TdbError::Deserialization(
310 "Binary data too short".to_string(),
311 ));
312 }
313 let count = u64::from_le_bytes(
314 data[..8]
315 .try_into()
316 .map_err(|_| TdbError::Deserialization("count slice".to_string()))?,
317 ) as usize;
318 let mut pos = 8usize;
319 let mut triples = Vec::with_capacity(count);
320 for _ in 0..count {
321 let s = read_str_at(data, &mut pos)?;
322 let p = read_str_at(data, &mut pos)?;
323 let o = read_str_at(data, &mut pos)?;
324 triples.push((s, p, o));
325 }
326 Ok(triples)
327 }
328 }
329 }
330
331 fn compress(data: Vec<u8>, compression: BackupCompression) -> Result<Vec<u8>> {
332 match compression {
333 BackupCompression::None => Ok(data),
334 BackupCompression::Zstd => oxiarc_zstd::encode_all(&data, 3)
335 .map_err(|e| TdbError::Other(format!("zstd compress: {}", e))),
336 BackupCompression::Gzip => {
337 use flate2::write::GzEncoder;
338 use flate2::Compression;
339 use std::io::Write;
340 let mut enc = GzEncoder::new(Vec::new(), Compression::default());
341 enc.write_all(&data)
342 .map_err(|e| TdbError::Other(format!("gzip write: {}", e)))?;
343 enc.finish()
344 .map_err(|e| TdbError::Other(format!("gzip finish: {}", e)))
345 }
346 }
347 }
348
349 fn decompress(data: Vec<u8>, compression: BackupCompression) -> Result<Vec<u8>> {
350 match compression {
351 BackupCompression::None => Ok(data),
352 BackupCompression::Zstd => oxiarc_zstd::decode_all(&data)
353 .map_err(|e| TdbError::Other(format!("zstd decompress: {}", e))),
354 BackupCompression::Gzip => {
355 use flate2::read::GzDecoder;
356 use std::io::Read;
357 let mut dec = GzDecoder::new(&data[..]);
358 let mut out = Vec::new();
359 dec.read_to_end(&mut out)
360 .map_err(|e| TdbError::Other(format!("gzip decode: {}", e)))?;
361 Ok(out)
362 }
363 }
364 }
365}
366
367fn read_str_at(data: &[u8], pos: &mut usize) -> Result<String> {
369 if *pos + 4 > data.len() {
370 return Err(TdbError::Deserialization("EOF reading length".to_string()));
371 }
372 let len = u32::from_le_bytes(
373 data[*pos..*pos + 4]
374 .try_into()
375 .map_err(|_| TdbError::Deserialization("len slice".to_string()))?,
376 ) as usize;
377 *pos += 4;
378 if *pos + len > data.len() {
379 return Err(TdbError::Deserialization("EOF reading string".to_string()));
380 }
381 let s = std::str::from_utf8(&data[*pos..*pos + len])
382 .map_err(|e| TdbError::Deserialization(e.to_string()))?
383 .to_string();
384 *pos += len;
385 Ok(s)
386}
387
388#[derive(Debug, Clone)]
392pub struct BackupDelta {
393 pub delta_id: String,
395 pub created_at_ms: i64,
397 pub added: Vec<(String, String, String)>,
399 pub removed: Vec<(String, String, String)>,
401}
402
403pub struct IncrementalBackup {
407 pub base_manifest: BackupManifest,
409 pub deltas: Vec<BackupDelta>,
411}
412
413impl IncrementalBackup {
414 pub fn new(base: BackupManifest) -> Self {
416 Self {
417 base_manifest: base,
418 deltas: Vec::new(),
419 }
420 }
421
422 pub fn add_delta(
424 &mut self,
425 added: Vec<(String, String, String)>,
426 removed: Vec<(String, String, String)>,
427 ) -> &BackupDelta {
428 static SEQ: AtomicU64 = AtomicU64::new(0);
429 let seq = SEQ.fetch_add(1, Ordering::Relaxed);
430 let t = SystemTime::now()
431 .duration_since(SystemTime::UNIX_EPOCH)
432 .unwrap_or_default();
433 let delta = BackupDelta {
434 delta_id: format!("delta_{}_{}_{}", t.as_millis(), t.subsec_nanos(), seq),
435 created_at_ms: t.as_millis() as i64,
436 added,
437 removed,
438 };
439 self.deltas.push(delta);
440 self.deltas.last().expect("just pushed")
441 }
442
443 pub fn reconstruct(&self) -> Vec<(String, String, String)> {
447 let mut current: std::collections::HashSet<(String, String, String)> =
448 std::collections::HashSet::new();
449 for delta in &self.deltas {
450 for t in &delta.removed {
451 current.remove(t);
452 }
453 for t in &delta.added {
454 current.insert(t.clone());
455 }
456 }
457 current.into_iter().collect()
458 }
459
460 pub fn delta_count(&self) -> usize {
462 self.deltas.len()
463 }
464
465 pub fn total_size(&self) -> usize {
467 self.deltas
468 .iter()
469 .map(|d| d.added.len() + d.removed.len())
470 .sum()
471 }
472}
473
474#[cfg(test)]
477mod tests {
478 use super::*;
479
480 fn sample_triples(n: usize) -> Vec<(String, String, String)> {
481 (0..n)
482 .map(|i| {
483 (
484 format!("http://ex.org/s{}", i),
485 format!("http://ex.org/p{}", i),
486 format!("http://ex.org/o{}", i),
487 )
488 })
489 .collect()
490 }
491
492 fn tmp_dir(name: &str) -> PathBuf {
493 let d = std::env::temp_dir().join(format!("oxirs_tdb_bkpeng_{}", name));
494 fs::remove_dir_all(&d).ok();
495 fs::create_dir_all(&d).unwrap();
496 d
497 }
498
499 fn dummy_manifest() -> BackupManifest {
500 BackupManifest {
501 backup_id: "base_001".to_string(),
502 created_at_ms: 0,
503 dataset_name: "test".to_string(),
504 triple_count: 0,
505 format: BackupFormat::NQuads,
506 compression: BackupCompression::None,
507 checksum: "abc123".to_string(),
508 size_bytes: 0,
509 }
510 }
511
512 #[test]
515 fn test_backup_engine_new() {
516 let dir = tmp_dir("new");
517 let engine = BackupEngine::new(dir.clone(), 5);
518 assert_eq!(engine.max_backups, 5);
519 assert_eq!(engine.backup_dir, dir);
520 fs::remove_dir_all(&dir).ok();
521 }
522
523 #[test]
524 fn test_create_backup_nquads_none() {
525 let dir = tmp_dir("create_nq");
526 let engine = BackupEngine::new(dir.clone(), 10);
527 let triples = sample_triples(5);
528 let m = engine
529 .create_backup(
530 &triples,
531 "test_ds",
532 BackupFormat::NQuads,
533 BackupCompression::None,
534 )
535 .unwrap();
536 assert_eq!(m.triple_count, 5);
537 assert_eq!(m.format, BackupFormat::NQuads);
538 assert_eq!(m.compression, BackupCompression::None);
539 assert!(!m.checksum.is_empty());
540 assert!(m.size_bytes > 0);
541 assert!(!m.backup_id.is_empty());
542 fs::remove_dir_all(&dir).ok();
543 }
544
545 #[test]
546 fn test_create_backup_binary_zstd() {
547 let dir = tmp_dir("create_bin_zstd");
548 let engine = BackupEngine::new(dir.clone(), 10);
549 let triples = sample_triples(10);
550 let m = engine
551 .create_backup(
552 &triples,
553 "ds",
554 BackupFormat::Binary,
555 BackupCompression::Zstd,
556 )
557 .unwrap();
558 assert_eq!(m.triple_count, 10);
559 assert_eq!(m.format, BackupFormat::Binary);
560 assert_eq!(m.compression, BackupCompression::Zstd);
561 fs::remove_dir_all(&dir).ok();
562 }
563
564 #[test]
565 fn test_create_backup_jsonld_gzip() {
566 let dir = tmp_dir("create_jsonld_gz");
567 let engine = BackupEngine::new(dir.clone(), 10);
568 let triples = sample_triples(3);
569 let m = engine
570 .create_backup(
571 &triples,
572 "ds",
573 BackupFormat::JsonLd,
574 BackupCompression::Gzip,
575 )
576 .unwrap();
577 assert_eq!(m.format, BackupFormat::JsonLd);
578 assert_eq!(m.compression, BackupCompression::Gzip);
579 fs::remove_dir_all(&dir).ok();
580 }
581
582 #[test]
583 fn test_create_backup_turtle_none() {
584 let dir = tmp_dir("create_turtle");
585 let engine = BackupEngine::new(dir.clone(), 10);
586 let triples = sample_triples(2);
587 let m = engine
588 .create_backup(
589 &triples,
590 "ds",
591 BackupFormat::Turtle,
592 BackupCompression::None,
593 )
594 .unwrap();
595 assert_eq!(m.format, BackupFormat::Turtle);
596 assert_eq!(m.triple_count, 2);
597 fs::remove_dir_all(&dir).ok();
598 }
599
600 #[test]
601 fn test_restore_nquads() {
602 let dir = tmp_dir("restore_nq");
603 let engine = BackupEngine::new(dir.clone(), 10);
604 let triples = sample_triples(4);
605 let m = engine
606 .create_backup(
607 &triples,
608 "ds",
609 BackupFormat::NQuads,
610 BackupCompression::None,
611 )
612 .unwrap();
613 let restored = engine.restore(&m.backup_id).unwrap();
614 assert_eq!(restored.len(), 4);
615 for t in &triples {
616 assert!(restored.contains(t), "missing triple {:?}", t);
617 }
618 fs::remove_dir_all(&dir).ok();
619 }
620
621 #[test]
622 fn test_restore_binary_zstd() {
623 let dir = tmp_dir("restore_bin_zstd");
624 let engine = BackupEngine::new(dir.clone(), 10);
625 let triples = sample_triples(7);
626 let m = engine
627 .create_backup(
628 &triples,
629 "ds",
630 BackupFormat::Binary,
631 BackupCompression::Zstd,
632 )
633 .unwrap();
634 let restored = engine.restore(&m.backup_id).unwrap();
635 assert_eq!(restored.len(), 7);
636 for t in &triples {
637 assert!(restored.contains(t));
638 }
639 fs::remove_dir_all(&dir).ok();
640 }
641
642 #[test]
643 fn test_restore_binary_gzip() {
644 let dir = tmp_dir("restore_bin_gz");
645 let engine = BackupEngine::new(dir.clone(), 10);
646 let triples = sample_triples(6);
647 let m = engine
648 .create_backup(
649 &triples,
650 "ds",
651 BackupFormat::Binary,
652 BackupCompression::Gzip,
653 )
654 .unwrap();
655 let restored = engine.restore(&m.backup_id).unwrap();
656 assert_eq!(restored.len(), 6);
657 fs::remove_dir_all(&dir).ok();
658 }
659
660 #[test]
661 fn test_verify_intact() {
662 let dir = tmp_dir("verify_ok");
663 let engine = BackupEngine::new(dir.clone(), 10);
664 let triples = sample_triples(3);
665 let m = engine
666 .create_backup(
667 &triples,
668 "ds",
669 BackupFormat::NQuads,
670 BackupCompression::None,
671 )
672 .unwrap();
673 assert!(engine.verify(&m.backup_id).unwrap());
674 fs::remove_dir_all(&dir).ok();
675 }
676
677 #[test]
678 fn test_verify_corrupted() {
679 let dir = tmp_dir("verify_corrupt");
680 let engine = BackupEngine::new(dir.clone(), 10);
681 let triples = sample_triples(3);
682 let m = engine
683 .create_backup(
684 &triples,
685 "ds",
686 BackupFormat::NQuads,
687 BackupCompression::None,
688 )
689 .unwrap();
690 fs::write(dir.join(format!("{}.dat", m.backup_id)), b"corrupted").unwrap();
692 assert!(!engine.verify(&m.backup_id).unwrap());
693 fs::remove_dir_all(&dir).ok();
694 }
695
696 #[test]
697 fn test_list_backups_empty() {
698 let dir = tmp_dir("list_empty");
699 let engine = BackupEngine::new(dir.clone(), 10);
700 assert!(engine.list_backups().is_empty());
701 fs::remove_dir_all(&dir).ok();
702 }
703
704 #[test]
705 fn test_list_backups_multiple_sorted() {
706 let dir = tmp_dir("list_multi");
707 let engine = BackupEngine::new(dir.clone(), 10);
708 let t = sample_triples(2);
709 engine
710 .create_backup(&t, "ds", BackupFormat::NQuads, BackupCompression::None)
711 .unwrap();
712 engine
713 .create_backup(&t, "ds", BackupFormat::Binary, BackupCompression::None)
714 .unwrap();
715 let list = engine.list_backups();
716 assert_eq!(list.len(), 2);
717 assert!(list[0].created_at_ms >= list[1].created_at_ms);
718 fs::remove_dir_all(&dir).ok();
719 }
720
721 #[test]
722 fn test_delete_backup() {
723 let dir = tmp_dir("delete");
724 let engine = BackupEngine::new(dir.clone(), 10);
725 let t = sample_triples(2);
726 let m = engine
727 .create_backup(&t, "ds", BackupFormat::NQuads, BackupCompression::None)
728 .unwrap();
729 engine.delete_backup(&m.backup_id).unwrap();
730 assert!(engine.list_backups().is_empty());
731 fs::remove_dir_all(&dir).ok();
732 }
733
734 #[test]
735 fn test_delete_nonexistent_is_ok() {
736 let dir = tmp_dir("delete_ne");
737 let engine = BackupEngine::new(dir.clone(), 10);
738 engine.delete_backup("no_such_id").unwrap();
740 fs::remove_dir_all(&dir).ok();
741 }
742
743 #[test]
744 fn test_prune_old_removes_oldest() {
745 let dir = tmp_dir("prune");
746 let engine = BackupEngine::new(dir.clone(), 10);
747 let t = sample_triples(1);
748 for _ in 0..5 {
749 engine
750 .create_backup(&t, "ds", BackupFormat::NQuads, BackupCompression::None)
751 .unwrap();
752 }
753 assert_eq!(engine.list_backups().len(), 5);
754 let removed = engine.prune_old(3);
755 assert_eq!(removed, 2);
756 assert_eq!(engine.list_backups().len(), 3);
757 fs::remove_dir_all(&dir).ok();
758 }
759
760 #[test]
761 fn test_prune_no_removal_needed() {
762 let dir = tmp_dir("prune_noop");
763 let engine = BackupEngine::new(dir.clone(), 10);
764 let t = sample_triples(1);
765 engine
766 .create_backup(&t, "ds", BackupFormat::NQuads, BackupCompression::None)
767 .unwrap();
768 let removed = engine.prune_old(5);
769 assert_eq!(removed, 0);
770 assert_eq!(engine.list_backups().len(), 1);
771 fs::remove_dir_all(&dir).ok();
772 }
773
774 #[test]
775 fn test_empty_triples_backup_restore() {
776 let dir = tmp_dir("empty_triples");
777 let engine = BackupEngine::new(dir.clone(), 10);
778 let triples: Vec<(String, String, String)> = vec![];
779 let m = engine
780 .create_backup(
781 &triples,
782 "ds",
783 BackupFormat::NQuads,
784 BackupCompression::None,
785 )
786 .unwrap();
787 assert_eq!(m.triple_count, 0);
788 let restored = engine.restore(&m.backup_id).unwrap();
789 assert!(restored.is_empty());
790 fs::remove_dir_all(&dir).ok();
791 }
792
793 #[test]
794 fn test_dataset_name_preserved_in_manifest() {
795 let dir = tmp_dir("ds_name");
796 let engine = BackupEngine::new(dir.clone(), 10);
797 let t = sample_triples(1);
798 let m = engine
799 .create_backup(
800 &t,
801 "my_dataset",
802 BackupFormat::Binary,
803 BackupCompression::None,
804 )
805 .unwrap();
806 assert_eq!(m.dataset_name, "my_dataset");
807 let list = engine.list_backups();
808 assert_eq!(list[0].dataset_name, "my_dataset");
809 fs::remove_dir_all(&dir).ok();
810 }
811
812 #[test]
815 fn test_incremental_new() {
816 let ib = IncrementalBackup::new(dummy_manifest());
817 assert_eq!(ib.base_manifest.backup_id, "base_001");
818 assert_eq!(ib.delta_count(), 0);
819 assert_eq!(ib.total_size(), 0);
820 }
821
822 #[test]
823 fn test_add_delta_returns_ref() {
824 let mut ib = IncrementalBackup::new(dummy_manifest());
825 let delta = ib.add_delta(
826 vec![("s".to_string(), "p".to_string(), "o".to_string())],
827 vec![],
828 );
829 assert!(!delta.delta_id.is_empty());
830 assert_eq!(delta.added.len(), 1);
831 assert_eq!(delta.removed.len(), 0);
832 }
833
834 #[test]
835 fn test_delta_count_multiple() {
836 let mut ib = IncrementalBackup::new(dummy_manifest());
837 ib.add_delta(vec![], vec![]);
838 ib.add_delta(vec![], vec![]);
839 assert_eq!(ib.delta_count(), 2);
840 }
841
842 #[test]
843 fn test_total_size_sums_adds_and_removes() {
844 let mut ib = IncrementalBackup::new(dummy_manifest());
845 ib.add_delta(
847 vec![("s1".to_string(), "p".to_string(), "o1".to_string())],
848 vec![("s2".to_string(), "p".to_string(), "o2".to_string())],
849 );
850 ib.add_delta(
852 vec![
853 ("s3".to_string(), "p".to_string(), "o3".to_string()),
854 ("s4".to_string(), "p".to_string(), "o4".to_string()),
855 ],
856 vec![],
857 );
858 assert_eq!(ib.total_size(), 4);
860 }
861
862 #[test]
863 fn test_reconstruct_add_only() {
864 let mut ib = IncrementalBackup::new(dummy_manifest());
865 let t1 = ("a".to_string(), "b".to_string(), "c".to_string());
866 let t2 = ("d".to_string(), "e".to_string(), "f".to_string());
867 ib.add_delta(vec![t1.clone(), t2.clone()], vec![]);
868 let result = ib.reconstruct();
869 assert_eq!(result.len(), 2);
870 assert!(result.contains(&t1));
871 assert!(result.contains(&t2));
872 }
873
874 #[test]
875 fn test_reconstruct_add_then_remove() {
876 let mut ib = IncrementalBackup::new(dummy_manifest());
877 let t1 = ("s1".to_string(), "p".to_string(), "o1".to_string());
878 let t2 = ("s2".to_string(), "p".to_string(), "o2".to_string());
879 ib.add_delta(vec![t1.clone(), t2.clone()], vec![]);
880 ib.add_delta(vec![], vec![t1.clone()]);
881 let result = ib.reconstruct();
882 assert_eq!(result.len(), 1);
883 assert!(result.contains(&t2));
884 assert!(!result.contains(&t1));
885 }
886
887 #[test]
888 fn test_reconstruct_empty() {
889 let ib = IncrementalBackup::new(dummy_manifest());
890 assert!(ib.reconstruct().is_empty());
891 }
892
893 #[test]
894 fn test_delta_timestamps_are_positive() {
895 let mut ib = IncrementalBackup::new(dummy_manifest());
896 let delta = ib.add_delta(vec![], vec![]);
897 assert!(delta.created_at_ms > 0);
898 }
899}