crates/kernel/ents-anchor/tests/binding_roundtrip.rs
binding_roundtrip.rshistorycomment on this file
| 1 | //! Round-trip fixture for [`Binding::deserialize`]/[`Binding::serialize_into`] |
| 2 | //! against the *existing* stored anchor format, captured from the current |
| 3 | //! code before this phase's changes: a byte-for-byte guarantee that |
| 4 | //! `Binding::Position` decodes, and re-encodes, the exact tree |
| 5 | //! `facet_git_tree::serialize_into(&anchor, ...)` has always produced. |
| 6 | //! |
| 7 | //! Every oid below is content-addressed from the bytes reconstructed in |
| 8 | //! this file, so a single corrupted byte in any hex constant — or in the |
| 9 | //! reconstructed content itself — fails an `assert_eq!` here rather than |
| 10 | //! silently drifting. |
| 11 | |
| 12 | #![allow( |
| 13 | clippy::unwrap_used, |
| 14 | clippy::expect_used, |
| 15 | clippy::panic, |
| 16 | reason = "integration test" |
| 17 | )] |
| 18 | |
| 19 | use ents_anchor::{Binding, LineRange}; |
| 20 | use facet_git_tree::ObjectStore; |
| 21 | use gix_object::tree::{Entry, EntryKind, EntryMode}; |
| 22 | use gix_object::{Kind, Tree, Write as _}; |
| 23 | |
| 24 | /// The root tree's oid, exactly as the current code produces it for an |
| 25 | /// anchor at `file.txt`, lines 3..=4, in a 10-line numbered file. |
| 26 | const ROOT: &str = "002b45e6824a3a9723ebc245104426c43ccf91be"; |
| 27 | |
| 28 | /// `blob` entry: [`ents_anchor::Anchor::blob`]'s 20 raw bytes, embedded — |
| 29 | /// equal to `CONTENT_OID`'s own bytes, by content addressing |
| 30 | /// (`anchor.retention`). |
| 31 | const BLOB_ENTRY_OID: &str = "4a3354a7c472ad13ffd9fb0e30d9a8fd66efd0b5"; |
| 32 | const BLOB_ENTRY_RAW: &str = "fa2da6e55caa540725b55c04d13f1e42b4c725ce"; |
| 33 | |
| 34 | /// `commit` entry: [`ents_anchor::Anchor::commit`]'s 20 raw bytes, embedded |
| 35 | /// (an arbitrary, best-effort commit id — it need not resolve to a real |
| 36 | /// object in this fixture). |
| 37 | const COMMIT_ENTRY_OID: &str = "a662e760fcd5534f59d7c7d72e401a646ac1a88f"; |
| 38 | const COMMIT_ENTRY_RAW: &str = "92cf309c4efcf8698a5bd8f82d56f68fd38cc963"; |
| 39 | |
| 40 | /// `content` entry: the anchored blob's own bytes, `"line 1\n"` through |
| 41 | /// `"line 10\n"`. |
| 42 | const CONTENT_OID: &str = "fa2da6e55caa540725b55c04d13f1e42b4c725ce"; |
| 43 | /// `context` entry: a three-line margin around lines 3..=4, `"line 1\n"` |
| 44 | /// through `"line 7\n"`. |
| 45 | const CONTEXT_OID: &str = "734156dc73cccb9703067e6366f3d09266e090dd"; |
| 46 | |
| 47 | const LINES_OID: &str = "b76e73cdb409fa346566f18e8f054dbdf04a7304"; |
| 48 | const LINES_SOME_OID: &str = "3433ad944c71f4b15c4de9e87568ae4cf03feb50"; |
| 49 | const LINES_END_OID: &str = "bf0d87ab1b2b0ec1a11a3973d2845b42413d9767"; |
| 50 | const LINES_START_OID: &str = "e440e5c842586965a7fb77deda2eca68612b1f53"; |
| 51 | |
| 52 | const PATH_OID: &str = "4c330738cc959751fb6760a91a50d9e58cfe5cb9"; |
| 53 | |
| 54 | fn oid(hex: &str) -> gix::ObjectId { |
| 55 | gix::ObjectId::from_hex(hex.as_bytes()).expect("valid hex oid") |
| 56 | } |
| 57 | |
| 58 | fn numbered(range: std::ops::RangeInclusive<u32>) -> String { |
| 59 | range.map(|n| format!("line {n}\n")).collect() |
| 60 | } |
| 61 | |
| 62 | fn write_blob(store: &ObjectStore, bytes: &[u8]) -> gix::ObjectId { |
| 63 | store.write_buf(Kind::Blob, bytes).expect("write blob") |
| 64 | } |
| 65 | |
| 66 | fn write_tree(store: &ObjectStore, mut entries: Vec<Entry>) -> gix::ObjectId { |
| 67 | entries.sort(); |
| 68 | store.write(&Tree { entries }).expect("write tree") |
| 69 | } |
| 70 | |
| 71 | fn entry(name: &str, kind: EntryKind, id: gix::ObjectId) -> Entry { |
| 72 | Entry { |
| 73 | mode: EntryMode::from(kind), |
| 74 | filename: name.into(), |
| 75 | oid: id, |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | /// Reconstruct the fixture's object set with `gix_object::Tree` + |
| 80 | /// `gix_object::Write`, asserting every intermediate oid along the way |
| 81 | /// (item 1 of the fixture contract) before returning the finished store. |
| 82 | fn build_fixture() -> (gix::ObjectId, ObjectStore) { |
| 83 | let store = ObjectStore::default(); |
| 84 | |
| 85 | let blob_entry = write_blob(&store, oid(BLOB_ENTRY_RAW).as_slice()); |
| 86 | assert_eq!(blob_entry.to_string(), BLOB_ENTRY_OID); |
| 87 | |
| 88 | let commit_entry = write_blob(&store, oid(COMMIT_ENTRY_RAW).as_slice()); |
| 89 | assert_eq!(commit_entry.to_string(), COMMIT_ENTRY_OID); |
| 90 | |
| 91 | let content = write_blob(&store, numbered(1..=10).as_bytes()); |
| 92 | assert_eq!(content.to_string(), CONTENT_OID); |
| 93 | |
| 94 | let context = write_blob(&store, numbered(1..=7).as_bytes()); |
| 95 | assert_eq!(context.to_string(), CONTEXT_OID); |
| 96 | |
| 97 | let end = write_blob(&store, b"4"); |
| 98 | assert_eq!(end.to_string(), LINES_END_OID); |
| 99 | let start = write_blob(&store, b"3"); |
| 100 | assert_eq!(start.to_string(), LINES_START_OID); |
| 101 | let some = write_tree( |
| 102 | &store, |
| 103 | vec![ |
| 104 | entry("end", EntryKind::Blob, end), |
| 105 | entry("start", EntryKind::Blob, start), |
| 106 | ], |
| 107 | ); |
| 108 | assert_eq!(some.to_string(), LINES_SOME_OID); |
| 109 | let lines = write_tree(&store, vec![entry("some", EntryKind::Tree, some)]); |
| 110 | assert_eq!(lines.to_string(), LINES_OID); |
| 111 | |
| 112 | let path = write_blob(&store, b"file.txt"); |
| 113 | assert_eq!(path.to_string(), PATH_OID); |
| 114 | |
| 115 | let root = write_tree( |
| 116 | &store, |
| 117 | vec![ |
| 118 | entry("blob", EntryKind::Blob, blob_entry), |
| 119 | entry("commit", EntryKind::Blob, commit_entry), |
| 120 | entry("content", EntryKind::Blob, content), |
| 121 | entry("context", EntryKind::Blob, context), |
| 122 | entry("lines", EntryKind::Tree, lines), |
| 123 | entry("path", EntryKind::Blob, path), |
| 124 | ], |
| 125 | ); |
| 126 | (root, store) |
| 127 | } |
| 128 | |
| 129 | /// Item 1 + 2 of the fixture contract: every reconstructed object's oid — |
| 130 | /// including the root's — matches the value the current code produces. |
| 131 | /// Content addressing means a single corrupted byte anywhere above fails |
| 132 | /// this assertion (or one of `build_fixture`'s own, reached first). |
| 133 | #[test] |
| 134 | fn reconstructing_the_fixture_reproduces_every_recorded_oid() { |
| 135 | let (root, _store) = build_fixture(); |
| 136 | assert_eq!(root.to_string(), ROOT); |
| 137 | } |
| 138 | |
| 139 | /// Item 3: `Binding::deserialize` decodes the fixture as |
| 140 | /// `Binding::Position`, recovering exactly the `Anchor` the current stored |
| 141 | /// format has always encoded. |
| 142 | #[test] |
| 143 | fn the_fixture_deserializes_as_a_position_binding() { |
| 144 | let (root, store) = build_fixture(); |
| 145 | |
| 146 | let binding = Binding::deserialize(&root, &store).expect("deserialize"); |
| 147 | let Binding::Position(anchor) = binding else { |
| 148 | panic!("the fixture must decode as Binding::Position"); |
| 149 | }; |
| 150 | assert_eq!(anchor.path, "file.txt"); |
| 151 | assert_eq!(anchor.lines, Some(LineRange { start: 3, end: 4 })); |
| 152 | assert_eq!(anchor.content, numbered(1..=10).into_bytes()); |
| 153 | assert_eq!(anchor.context, numbered(1..=7).into_bytes()); |
| 154 | assert_eq!(anchor.blob().to_string(), CONTENT_OID); |
| 155 | assert_eq!(anchor.commit().to_string(), COMMIT_ENTRY_RAW); |
| 156 | } |
| 157 | |
| 158 | /// Item 4: re-encoding the decoded binding into a fresh store reproduces |
| 159 | /// the fixture's root oid exactly — the existing anchor storage format is |
| 160 | /// unchanged, byte for byte, now that it decodes through `Binding`. |
| 161 | #[test] |
| 162 | fn re_encoding_reproduces_the_fixture_root_byte_for_byte() { |
| 163 | let (root, store) = build_fixture(); |
| 164 | let binding = Binding::deserialize(&root, &store).expect("deserialize"); |
| 165 | |
| 166 | let fresh = ObjectStore::default(); |
| 167 | let re_root = binding.serialize_into(&fresh).expect("serialize"); |
| 168 | assert_eq!(re_root.to_string(), ROOT); |
| 169 | } |