crates/kernel/ents-testutil/src/seed.rs
seed.rshistorycomment on this file
| 1 | //! Seeding helpers: members, meta entities, results, and code-ref history. |
| 2 | |
| 3 | use ents_model::{Member, MemberId, Provenance, ResultRecord, Status, namespace}; |
| 4 | use gix::refs::FullName; |
| 5 | use gix_hash::ObjectId; |
| 6 | use gix_object::{Find, Kind, Write}; |
| 7 | |
| 8 | use crate::commit::{CommitSpec, write_commit}; |
| 9 | use crate::keys::Keypair; |
| 10 | use crate::refs::MemRefStore; |
| 11 | |
| 12 | /// Write the empty tree into `objects` and return its id. |
| 13 | /// |
| 14 | /// # Examples |
| 15 | /// |
| 16 | /// ``` |
| 17 | /// use ents_testutil::{ObjectStore, empty_tree}; |
| 18 | /// |
| 19 | /// let objects = ObjectStore::default(); |
| 20 | /// let tree = empty_tree(&objects); |
| 21 | /// assert_eq!(tree.to_string(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904"); |
| 22 | /// ``` |
| 23 | pub fn empty_tree(objects: &impl Write) -> ObjectId { |
| 24 | objects |
| 25 | .write_buf(Kind::Tree, b"") |
| 26 | .expect("in-memory object write cannot fail") |
| 27 | } |
| 28 | |
| 29 | /// Serialize `entity` as its typed tree and land it on `refname` as a |
| 30 | /// mutation commit signed by `signer` when one is given. Parents come from |
| 31 | /// `refname`'s current tip. Returns the new tip. The refname is bound to |
| 32 | /// the signed content by the gate (`meta-ref.identity-binding`), not by |
| 33 | /// any commit trailer. |
| 34 | /// |
| 35 | /// # Examples |
| 36 | /// |
| 37 | /// ``` |
| 38 | /// use ents_testutil::{Keypair, MemRefStore, ObjectStore, write_meta_entity}; |
| 39 | /// use gix_ref_store::RefStoreRead; |
| 40 | /// |
| 41 | /// let refs = MemRefStore::default(); |
| 42 | /// let objects = ObjectStore::default(); |
| 43 | /// let name: gix::refs::FullName = "refs/meta/redactions/1".try_into().expect("valid"); |
| 44 | /// |
| 45 | /// let redaction = ents_model::Redaction::new( |
| 46 | /// gix_hash::ObjectId::null(gix_hash::Kind::Sha1), |
| 47 | /// "leaked credential", |
| 48 | /// ); |
| 49 | /// let tip = write_meta_entity(&refs, &objects, name.clone(), &redaction, None, 1_000); |
| 50 | /// assert_eq!(refs.get(name.as_ref()).expect("readable"), Some(tip)); |
| 51 | /// ``` |
| 52 | pub fn write_meta_entity<T: for<'facet> facet::Facet<'facet>>( |
| 53 | refs: &MemRefStore, |
| 54 | objects: &(impl Write + Find), |
| 55 | refname: FullName, |
| 56 | entity: &T, |
| 57 | signer: Option<&Keypair>, |
| 58 | seconds: i64, |
| 59 | ) -> ObjectId { |
| 60 | let tree = |
| 61 | facet_git_tree::serialize_into(entity, objects).expect("fixture entity always serializes"); |
| 62 | let message = format!("Mutate {}", refname.as_bstr()); |
| 63 | let parents = crate::refs_get(refs, &refname).into_iter().collect(); |
| 64 | let tip = write_commit( |
| 65 | objects, |
| 66 | &CommitSpec { |
| 67 | tree, |
| 68 | parents, |
| 69 | message, |
| 70 | seconds, |
| 71 | }, |
| 72 | signer, |
| 73 | ); |
| 74 | refs.set(refname.as_ref(), tip); |
| 75 | tip |
| 76 | } |
| 77 | |
| 78 | /// Enroll `id` as a member whose key is `key`'s public half, active, with |
| 79 | /// the given provenance, and return the member ref's new tip. |
| 80 | /// |
| 81 | /// The enrollment commit is signed by the member's own key — the |
| 82 | /// self-admitting shape the bootstrap window admits. |
| 83 | /// |
| 84 | /// # Examples |
| 85 | /// |
| 86 | /// ``` |
| 87 | /// use ents_model::Provenance; |
| 88 | /// use ents_testutil::{Keypair, MemRefStore, ObjectStore, enroll_member}; |
| 89 | /// |
| 90 | /// let refs = MemRefStore::default(); |
| 91 | /// let objects = ObjectStore::default(); |
| 92 | /// enroll_member(&refs, &objects, "jdc", &Keypair::from_seed(1), Provenance::AdminRegistered, 500); |
| 93 | /// ``` |
| 94 | pub fn enroll_member( |
| 95 | refs: &MemRefStore, |
| 96 | objects: &(impl Write + Find), |
| 97 | id: &str, |
| 98 | key: &Keypair, |
| 99 | provenance: Provenance, |
| 100 | seconds: i64, |
| 101 | ) -> ObjectId { |
| 102 | let member = Member::new(id, key.public_openssh(), provenance); |
| 103 | write_member(refs, objects, id, &member, Some(key), seconds) |
| 104 | } |
| 105 | |
| 106 | /// Land an arbitrary [`Member`] state on `refs/meta/member/<id>` — the |
| 107 | /// general form of [`enroll_member`], for revocations, unrevocations, and |
| 108 | /// promotions, signed by whoever `signer` is (an admin, usually). |
| 109 | /// |
| 110 | /// # Examples |
| 111 | /// |
| 112 | /// ``` |
| 113 | /// use ents_model::{Member, Provenance}; |
| 114 | /// use ents_testutil::{Keypair, MemRefStore, ObjectStore, write_member}; |
| 115 | /// |
| 116 | /// let refs = MemRefStore::default(); |
| 117 | /// let objects = ObjectStore::default(); |
| 118 | /// let key = Keypair::from_seed(1); |
| 119 | /// |
| 120 | /// let mut member = Member::new("jdc", key.public_openssh(), Provenance::AdminRegistered); |
| 121 | /// member.revoke(); |
| 122 | /// write_member(&refs, &objects, "jdc", &member, Some(&key), 900); |
| 123 | /// ``` |
| 124 | pub fn write_member( |
| 125 | refs: &MemRefStore, |
| 126 | objects: &(impl Write + Find), |
| 127 | id: &str, |
| 128 | member: &Member, |
| 129 | signer: Option<&Keypair>, |
| 130 | seconds: i64, |
| 131 | ) -> ObjectId { |
| 132 | let refname = namespace::member_ref(&MemberId::new(id)).expect("valid member id in fixture"); |
| 133 | write_meta_entity(refs, objects, refname, member, signer, seconds) |
| 134 | } |
| 135 | |
| 136 | /// Record a result of `status` for `effect` on the commit whose hex id |
| 137 | /// starts with `short_oid`, at the canonical |
| 138 | /// `refs/meta/results/<effect>/<short_oid>` ref (`effect.results-writeback`). |
| 139 | /// |
| 140 | /// The tree is a [`ResultRecord`] carrying `effect` and a target oid whose |
| 141 | /// hex begins with `short_oid` (`model.result-identity`): when `short_oid` |
| 142 | /// is a hex prefix it is right-padded with zeros to a full oid, so the |
| 143 | /// gate's identity binding recomputes the ref; when it is not hex, a null |
| 144 | /// target is used, sufficient for query scan tests that never gate. |
| 145 | /// |
| 146 | /// The result commit is unsigned unless `signer` is given — query tests |
| 147 | /// exercise scan semantics, gate tests exercise signatures. |
| 148 | /// |
| 149 | /// # Examples |
| 150 | /// |
| 151 | /// ``` |
| 152 | /// use ents_model::Status; |
| 153 | /// use ents_testutil::{MemRefStore, ObjectStore, record_result}; |
| 154 | /// |
| 155 | /// let refs = MemRefStore::default(); |
| 156 | /// let objects = ObjectStore::default(); |
| 157 | /// record_result(&refs, &objects, "unit", "abc123", Status::Pass, None, 1_000); |
| 158 | /// ``` |
| 159 | pub fn record_result( |
| 160 | refs: &MemRefStore, |
| 161 | objects: &(impl Write + Find), |
| 162 | effect: &str, |
| 163 | short_oid: &str, |
| 164 | status: Status, |
| 165 | signer: Option<&Keypair>, |
| 166 | seconds: i64, |
| 167 | ) -> ObjectId { |
| 168 | let refname = |
| 169 | namespace::result_ref(effect, short_oid).expect("valid result segments in fixture"); |
| 170 | let target = target_for(short_oid); |
| 171 | let record = ResultRecord::new(effect, target, status); |
| 172 | write_meta_entity(refs, objects, refname, &record, signer, seconds) |
| 173 | } |
| 174 | |
| 175 | /// An oid whose hex form begins with `short_oid`: right-pad a hex prefix |
| 176 | /// with zeros to 40 chars, or fall back to the null oid when `short_oid` |
| 177 | /// is not hex (query scan fixtures do not gate on the target). |
| 178 | fn target_for(short_oid: &str) -> ObjectId { |
| 179 | let padded = format!("{short_oid:0<40}"); |
| 180 | ObjectId::from_hex(padded.as_bytes()).unwrap_or_else(|_| ObjectId::null(gix_hash::Kind::Sha1)) |
| 181 | } |
| 182 | |
| 183 | /// Append `count` empty-tree commits on top of `refname`'s current tip |
| 184 | /// (creating the ref if absent), one second apart starting at |
| 185 | /// `start_seconds`, and return the new commits oldest-first. |
| 186 | /// |
| 187 | /// # Examples |
| 188 | /// |
| 189 | /// ``` |
| 190 | /// use ents_testutil::{MemRefStore, ObjectStore, advance_ref}; |
| 191 | /// use gix_ref_store::RefStoreRead; |
| 192 | /// |
| 193 | /// let refs = MemRefStore::default(); |
| 194 | /// let objects = ObjectStore::default(); |
| 195 | /// let commits = advance_ref(&refs, &objects, "refs/heads/main", 3, 100); |
| 196 | /// assert_eq!(commits.len(), 3); |
| 197 | /// |
| 198 | /// let name: gix::refs::FullName = "refs/heads/main".try_into().expect("valid"); |
| 199 | /// assert_eq!(refs.get(name.as_ref()).expect("readable"), commits.last().copied()); |
| 200 | /// ``` |
| 201 | pub fn advance_ref( |
| 202 | refs: &MemRefStore, |
| 203 | objects: &(impl Write + Find), |
| 204 | refname: &str, |
| 205 | count: usize, |
| 206 | start_seconds: i64, |
| 207 | ) -> Vec<ObjectId> { |
| 208 | let name: FullName = refname.try_into().expect("valid refname in fixture"); |
| 209 | let tree = empty_tree(objects); |
| 210 | let mut tip = crate::refs_get(refs, &name); |
| 211 | let mut out = Vec::with_capacity(count); |
| 212 | for i in 0..count { |
| 213 | let seconds = start_seconds.saturating_add(i64::try_from(i).unwrap_or(i64::MAX)); |
| 214 | let commit = write_commit( |
| 215 | objects, |
| 216 | &CommitSpec { |
| 217 | tree, |
| 218 | parents: tip.into_iter().collect(), |
| 219 | message: format!("{refname} commit {i} at {seconds}"), |
| 220 | seconds, |
| 221 | }, |
| 222 | None, |
| 223 | ); |
| 224 | tip = Some(commit); |
| 225 | out.push(commit); |
| 226 | } |
| 227 | if let Some(tip) = tip { |
| 228 | refs.set(name.as_ref(), tip); |
| 229 | } |
| 230 | out |
| 231 | } |