1use std::collections::HashMap;
6
7use serde::{Deserialize, Serialize};
8
9use super::manifest::ConfigProvenance;
10
11pub const MAGIC_BYTES: &[u8; 13] = b"SQRY_GRAPH_V7";
27
28pub const VERSION: u32 = 7;
32
33pub const MAGIC_BYTES_V7: &[u8; 13] = b"SQRY_GRAPH_V7";
38
39pub const MAGIC_BYTES_V8: &[u8; 13] = b"SQRY_GRAPH_V8";
45
46pub const MAGIC_BYTES_V9: &[u8; 13] = b"SQRY_GRAPH_V9";
53
54pub const MAGIC_BYTES_V10: &[u8; 14] = b"SQRY_GRAPH_V10";
61
62pub const MAGIC_BYTES_V11: &[u8; 14] = b"SQRY_GRAPH_V11";
77
78pub const MAGIC_BYTES_V12: &[u8; 14] = b"SQRY_GRAPH_V12";
100
101pub const MAGIC_BYTES_V13: &[u8; 14] = b"SQRY_GRAPH_V13";
110
111pub const MAGIC_BYTES_V14: &[u8; 14] = b"SQRY_GRAPH_V14";
124
125pub const MAGIC_BYTES_V15: &[u8; 14] = b"SQRY_GRAPH_V15";
138
139pub const LEGACY_VERSION_V7: u32 = 7;
142
143#[repr(u32)]
150#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
151pub enum FormatVersion {
152 V7 = 7,
154 V8 = 8,
156 V9 = 9,
160 V10 = 10,
164 V11 = 11,
170 V12 = 12,
174 V13 = 13,
179 V14 = 14,
188 V15 = 15,
197}
198
199impl FormatVersion {
200 #[must_use]
202 pub const fn magic(self) -> &'static [u8] {
203 match self {
204 Self::V7 => MAGIC_BYTES_V7.as_slice(),
205 Self::V8 => MAGIC_BYTES_V8.as_slice(),
206 Self::V9 => MAGIC_BYTES_V9.as_slice(),
207 Self::V10 => MAGIC_BYTES_V10.as_slice(),
208 Self::V11 => MAGIC_BYTES_V11.as_slice(),
209 Self::V12 => MAGIC_BYTES_V12.as_slice(),
210 Self::V13 => MAGIC_BYTES_V13.as_slice(),
211 Self::V14 => MAGIC_BYTES_V14.as_slice(),
212 Self::V15 => MAGIC_BYTES_V15.as_slice(),
213 }
214 }
215
216 #[must_use]
218 pub const fn as_u32(self) -> u32 {
219 self as u32
220 }
221
222 #[must_use]
226 pub fn from_magic(bytes: &[u8]) -> Option<Self> {
227 if bytes.len() >= MAGIC_BYTES_V15.len()
231 && bytes[..MAGIC_BYTES_V15.len()] == *MAGIC_BYTES_V15
232 {
233 return Some(Self::V15);
234 }
235 if bytes.len() >= MAGIC_BYTES_V14.len()
236 && bytes[..MAGIC_BYTES_V14.len()] == *MAGIC_BYTES_V14
237 {
238 return Some(Self::V14);
239 }
240 if bytes.len() >= MAGIC_BYTES_V13.len()
241 && bytes[..MAGIC_BYTES_V13.len()] == *MAGIC_BYTES_V13
242 {
243 return Some(Self::V13);
244 }
245 if bytes.len() >= MAGIC_BYTES_V12.len()
246 && bytes[..MAGIC_BYTES_V12.len()] == *MAGIC_BYTES_V12
247 {
248 return Some(Self::V12);
249 }
250 if bytes.len() >= MAGIC_BYTES_V11.len()
251 && bytes[..MAGIC_BYTES_V11.len()] == *MAGIC_BYTES_V11
252 {
253 return Some(Self::V11);
254 }
255 if bytes.len() >= MAGIC_BYTES_V10.len()
256 && bytes[..MAGIC_BYTES_V10.len()] == *MAGIC_BYTES_V10
257 {
258 return Some(Self::V10);
259 }
260 if bytes.len() < MAGIC_BYTES_V7.len() {
261 return None;
262 }
263 let prefix = &bytes[..MAGIC_BYTES_V7.len()];
264 if prefix == MAGIC_BYTES_V7 {
265 Some(Self::V7)
266 } else if prefix == MAGIC_BYTES_V8 {
267 Some(Self::V8)
268 } else if prefix == MAGIC_BYTES_V9 {
269 Some(Self::V9)
270 } else {
271 None
272 }
273 }
274}
275
276pub const CURRENT_VERSION: FormatVersion = FormatVersion::V15;
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct GraphHeader {
285 pub version: u32,
287
288 pub node_count: usize,
290
291 pub edge_count: usize,
293
294 pub string_count: usize,
296
297 pub file_count: usize,
299
300 pub timestamp: u64,
302
303 #[serde(default)]
305 pub config_provenance: Option<ConfigProvenance>,
306
307 #[serde(default)]
312 pub plugin_versions: HashMap<String, String>,
313
314 #[serde(default)]
328 pub fact_epoch: u64,
329}
330
331impl GraphHeader {
332 #[must_use]
334 pub fn new(
335 node_count: usize,
336 edge_count: usize,
337 string_count: usize,
338 file_count: usize,
339 ) -> Self {
340 Self {
341 version: VERSION,
342 node_count,
343 edge_count,
344 string_count,
345 file_count,
346 timestamp: std::time::SystemTime::now()
347 .duration_since(std::time::UNIX_EPOCH)
348 .unwrap_or_default()
349 .as_secs(),
350 config_provenance: None,
351 plugin_versions: HashMap::new(),
352 fact_epoch: 0,
353 }
354 }
355
356 #[must_use]
358 pub fn with_provenance(
359 node_count: usize,
360 edge_count: usize,
361 string_count: usize,
362 file_count: usize,
363 provenance: ConfigProvenance,
364 ) -> Self {
365 Self {
366 version: VERSION,
367 node_count,
368 edge_count,
369 string_count,
370 file_count,
371 timestamp: std::time::SystemTime::now()
372 .duration_since(std::time::UNIX_EPOCH)
373 .unwrap_or_default()
374 .as_secs(),
375 config_provenance: Some(provenance),
376 plugin_versions: HashMap::new(),
377 fact_epoch: 0,
378 }
379 }
380
381 #[must_use]
383 pub fn with_provenance_and_plugins(
384 node_count: usize,
385 edge_count: usize,
386 string_count: usize,
387 file_count: usize,
388 provenance: ConfigProvenance,
389 plugin_versions: HashMap<String, String>,
390 ) -> Self {
391 Self {
392 version: VERSION,
393 node_count,
394 edge_count,
395 string_count,
396 file_count,
397 timestamp: std::time::SystemTime::now()
398 .duration_since(std::time::UNIX_EPOCH)
399 .unwrap_or_default()
400 .as_secs(),
401 config_provenance: Some(provenance),
402 plugin_versions,
403 fact_epoch: 0,
404 }
405 }
406
407 #[must_use]
409 pub fn provenance(&self) -> Option<&ConfigProvenance> {
410 self.config_provenance.as_ref()
411 }
412
413 #[must_use]
415 pub fn has_provenance(&self) -> bool {
416 self.config_provenance.is_some()
417 }
418
419 #[must_use]
421 pub fn plugin_versions(&self) -> &HashMap<String, String> {
422 &self.plugin_versions
423 }
424
425 pub fn set_plugin_versions(&mut self, versions: HashMap<String, String>) {
427 self.plugin_versions = versions;
428 }
429
430 #[must_use]
437 pub fn fact_epoch(&self) -> u64 {
438 self.fact_epoch
439 }
440
441 pub fn set_fact_epoch(&mut self, epoch: u64) {
447 self.fact_epoch = epoch;
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454 use std::collections::HashMap;
455 use std::path::PathBuf;
456
457 fn make_test_provenance() -> ConfigProvenance {
458 ConfigProvenance {
459 config_file: PathBuf::from(".sqry/graph/config/config.json"),
460 config_checksum: "abc123def456".to_string(),
461 schema_version: 1,
462 overrides: HashMap::new(),
463 build_timestamp: std::time::SystemTime::now()
464 .duration_since(std::time::UNIX_EPOCH)
465 .unwrap_or_default()
466 .as_secs(),
467 build_host: Some("test-host".to_string()),
468 }
469 }
470
471 #[test]
472 fn test_magic_bytes() {
473 assert_eq!(MAGIC_BYTES, b"SQRY_GRAPH_V7");
474 assert_eq!(MAGIC_BYTES.len(), 13);
475 }
476
477 #[test]
478 fn test_version() {
479 assert_eq!(VERSION, 7);
480 }
481
482 #[test]
483 fn test_graph_header_new() {
484 let header = GraphHeader::new(100, 50, 200, 10);
485
486 assert_eq!(header.version, VERSION);
487 assert_eq!(header.node_count, 100);
488 assert_eq!(header.edge_count, 50);
489 assert_eq!(header.string_count, 200);
490 assert_eq!(header.file_count, 10);
491 assert!(header.timestamp > 0);
492 assert!(header.config_provenance.is_none());
493 }
494
495 #[test]
496 fn test_graph_header_with_provenance() {
497 let provenance = make_test_provenance();
498 let header = GraphHeader::with_provenance(100, 50, 200, 10, provenance);
499
500 assert_eq!(header.version, VERSION);
501 assert_eq!(header.node_count, 100);
502 assert_eq!(header.edge_count, 50);
503 assert!(header.config_provenance.is_some());
504 assert_eq!(
505 header.config_provenance.as_ref().unwrap().config_checksum,
506 "abc123def456"
507 );
508 }
509
510 #[test]
511 fn test_graph_header_provenance_method() {
512 let header = GraphHeader::new(10, 5, 20, 2);
513 assert!(header.provenance().is_none());
514
515 let provenance = make_test_provenance();
516 let header_with = GraphHeader::with_provenance(10, 5, 20, 2, provenance);
517 assert!(header_with.provenance().is_some());
518 assert_eq!(
519 header_with.provenance().unwrap().config_checksum,
520 "abc123def456"
521 );
522 }
523
524 #[test]
525 fn test_graph_header_has_provenance() {
526 let header = GraphHeader::new(10, 5, 20, 2);
527 assert!(!header.has_provenance());
528
529 let provenance = make_test_provenance();
530 let header_with = GraphHeader::with_provenance(10, 5, 20, 2, provenance);
531 assert!(header_with.has_provenance());
532 }
533
534 #[test]
535 fn test_graph_header_clone() {
536 let header = GraphHeader::new(100, 50, 200, 10);
537 let cloned = header.clone();
538
539 assert_eq!(header.version, cloned.version);
540 assert_eq!(header.node_count, cloned.node_count);
541 assert_eq!(header.edge_count, cloned.edge_count);
542 assert_eq!(header.string_count, cloned.string_count);
543 assert_eq!(header.file_count, cloned.file_count);
544 }
545
546 #[test]
547 fn test_graph_header_debug() {
548 let header = GraphHeader::new(100, 50, 200, 10);
549 let debug_str = format!("{header:?}");
550
551 assert!(debug_str.contains("GraphHeader"));
552 assert!(debug_str.contains("version"));
553 assert!(debug_str.contains("node_count"));
554 }
555
556 #[test]
557 fn test_graph_header_timestamp_is_recent() {
558 let header = GraphHeader::new(10, 5, 20, 2);
559 let now = std::time::SystemTime::now()
560 .duration_since(std::time::UNIX_EPOCH)
561 .unwrap()
562 .as_secs();
563
564 assert!(header.timestamp <= now);
566 assert!(header.timestamp >= now - 1);
567 }
568
569 #[test]
570 fn test_graph_header_zero_counts() {
571 let header = GraphHeader::new(0, 0, 0, 0);
572
573 assert_eq!(header.node_count, 0);
574 assert_eq!(header.edge_count, 0);
575 assert_eq!(header.string_count, 0);
576 assert_eq!(header.file_count, 0);
577 }
578
579 #[test]
580 fn test_graph_header_large_counts() {
581 let header = GraphHeader::new(1_000_000, 5_000_000, 10_000_000, 100_000);
582
583 assert_eq!(header.node_count, 1_000_000);
584 assert_eq!(header.edge_count, 5_000_000);
585 assert_eq!(header.string_count, 10_000_000);
586 assert_eq!(header.file_count, 100_000);
587 }
588
589 #[test]
590 fn test_graph_header_plugin_versions_empty_by_default() {
591 let header = GraphHeader::new(10, 5, 20, 2);
592 assert!(header.plugin_versions().is_empty());
593 }
594
595 #[test]
596 fn test_graph_header_set_plugin_versions() {
597 let mut header = GraphHeader::new(10, 5, 20, 2);
598
599 let mut versions = HashMap::new();
600 versions.insert("rust".to_string(), "3.3.0".to_string());
601 versions.insert("javascript".to_string(), "3.3.0".to_string());
602
603 header.set_plugin_versions(versions.clone());
604
605 assert_eq!(header.plugin_versions().len(), 2);
606 assert_eq!(
607 header.plugin_versions().get("rust"),
608 Some(&"3.3.0".to_string())
609 );
610 assert_eq!(
611 header.plugin_versions().get("javascript"),
612 Some(&"3.3.0".to_string())
613 );
614 }
615
616 #[test]
621 fn phase1_graph_header_new_defaults_fact_epoch_to_zero() {
622 let header = GraphHeader::new(10, 5, 20, 2);
623 assert_eq!(header.fact_epoch, 0);
624 assert_eq!(header.fact_epoch(), 0);
625 }
626
627 #[test]
628 fn phase1_graph_header_with_provenance_defaults_fact_epoch_to_zero() {
629 let header = GraphHeader::with_provenance(10, 5, 20, 2, make_test_provenance());
630 assert_eq!(header.fact_epoch, 0);
631 }
632
633 #[test]
634 fn phase1_graph_header_set_fact_epoch_round_trip() {
635 let mut header = GraphHeader::new(10, 5, 20, 2);
636 header.set_fact_epoch(42);
637 assert_eq!(header.fact_epoch(), 42);
638 }
639
640 #[test]
641 fn phase1_graph_header_postcard_round_trip_with_fact_epoch() {
642 let mut header = GraphHeader::new(100, 50, 200, 10);
643 header.set_fact_epoch(1_234_567);
644
645 let encoded = postcard::to_allocvec(&header).expect("encode");
646 let decoded: GraphHeader = postcard::from_bytes(&encoded).expect("decode");
647
648 assert_eq!(decoded.fact_epoch(), 1_234_567);
649 assert_eq!(decoded.node_count, 100);
650 assert_eq!(decoded.edge_count, 50);
651 }
652
653 #[test]
654 fn phase1_graph_header_fact_epoch_preserved_through_clone() {
655 let mut header = GraphHeader::new(10, 5, 20, 2);
656 header.set_fact_epoch(9_999);
657 let cloned = header.clone();
658 assert_eq!(cloned.fact_epoch(), 9_999);
659 }
660
661 #[test]
666 fn phase1_magic_bytes_v7_matches_legacy() {
667 assert_eq!(MAGIC_BYTES_V7, b"SQRY_GRAPH_V7");
668 assert_eq!(MAGIC_BYTES_V7, MAGIC_BYTES);
669 assert_eq!(MAGIC_BYTES_V7.len(), 13);
670 }
671
672 #[test]
673 fn phase1_magic_bytes_v8_is_distinct_and_13_bytes() {
674 assert_eq!(MAGIC_BYTES_V8, b"SQRY_GRAPH_V8");
675 assert_eq!(MAGIC_BYTES_V8.len(), 13);
676 assert_ne!(MAGIC_BYTES_V8, MAGIC_BYTES_V7);
677 }
678
679 #[test]
680 fn phase1_legacy_version_v7_equals_seven() {
681 assert_eq!(LEGACY_VERSION_V7, 7);
682 }
683
684 #[test]
685 fn phase1_format_version_discriminants() {
686 assert_eq!(FormatVersion::V7 as u32, 7);
687 assert_eq!(FormatVersion::V8 as u32, 8);
688 assert_eq!(FormatVersion::V9 as u32, 9);
689 assert_eq!(FormatVersion::V10 as u32, 10);
690 assert_eq!(FormatVersion::V11 as u32, 11);
691 assert_eq!(FormatVersion::V12 as u32, 12);
692 assert_eq!(FormatVersion::V13 as u32, 13);
693 assert_eq!(FormatVersion::V14 as u32, 14);
694 assert_eq!(FormatVersion::V15 as u32, 15);
695 }
696
697 #[test]
698 fn current_version_is_v15() {
699 assert_eq!(CURRENT_VERSION, FormatVersion::V15);
700 }
701
702 #[test]
703 fn shape_magic_bytes_v15_is_distinct_and_14_bytes() {
704 assert_eq!(MAGIC_BYTES_V15, b"SQRY_GRAPH_V15");
705 assert_eq!(MAGIC_BYTES_V15.len(), 14);
706 assert_ne!(MAGIC_BYTES_V15.as_slice(), MAGIC_BYTES_V14.as_slice());
707 assert_ne!(MAGIC_BYTES_V15.as_slice(), MAGIC_BYTES_V13.as_slice());
708 }
709
710 #[test]
713 fn shape_format_version_dispatch_v15_before_older() {
714 let mut buf = MAGIC_BYTES_V15.to_vec();
715 buf.extend_from_slice(&[0u8; 8]);
716 assert_eq!(FormatVersion::from_magic(&buf), Some(FormatVersion::V15));
717
718 let mut buf14 = MAGIC_BYTES_V14.to_vec();
719 buf14.extend_from_slice(&[0u8; 8]);
720 assert_eq!(FormatVersion::from_magic(&buf14), Some(FormatVersion::V14));
721 }
722
723 #[test]
724 fn shape_format_version_v15_magic_round_trip() {
725 let v = FormatVersion::V15;
726 let bytes = v.magic();
727 assert_eq!(bytes, MAGIC_BYTES_V15.as_slice());
728 assert_eq!(FormatVersion::from_magic(bytes), Some(v));
729 }
730
731 #[test]
732 fn t2_magic_bytes_v14_is_distinct_and_14_bytes() {
733 assert_eq!(MAGIC_BYTES_V14, b"SQRY_GRAPH_V14");
734 assert_eq!(MAGIC_BYTES_V14.len(), 14);
735 assert_ne!(MAGIC_BYTES_V14.as_slice(), MAGIC_BYTES_V13.as_slice());
736 assert_ne!(MAGIC_BYTES_V14.as_slice(), MAGIC_BYTES_V12.as_slice());
737 }
738
739 #[test]
740 fn t2_format_version_from_magic_v14() {
741 assert_eq!(
742 FormatVersion::from_magic(MAGIC_BYTES_V14),
743 Some(FormatVersion::V14),
744 );
745 }
746
747 #[test]
750 fn t2_format_version_dispatch_v14_before_older() {
751 let mut buf = MAGIC_BYTES_V14.to_vec();
752 buf.extend_from_slice(&[0u8; 8]);
753 assert_eq!(FormatVersion::from_magic(&buf), Some(FormatVersion::V14));
754
755 let mut buf13 = MAGIC_BYTES_V13.to_vec();
756 buf13.extend_from_slice(&[0u8; 8]);
757 assert_eq!(FormatVersion::from_magic(&buf13), Some(FormatVersion::V13));
758 }
759
760 #[test]
761 fn t2_format_version_v14_magic_round_trip() {
762 let v = FormatVersion::V14;
763 let bytes = v.magic();
764 assert_eq!(bytes, MAGIC_BYTES_V14.as_slice());
765 assert_eq!(FormatVersion::from_magic(bytes), Some(v));
766 }
767
768 #[test]
769 fn t3_magic_bytes_v13_is_distinct_and_14_bytes() {
770 assert_eq!(MAGIC_BYTES_V13, b"SQRY_GRAPH_V13");
771 assert_eq!(MAGIC_BYTES_V13.len(), 14);
772 assert_ne!(MAGIC_BYTES_V13.as_slice(), MAGIC_BYTES_V12.as_slice());
773 assert_ne!(MAGIC_BYTES_V13.as_slice(), MAGIC_BYTES_V10.as_slice());
774 }
775
776 #[test]
777 fn t3_format_version_from_magic_v13() {
778 assert_eq!(
779 FormatVersion::from_magic(MAGIC_BYTES_V13),
780 Some(FormatVersion::V13),
781 );
782 }
783
784 #[test]
790 fn t3_format_version_dispatch_v13_before_v12_v11_v10() {
791 let mut buf = MAGIC_BYTES_V13.to_vec();
792 buf.extend_from_slice(&[0u8; 8]);
793 assert_eq!(FormatVersion::from_magic(&buf), Some(FormatVersion::V13));
794
795 let mut buf12 = MAGIC_BYTES_V12.to_vec();
796 buf12.extend_from_slice(&[0u8; 8]);
797 assert_eq!(FormatVersion::from_magic(&buf12), Some(FormatVersion::V12));
798 }
799
800 #[test]
801 fn t3_format_version_v13_magic_round_trip() {
802 let v = FormatVersion::V13;
803 let bytes = v.magic();
804 assert_eq!(bytes, MAGIC_BYTES_V13.as_slice());
805 assert_eq!(FormatVersion::from_magic(bytes), Some(v));
806 }
807
808 #[test]
809 fn phase_a_magic_bytes_v11_is_distinct_and_14_bytes() {
810 assert_eq!(MAGIC_BYTES_V11, b"SQRY_GRAPH_V11");
811 assert_eq!(MAGIC_BYTES_V11.len(), 14);
812 assert_ne!(MAGIC_BYTES_V11, MAGIC_BYTES_V10);
813 }
814
815 #[test]
816 fn phase_a_format_version_from_magic_v11() {
817 assert_eq!(
818 FormatVersion::from_magic(MAGIC_BYTES_V11),
819 Some(FormatVersion::V11),
820 );
821 }
822
823 #[test]
829 fn phase_a_format_version_dispatch_v11_before_v10() {
830 let mut buf = MAGIC_BYTES_V11.to_vec();
831 buf.extend_from_slice(&[0u8; 8]);
833 assert_eq!(FormatVersion::from_magic(&buf), Some(FormatVersion::V11));
834
835 let mut buf10 = MAGIC_BYTES_V10.to_vec();
836 buf10.extend_from_slice(&[0u8; 8]);
837 assert_eq!(FormatVersion::from_magic(&buf10), Some(FormatVersion::V10));
838 }
839
840 #[test]
841 fn phase_a_format_version_v11_magic_round_trip() {
842 let v = FormatVersion::V11;
843 let bytes = v.magic();
844 assert_eq!(bytes, MAGIC_BYTES_V11.as_slice());
845 assert_eq!(FormatVersion::from_magic(bytes), Some(v));
846 }
847
848 #[test]
849 fn phase1_format_version_from_magic_v7() {
850 assert_eq!(
851 FormatVersion::from_magic(MAGIC_BYTES_V7),
852 Some(FormatVersion::V7),
853 );
854 }
855
856 #[test]
857 fn phase1_format_version_from_magic_v8() {
858 assert_eq!(
859 FormatVersion::from_magic(MAGIC_BYTES_V8),
860 Some(FormatVersion::V8),
861 );
862 }
863
864 #[test]
865 fn phase2_magic_bytes_v9_is_distinct_and_13_bytes() {
866 assert_eq!(MAGIC_BYTES_V9, b"SQRY_GRAPH_V9");
867 assert_eq!(MAGIC_BYTES_V9.len(), 13);
868 assert_ne!(MAGIC_BYTES_V9, MAGIC_BYTES_V7);
869 assert_ne!(MAGIC_BYTES_V9, MAGIC_BYTES_V8);
870 }
871
872 #[test]
873 fn phase2_format_version_from_magic_v9() {
874 assert_eq!(
875 FormatVersion::from_magic(MAGIC_BYTES_V9),
876 Some(FormatVersion::V9),
877 );
878 }
879
880 #[test]
881 fn phase1_format_version_from_magic_unknown() {
882 assert_eq!(FormatVersion::from_magic(b"SQRY_GRAPH_V1"), None);
883 assert_eq!(FormatVersion::from_magic(b"NOT_A_GRAPH_!"), None);
884 }
885
886 #[test]
887 fn phase1_format_version_magic_round_trip() {
888 for version in [
889 FormatVersion::V7,
890 FormatVersion::V8,
891 FormatVersion::V9,
892 FormatVersion::V10,
893 FormatVersion::V11,
894 FormatVersion::V12,
895 FormatVersion::V13,
896 ] {
897 let bytes = version.magic();
898 assert_eq!(FormatVersion::from_magic(bytes), Some(version));
899 }
900 }
901
902 #[test]
907 fn phase_beta_magic_bytes_v12_is_distinct_and_14_bytes() {
908 assert_eq!(MAGIC_BYTES_V12, b"SQRY_GRAPH_V12");
909 assert_eq!(MAGIC_BYTES_V12.len(), 14);
910 assert_ne!(MAGIC_BYTES_V12, MAGIC_BYTES_V11);
911 assert_ne!(MAGIC_BYTES_V12, MAGIC_BYTES_V10);
912 }
913
914 #[test]
915 fn phase_beta_format_version_from_magic_v12() {
916 assert_eq!(
917 FormatVersion::from_magic(MAGIC_BYTES_V12),
918 Some(FormatVersion::V12),
919 );
920 }
921
922 #[test]
927 fn phase_beta_format_version_dispatch_v12_before_v11_v10() {
928 let mut buf = MAGIC_BYTES_V12.to_vec();
929 buf.extend_from_slice(&[0u8; 8]);
930 assert_eq!(FormatVersion::from_magic(&buf), Some(FormatVersion::V12));
931
932 let mut buf11 = MAGIC_BYTES_V11.to_vec();
933 buf11.extend_from_slice(&[0u8; 8]);
934 assert_eq!(FormatVersion::from_magic(&buf11), Some(FormatVersion::V11));
935
936 let mut buf10 = MAGIC_BYTES_V10.to_vec();
937 buf10.extend_from_slice(&[0u8; 8]);
938 assert_eq!(FormatVersion::from_magic(&buf10), Some(FormatVersion::V10));
939 }
940
941 #[test]
942 fn phase_beta_format_version_v12_magic_round_trip() {
943 let v = FormatVersion::V12;
944 let bytes = v.magic();
945 assert_eq!(bytes, MAGIC_BYTES_V12.as_slice());
946 assert_eq!(FormatVersion::from_magic(bytes), Some(v));
947 }
948
949 #[test]
950 fn phase1_format_version_copy_eq_debug() {
951 let v = FormatVersion::V8;
952 let copied = v;
953 assert_eq!(v, copied);
954 assert_eq!(format!("{v:?}"), "V8");
955 }
956
957 #[test]
958 fn phase2_format_version_v9_copy_eq_debug() {
959 let v = FormatVersion::V9;
960 let copied = v;
961 assert_eq!(v, copied);
962 assert_eq!(format!("{v:?}"), "V9");
963 }
964
965 #[test]
966 fn test_graph_header_with_provenance_and_plugins() {
967 let provenance = make_test_provenance();
968
969 let mut plugin_versions = HashMap::new();
970 plugin_versions.insert("rust".to_string(), "3.3.0".to_string());
971 plugin_versions.insert("python".to_string(), "3.3.0".to_string());
972
973 let header = GraphHeader::with_provenance_and_plugins(
974 100,
975 50,
976 200,
977 10,
978 provenance,
979 plugin_versions.clone(),
980 );
981
982 assert_eq!(header.version, VERSION);
983 assert_eq!(header.node_count, 100);
984 assert!(header.config_provenance.is_some());
985 assert_eq!(header.plugin_versions().len(), 2);
986 assert_eq!(
987 header.plugin_versions().get("rust"),
988 Some(&"3.3.0".to_string())
989 );
990 }
991}