git-ents.gitmain
⌘K
foforge
retention.rs148 lines · 5.3 KB · rusthistorycomment on this file
1//! Integration tests for `anchor.retention`: the serialized anchor embeds
2//! the anchored blob and context blob as ordinary tree entries — reachable
3//! from the storing document's tree, reproducing the original blob's object
4//! id by content addressing, and never via a gitlink.
5
6#![allow(clippy::unwrap_used, clippy::expect_used, reason = "integration test")]
7
8use std::process::Command;
9
10use ents_anchor::LineRange;
11use facet_git_tree::{EntryKind, ObjectStore, RawTree, serialize_into};
12
13/// A stand-in for `ents-forge`'s `Comment` (this crate cannot depend
14/// on `ents-forge`, which itself depends on this crate): any struct
15/// embedding an anchor's tree by [`RawTree`] exercises the same
16/// reachability property `anchor.retention` requires.
17#[derive(facet::Facet)]
18struct Comment {
19 body: String,
20 anchor: RawTree,
21}
22
23fn fixture_repo(content: &str) -> tempfile::TempDir {
24 let dir = tempfile::tempdir().unwrap();
25 let run = |args: &[&str]| {
26 let status = Command::new("git")
27 .arg("-C")
28 .arg(dir.path())
29 .args(["-c", "user.name=test", "-c", "user.email=test@example.com"])
30 .args(args)
31 .status()
32 .unwrap();
33 assert!(status.success());
34 };
35 run(&["init", "-q"]);
36 std::fs::write(dir.path().join("file.txt"), content).unwrap();
37 run(&["add", "-A"]);
38 run(&["commit", "-q", "-m", "one"]);
39 dir
40}
41
42fn numbered(range: std::ops::RangeInclusive<u32>) -> String {
43 range.map(|n| format!("line {n}\n")).collect()
44}
45
46/// The serialized anchor's `content` and `context` entries are blobs — mode
47/// `100644`, never a gitlink (`160000`) — and `content`'s object id is the
48/// anchored blob's own id: referenced by content addressing, not copied
49/// under a new identity.
50// @relation(anchor.retention, scope=function, role=Verifies)
51#[test]
52fn retention_embeds_blobs_by_the_original_object_id_and_never_a_gitlink() {
53 let dir = fixture_repo(&numbered(1..=10));
54 let repo = gix::open(dir.path()).unwrap();
55 let anchor = ents_anchor::capture(
56 &repo,
57 "HEAD",
58 "file.txt",
59 Some(LineRange { start: 3, end: 4 }),
60 )
61 .unwrap();
62
63 let store = ObjectStore::default();
64 let root = serialize_into(&anchor, &store).expect("serialize");
65 let entries = store.get_tree(&root).expect("anchor tree");
66
67 for entry in &entries {
68 assert_ne!(
69 entry.mode.kind(),
70 EntryKind::Commit,
71 "a gitlink retains nothing (anchor.retention): {:?}",
72 entry.filename
73 );
74 }
75 let content = entries
76 .iter()
77 .find(|e| e.filename == "content")
78 .expect("content entry");
79 assert_eq!(content.mode.kind(), EntryKind::Blob);
80 assert_eq!(
81 content.oid,
82 anchor.blob(),
83 "content addressing must reproduce the anchored blob's own id"
84 );
85 let context = entries
86 .iter()
87 .find(|e| e.filename == "context")
88 .expect("context entry");
89 assert_eq!(context.mode.kind(), EntryKind::Blob);
90}
91
92/// The anchored content stays reachable from the storing document's own
93/// tree: walking the comment's tree (the shape `refs/meta/comments/*`
94/// points at) reaches the anchored blob, so the ref keeps it alive through
95/// force-push, branch deletion, and gc with no special-casing.
96// @relation(anchor.retention, scope=function, role=Verifies)
97#[test]
98fn anchored_content_is_reachable_from_the_storing_documents_tree() {
99 let dir = fixture_repo(&numbered(1..=10));
100 let repo = gix::open(dir.path()).unwrap();
101 let anchor = ents_anchor::capture(&repo, "HEAD", "file.txt", None).unwrap();
102
103 let store = ObjectStore::default();
104 let anchor_tree = serialize_into(&anchor, &store).expect("serialize anchor");
105 let comment = Comment {
106 body: "anchored".to_owned(),
107 anchor: RawTree::new(anchor_tree),
108 };
109 let root = serialize_into(&comment, &store).expect("serialize comment");
110
111 // Walk every tree reachable from the comment root; the anchored blob
112 // must be among the reachable objects.
113 let mut stack = vec![root];
114 let mut found = false;
115 while let Some(tree) = stack.pop() {
116 for entry in store.get_tree(&tree).expect("tree") {
117 match entry.mode.kind() {
118 EntryKind::Tree => stack.push(entry.oid),
119 _ => {
120 if entry.oid == anchor.blob() {
121 found = true;
122 }
123 }
124 }
125 }
126 }
127 assert!(
128 found,
129 "the anchored blob must be reachable from the comment's own tree"
130 );
131}
132
133/// A captured anchor round-trips through its tree unchanged — the struct is
134/// the schema, and the retained bytes survive storage verbatim, non-ASCII
135/// included.
136// @relation(anchor.retention, scope=function, role=Verifies)
137#[test]
138fn anchor_round_trips_through_its_tree() {
139 let dir = fixture_repo("line 1\nline 2\n\u{fe}\u{ff} non-ascii bytes\n");
140 let repo = gix::open(dir.path()).unwrap();
141 for lines in [None, Some(LineRange { start: 2, end: 3 })] {
142 let anchor = ents_anchor::capture(&repo, "HEAD", "file.txt", lines).unwrap();
143 let store = ObjectStore::default();
144 let root = serialize_into(&anchor, &store).unwrap();
145 let back: ents_anchor::Anchor = facet_git_tree::deserialize(&root, &store).unwrap();
146 assert_eq!(back, anchor);
147 }
148}