|
|
… |
61 unchanged lines |
| 62 |
62 |
|
/// Memoised transitive dependencies. A mutex, not a `RefCell`, so a |
| 63 |
63 |
|
/// loaded state can be shared across threads by a host that caches it. |
| 64 |
64 |
|
closure: Mutex<HashMap<ChangeHash, Arc<HashSet<ChangeHash>>>>, |
|
65 |
+ |
/// `None` for a full state. For a history-only state (loaded for its |
|
66 |
+ |
/// changes and tree), the file graphs not built yet, by node: `files` |
|
67 |
+ |
/// holds only those loaded with [`State::load_graphs`]. |
|
68 |
+ |
deferred: Option<HashMap<NodeId, Deferred>>, |
| 65 |
69 |
|
} |
| 66 |
70 |
|
|
|
71 |
+ |
/// A file graph a history-only state has not built: its base from a |
|
72 |
+ |
/// version 2 checkpoint, if any, and the edits of later changes in the |
|
73 |
+ |
/// order they were applied (spec §11). Building it later gives the same |
|
74 |
+ |
/// graph, since graph construction does not depend on order (§8). |
|
75 |
+ |
#[derive(Clone, Default)] |
|
76 |
+ |
struct Deferred { |
|
77 |
+ |
base: Option<DeferredBase>, |
|
78 |
+ |
edits: Vec<(ChangeHash, Vec<Edit>)>, |
|
79 |
+ |
} |
|
80 |
+ |
|
|
81 |
+ |
#[derive(Clone)] |
|
82 |
+ |
enum DeferredBase { |
|
83 |
+ |
Inline(Value), |
|
84 |
+ |
Blob(ObjectRef), |
|
85 |
+ |
} |
|
86 |
+ |
|
| 67 |
87 |
|
impl Clone for State { |
| 68 |
88 |
|
fn clone(&self) -> State { |
| 69 |
89 |
|
State { |
|
|
… |
2 unchanged lines |
| 72 |
92 |
|
files: self.files.clone(), |
| 73 |
93 |
|
replaces: self.replaces.clone(), |
| 74 |
94 |
|
closure: Mutex::new(self.closure.lock().unwrap().clone()), |
|
95 |
+ |
deferred: self.deferred.clone(), |
| 75 |
96 |
|
} |
| 76 |
97 |
|
} |
| 77 |
98 |
|
} |
|
|
… |
82 unchanged lines |
| 160 |
181 |
|
mode: *mode, |
| 161 |
182 |
|
}, |
| 162 |
183 |
|
add @ TreeOp::AddNode { .. } => add.clone(), |
|
184 |
+ |
} |
|
185 |
+ |
} |
|
186 |
+ |
|
|
187 |
+ |
/// Apply one change's edits of one file (inserts and deletes; replaces |
|
188 |
+ |
/// live beside the graphs) to that file's graph. |
|
189 |
+ |
fn apply_file_edits(g: &mut FileGraph, h: ChangeHash, edits: Vec<Edit>) { |
|
190 |
+ |
let mut next = 0u32; |
|
191 |
+ |
for e in edits { |
|
192 |
+ |
match e { |
|
193 |
+ |
Edit::Insert { |
|
194 |
+ |
after, |
|
195 |
+ |
before, |
|
196 |
+ |
lines, |
|
197 |
+ |
.. |
|
198 |
+ |
} => { |
|
199 |
+ |
let first = next; |
|
200 |
+ |
next += lines.len() as u32; |
|
201 |
+ |
g.insert_owned( |
|
202 |
+ |
h, |
|
203 |
+ |
first, |
|
204 |
+ |
vertex(after.resolve(h)), |
|
205 |
+ |
vertex(before.resolve(h)), |
|
206 |
+ |
lines, |
|
207 |
+ |
); |
|
208 |
+ |
} |
|
209 |
+ |
Edit::Delete { atoms, .. } => { |
|
210 |
+ |
for r in atoms { |
|
211 |
+ |
g.delete(h, &r.resolve(h)); |
|
212 |
+ |
} |
|
213 |
+ |
} |
|
214 |
+ |
Edit::Replace { .. } => {} |
|
215 |
+ |
} |
| 163 |
216 |
|
} |
| 164 |
217 |
|
} |
| 165 |
218 |
|
|
| 166 |
219 |
|
impl State { |
| 167 |
220 |
|
pub fn new() -> Self { |
| 168 |
221 |
|
Self::default() |
|
222 |
+ |
} |
|
223 |
+ |
|
|
224 |
+ |
/// An empty state for history only: changes and tree are kept, file |
|
225 |
+ |
/// edits are recorded but no file graph is built until |
|
226 |
+ |
/// [`State::load_graphs`] asks for it. For log and other commands that |
|
227 |
+ |
/// need no file contents, or only a few files. |
|
228 |
+ |
pub fn new_history_only() -> Self { |
|
229 |
+ |
State { |
|
230 |
+ |
deferred: Some(HashMap::new()), |
|
231 |
+ |
..Self::default() |
|
232 |
+ |
} |
|
233 |
+ |
} |
|
234 |
+ |
|
|
235 |
+ |
/// Whether this state holds every file graph. |
|
236 |
+ |
pub fn is_full(&self) -> bool { |
|
237 |
+ |
self.deferred.as_ref().is_none_or(|d| d.is_empty()) |
|
238 |
+ |
} |
|
239 |
+ |
|
|
240 |
+ |
/// The graph of `node`, which must be loaded: asking a history-only |
|
241 |
+ |
/// state for a graph it has not built is a bug, never an empty file. |
|
242 |
+ |
fn graph(&self, node: &NodeId) -> Option<&FileGraph> { |
|
243 |
+ |
if let Some(d) = &self.deferred { |
|
244 |
+ |
assert!( |
|
245 |
+ |
!d.contains_key(node), |
|
246 |
+ |
"file graph {} of a history-only state was not loaded (State::load_graphs)", |
|
247 |
+ |
node.short(12) |
|
248 |
+ |
); |
|
249 |
+ |
} |
|
250 |
+ |
self.files.get(node) |
|
251 |
+ |
} |
|
252 |
+ |
|
|
253 |
+ |
/// Operations over every file need every graph. |
|
254 |
+ |
fn require_full(&self, what: &str) { |
|
255 |
+ |
assert!( |
|
256 |
+ |
self.is_full(), |
|
257 |
+ |
"{what} needs every file graph; this state was loaded for history only" |
|
258 |
+ |
); |
|
259 |
+ |
} |
|
260 |
+ |
|
|
261 |
+ |
/// Build the graphs of `nodes` in a history-only state: from their |
|
262 |
+ |
/// checkpoint base (blobs read in one batch through `load`) and their |
|
263 |
+ |
/// deferred edits. Nodes already built, or with no graph, are skipped. |
|
264 |
+ |
pub fn load_graphs( |
|
265 |
+ |
&mut self, |
|
266 |
+ |
nodes: &[NodeId], |
|
267 |
+ |
load: impl FnOnce(&[ObjectRef]) -> Result<Vec<Vec<u8>>, ObjectError>, |
|
268 |
+ |
) -> Result<(), ObjectError> { |
|
269 |
+ |
let Some(deferred) = self.deferred.as_mut() else { |
|
270 |
+ |
return Ok(()); |
|
271 |
+ |
}; |
|
272 |
+ |
let mut taken: Vec<(NodeId, Deferred)> = nodes |
|
273 |
+ |
.iter() |
|
274 |
+ |
.filter_map(|n| deferred.remove(n).map(|d| (*n, d))) |
|
275 |
+ |
.collect(); |
|
276 |
+ |
let refs: Vec<ObjectRef> = taken |
|
277 |
+ |
.iter() |
|
278 |
+ |
.filter_map(|(_, d)| match &d.base { |
|
279 |
+ |
Some(DeferredBase::Blob(r)) => Some(*r), |
|
280 |
+ |
_ => None, |
|
281 |
+ |
}) |
|
282 |
+ |
.collect(); |
|
283 |
+ |
let mut blobs = if refs.is_empty() { |
|
284 |
+ |
Vec::new() |
|
285 |
+ |
} else { |
|
286 |
+ |
load(&refs)? |
|
287 |
+ |
} |
|
288 |
+ |
.into_iter(); |
|
289 |
+ |
for (node, d) in taken.iter_mut() { |
|
290 |
+ |
let mut graph = match d.base.take() { |
|
291 |
+ |
Some(DeferredBase::Inline(v)) => FileGraph::from_value(&v)?, |
|
292 |
+ |
Some(DeferredBase::Blob(_)) => { |
|
293 |
+ |
let bytes = blobs |
|
294 |
+ |
.next() |
|
295 |
+ |
.ok_or_else(|| ObjectError::Schema("graph blobs: wrong count".into()))?; |
|
296 |
+ |
FileGraph::from_value(&cbor::decode(&bytes)?)? |
|
297 |
+ |
} |
|
298 |
+ |
None => FileGraph::default(), |
|
299 |
+ |
}; |
|
300 |
+ |
for (h, edits) in std::mem::take(&mut d.edits) { |
|
301 |
+ |
apply_file_edits(&mut graph, h, edits); |
|
302 |
+ |
} |
|
303 |
+ |
self.files.insert(*node, graph); |
|
304 |
+ |
} |
|
305 |
+ |
Ok(()) |
| 169 |
306 |
|
} |
| 170 |
307 |
|
|
| 171 |
308 |
|
pub fn len(&self) -> usize { |
|
|
… |
15 unchanged lines |
| 187 |
324 |
|
&self.tree |
| 188 |
325 |
|
} |
| 189 |
326 |
|
pub fn file(&self, node: &NodeId) -> Option<&FileGraph> { |
| 190 |
|
- |
self.files.get(node) |
|
327 |
+ |
self.graph(node) |
| 191 |
328 |
|
} |
| 192 |
329 |
|
|
| 193 |
330 |
|
/// Add a change whose dependencies are already present. |
|
|
… |
28 unchanged lines |
| 222 |
359 |
|
/// Serialize the whole state in payload version 1: change metadata, |
| 223 |
360 |
|
/// tree, file graphs, replaces, all in one value (spec §11). |
| 224 |
361 |
|
pub fn checkpoint_payload(&self) -> Vec<u8> { |
|
362 |
+ |
self.require_full("a checkpoint"); |
| 225 |
363 |
|
let mut nodes: Vec<&NodeId> = self.files.keys().collect(); |
| 226 |
364 |
|
nodes.sort(); |
| 227 |
365 |
|
let files = Value::Array( |
|
|
… |
9 unchanged lines |
| 237 |
375 |
|
/// (spec §11): the graphs of large files become blobs of their own. |
| 238 |
376 |
|
pub fn checkpoint_file_graphs(&self) -> Vec<(NodeId, Vec<u8>)> { |
| 239 |
377 |
|
use rayon::prelude::*; |
|
378 |
+ |
self.require_full("a checkpoint"); |
| 240 |
379 |
|
let mut nodes: Vec<(&NodeId, &FileGraph)> = self.files.iter().collect(); |
| 241 |
380 |
|
nodes.sort_by_key(|(n, _)| **n); |
| 242 |
381 |
|
nodes |
|
|
… |
168 unchanged lines |
| 411 |
550 |
|
} |
| 412 |
551 |
|
} |
| 413 |
552 |
|
Ok(out) |
|
553 |
+ |
} |
|
554 |
+ |
|
|
555 |
+ |
/// Decode a checkpoint payload for history only: a version 2 payload's |
|
556 |
+ |
/// graphs are noted, not read or built, and come later through |
|
557 |
+ |
/// [`State::load_graphs`]. A version 1 payload holds them inline and is |
|
558 |
+ |
/// decoded in full. |
|
559 |
+ |
pub fn from_checkpoint_history(bytes: &[u8]) -> Result<State, ObjectError> { |
|
560 |
+ |
let v = cbor::decode(bytes)?; |
|
561 |
+ |
let m = o::map(&v, "checkpoint payload")?; |
|
562 |
+ |
let version = o::uint(o::field(m, 0, "version")?, "version")?; |
|
563 |
+ |
if version != CHECKPOINT_INDEX_VERSION { |
|
564 |
+ |
return State::from_checkpoint_payload(bytes); |
|
565 |
+ |
} |
|
566 |
+ |
let mut st = State::payload_common(m)?; |
|
567 |
+ |
let mut deferred = HashMap::new(); |
|
568 |
+ |
for f in o::array(o::field(m, 3, "files")?, "files")? { |
|
569 |
+ |
let e = o::array(f, "file")?; |
|
570 |
+ |
if e.len() != 2 { |
|
571 |
+ |
return Err(ObjectError::Schema("file: expected [node, graph]".into())); |
|
572 |
+ |
} |
|
573 |
+ |
let node = NodeId::from_slice(o::bytes(&e[0], "node")?) |
|
574 |
+ |
.ok_or_else(|| ObjectError::Schema("node: expected 16 bytes".into()))?; |
|
575 |
+ |
let base = match &e[1] { |
|
576 |
+ |
Value::Bytes(_) => DeferredBase::Blob(o::oref(&e[1], "graph blob")?), |
|
577 |
+ |
other => DeferredBase::Inline(other.clone()), |
|
578 |
+ |
}; |
|
579 |
+ |
deferred.insert( |
|
580 |
+ |
node, |
|
581 |
+ |
Deferred { |
|
582 |
+ |
base: Some(base), |
|
583 |
+ |
edits: Vec::new(), |
|
584 |
+ |
}, |
|
585 |
+ |
); |
|
586 |
+ |
} |
|
587 |
+ |
st.deferred = Some(deferred); |
|
588 |
+ |
Ok(st) |
| 414 |
589 |
|
} |
| 415 |
590 |
|
|
| 416 |
591 |
|
/// Changes, tree and replaces of a payload of either version. |
|
|
… |
67 unchanged lines |
| 484 |
659 |
|
for (i, op) in c.tree_ops.iter().enumerate() { |
| 485 |
660 |
|
self.tree.apply(h, i as u32, &resolve_tree_op(op, h)); |
| 486 |
661 |
|
} |
|
662 |
+ |
if let Some(deferred) = self.deferred.as_mut() { |
|
663 |
+ |
// History only: edits of files not built wait, grouped per file. |
|
664 |
+ |
let mut per_node: HashMap<NodeId, Vec<Edit>> = HashMap::new(); |
|
665 |
+ |
for e in c.edits { |
|
666 |
+ |
match e { |
|
667 |
+ |
Edit::Replace { node, old, new } => { |
|
668 |
+ |
self.replaces.entry(node).or_default().push((h, old, new)) |
|
669 |
+ |
} |
|
670 |
+ |
e => per_node.entry(e.node()).or_default().push(e), |
|
671 |
+ |
} |
|
672 |
+ |
} |
|
673 |
+ |
for (node, edits) in per_node { |
|
674 |
+ |
match self.files.get_mut(&node) { |
|
675 |
+ |
Some(g) => apply_file_edits(g, h, edits), |
|
676 |
+ |
None => deferred.entry(node).or_default().edits.push((h, edits)), |
|
677 |
+ |
} |
|
678 |
+ |
} |
|
679 |
+ |
return; |
|
680 |
+ |
} |
| 487 |
681 |
|
let mut counters: HashMap<NodeId, u32> = HashMap::new(); |
| 488 |
682 |
|
for e in c.edits { |
| 489 |
683 |
|
match e { |
|
|
… |
88 unchanged lines |
| 578 |
772 |
|
} |
| 579 |
773 |
|
|
| 580 |
774 |
|
pub fn materialize_file(&self, node: &NodeId) -> Materialized { |
| 581 |
|
- |
self.files |
| 582 |
|
- |
.get(node) |
|
775 |
+ |
self.graph(node) |
| 583 |
776 |
|
.map(|g| g.materialize(self)) |
| 584 |
777 |
|
.unwrap_or_default() |
| 585 |
778 |
|
} |
| 586 |
779 |
|
|
| 587 |
780 |
|
/// Live lines of a text node in materialized order. |
| 588 |
781 |
|
pub fn live_lines(&self, node: &NodeId) -> Vec<(AtomKey, Vec<u8>)> { |
| 589 |
|
- |
let Some(g) = self.files.get(node) else { |
|
782 |
+ |
let Some(g) = self.graph(node) else { |
| 590 |
783 |
|
return vec![]; |
| 591 |
784 |
|
}; |
| 592 |
785 |
|
let m = g.materialize(self); |
|
|
… |
74 unchanged lines |
| 667 |
860 |
|
|
| 668 |
861 |
|
pub fn conflict_regions(&self, node: &NodeId) -> ConflictRegions { |
| 669 |
862 |
|
let mut out = ConflictRegions::default(); |
| 670 |
|
- |
let Some(g) = self.files.get(node) else { |
|
863 |
+ |
let Some(g) = self.graph(node) else { |
| 671 |
864 |
|
return out; |
| 672 |
865 |
|
}; |
| 673 |
866 |
|
let m = g.materialize(self); |
|
|
… |
31 unchanged lines |
| 705 |
898 |
|
} |
| 706 |
899 |
|
|
| 707 |
900 |
|
pub fn render_file(&self, node: &NodeId) -> (Vec<u8>, Materialized) { |
| 708 |
|
- |
let Some(g) = self.files.get(node) else { |
|
901 |
+ |
let Some(g) = self.graph(node) else { |
| 709 |
902 |
|
return (Vec::new(), Materialized::default()); |
| 710 |
903 |
|
}; |
| 711 |
904 |
|
let m = g.materialize(self); |
|
|
… |
5 unchanged lines |
| 717 |
910 |
|
/// rendering them is most of a snapshot's cost in a large tree. |
| 718 |
911 |
|
pub fn materialize(&self) -> Snapshot { |
| 719 |
912 |
|
use rayon::prelude::*; |
|
913 |
+ |
self.require_full("materializing a state"); |
| 720 |
914 |
|
let tree = self.tree.view(); |
| 721 |
915 |
|
let entries: Vec<(&String, &crate::tree::TreeEntry)> = tree.entries.iter().collect(); |
| 722 |
916 |
|
let rendered: Vec<(String, FileState)> = entries |
|
|
… |
309 unchanged lines |
| 1032 |
1226 |
|
matches!(&b.files["a"].content, FileContent::Text { bytes, .. } if bytes == b"2\n3\n") |
| 1033 |
1227 |
|
); |
| 1034 |
1228 |
|
assert!(State::from_checkpoint_payload(b"nonsense").is_err()); |
|
1229 |
+ |
} |
|
1230 |
+ |
|
|
1231 |
+ |
/// History-only states (O10f): the same state as a full load once its |
|
1232 |
+ |
/// graphs are built, by replay and from a version 2 checkpoint with |
|
1233 |
+ |
/// later changes; unloaded graphs are never read as empty. |
|
1234 |
+ |
#[test] |
|
1235 |
+ |
fn history_only_states_build_the_same_graphs_on_demand() { |
|
1236 |
+ |
let small = NodeId::random(); |
|
1237 |
+ |
let big = NodeId::random(); |
|
1238 |
+ |
let add = |node: NodeId, name: &str| TreeOp::AddNode { |
|
1239 |
+ |
node, |
|
1240 |
+ |
kind: NodeKind::File, |
|
1241 |
+ |
mode: Mode::TEXT, |
|
1242 |
+ |
parent: NodeId::ROOT, |
|
1243 |
+ |
name: name.into(), |
|
1244 |
+ |
}; |
|
1245 |
+ |
let lines: Vec<Vec<u8>> = (0..300) |
|
1246 |
+ |
.map(|i| format!("line {i}\n").into_bytes()) |
|
1247 |
+ |
.collect(); |
|
1248 |
+ |
let c1 = change( |
|
1249 |
+ |
vec![], |
|
1250 |
+ |
vec![add(small, "small"), add(big, "big")], |
|
1251 |
+ |
vec![ |
|
1252 |
+ |
Edit::Insert { |
|
1253 |
+ |
node: small, |
|
1254 |
+ |
after: AtomRef::Start, |
|
1255 |
+ |
before: AtomRef::End, |
|
1256 |
+ |
lines: vec![b"x\n".to_vec()], |
|
1257 |
+ |
}, |
|
1258 |
+ |
Edit::Insert { |
|
1259 |
+ |
node: big, |
|
1260 |
+ |
after: AtomRef::Start, |
|
1261 |
+ |
before: AtomRef::End, |
|
1262 |
+ |
lines, |
|
1263 |
+ |
}, |
|
1264 |
+ |
], |
|
1265 |
+ |
); |
|
1266 |
+ |
let h1 = c1.hash(); |
|
1267 |
+ |
let c2 = change( |
|
1268 |
+ |
vec![h1], |
|
1269 |
+ |
vec![], |
|
1270 |
+ |
vec![ |
|
1271 |
+ |
Edit::Delete { |
|
1272 |
+ |
node: big, |
|
1273 |
+ |
atoms: vec![AtomRange { |
|
1274 |
+ |
change: h1, |
|
1275 |
+ |
from: 2, |
|
1276 |
+ |
to: 9, |
|
1277 |
+ |
}], |
|
1278 |
+ |
}, |
|
1279 |
+ |
Edit::Insert { |
|
1280 |
+ |
node: big, |
|
1281 |
+ |
after: AtomRef::Atom { |
|
1282 |
+ |
change: h1, |
|
1283 |
+ |
index: 20, |
|
1284 |
+ |
}, |
|
1285 |
+ |
before: AtomRef::Atom { |
|
1286 |
+ |
change: h1, |
|
1287 |
+ |
index: 21, |
|
1288 |
+ |
}, |
|
1289 |
+ |
lines: vec![b"new a\n".to_vec(), b"new b\n".to_vec()], |
|
1290 |
+ |
}, |
|
1291 |
+ |
Edit::Insert { |
|
1292 |
+ |
node: small, |
|
1293 |
+ |
after: AtomRef::Atom { |
|
1294 |
+ |
change: h1, |
|
1295 |
+ |
index: 0, |
|
1296 |
+ |
}, |
|
1297 |
+ |
before: AtomRef::End, |
|
1298 |
+ |
lines: vec![b"y\n".to_vec()], |
|
1299 |
+ |
}, |
|
1300 |
+ |
], |
|
1301 |
+ |
); |
|
1302 |
+ |
let h2 = c2.hash(); |
|
1303 |
+ |
let c3 = change( |
|
1304 |
+ |
vec![h2], |
|
1305 |
+ |
vec![], |
|
1306 |
+ |
vec![Edit::Delete { |
|
1307 |
+ |
node: small, |
|
1308 |
+ |
atoms: vec![AtomRange { |
|
1309 |
+ |
change: h1, |
|
1310 |
+ |
from: 0, |
|
1311 |
+ |
to: 1, |
|
1312 |
+ |
}], |
|
1313 |
+ |
}], |
|
1314 |
+ |
); |
|
1315 |
+ |
|
|
1316 |
+ |
let mut full = State::new(); |
|
1317 |
+ |
for c in [&c1, &c2, &c3] { |
|
1318 |
+ |
full.add(c.clone()).unwrap(); |
|
1319 |
+ |
} |
|
1320 |
+ |
// By replay: nothing built until asked, then exactly the full graphs. |
|
1321 |
+ |
let mut hist = State::new_history_only(); |
|
1322 |
+ |
for c in [&c1, &c2, &c3] { |
|
1323 |
+ |
hist.add(c.clone()).unwrap(); |
|
1324 |
+ |
} |
|
1325 |
+ |
assert!(!hist.is_full() && hist.files.is_empty(), "no graph built"); |
|
1326 |
+ |
assert_eq!(hist.len(), 3); |
|
1327 |
+ |
hist.load_graphs(&[big], |_| unreachable!("no checkpoint")) |
|
1328 |
+ |
.unwrap(); |
|
1329 |
+ |
assert_eq!( |
|
1330 |
+ |
hist.render_file(&big).0, |
|
1331 |
+ |
full.render_file(&big).0, |
|
1332 |
+ |
"one file on its own" |
|
1333 |
+ |
); |
|
1334 |
+ |
assert!(!hist.is_full()); |
|
1335 |
+ |
hist.load_graphs(&[small, big], |_| unreachable!()).unwrap(); |
|
1336 |
+ |
assert!(hist.is_full()); |
|
1337 |
+ |
assert_eq!(hist.checkpoint_payload(), full.checkpoint_payload()); |
|
1338 |
+ |
|
|
1339 |
+ |
// From a version 2 checkpoint of c1+c2, then c3 applied later. |
|
1340 |
+ |
let mut base = State::new(); |
|
1341 |
+ |
base.add(c1.clone()).unwrap(); |
|
1342 |
+ |
base.add(c2.clone()).unwrap(); |
|
1343 |
+ |
let graphs = base.checkpoint_file_graphs(); |
|
1344 |
+ |
let mut blobs: HashMap<ObjectRef, Vec<u8>> = HashMap::new(); |
|
1345 |
+ |
let index = base.checkpoint_index(&graphs, |enc| { |
|
1346 |
+ |
let r = crate::hash::hash_object(crate::hash::ObjectType::Blob, enc); |
|
1347 |
+ |
blobs.insert(r, enc.to_vec()); |
|
1348 |
+ |
r |
|
1349 |
+ |
}); |
|
1350 |
+ |
let mut from_cp = State::from_checkpoint_history(&index).unwrap(); |
|
1351 |
+ |
from_cp.add(c3.clone()).unwrap(); |
|
1352 |
+ |
assert!(!from_cp.is_full()); |
|
1353 |
+ |
let mut fetched = 0; |
|
1354 |
+ |
from_cp |
|
1355 |
+ |
.load_graphs(&[small, big], |refs| { |
|
1356 |
+ |
fetched += refs.len(); |
|
1357 |
+ |
refs.iter() |
|
1358 |
+ |
.map(|r| { |
|
1359 |
+ |
blobs |
|
1360 |
+ |
.get(r) |
|
1361 |
+ |
.cloned() |
|
1362 |
+ |
.ok_or_else(|| ObjectError::Schema("missing".into())) |
|
1363 |
+ |
}) |
|
1364 |
+ |
.collect() |
|
1365 |
+ |
}) |
|
1366 |
+ |
.unwrap(); |
|
1367 |
+ |
assert_eq!( |
|
1368 |
+ |
fetched, 1, |
|
1369 |
+ |
"the large graph's blob, in one batch; the small one was inline" |
|
1370 |
+ |
); |
|
1371 |
+ |
assert_eq!(from_cp.checkpoint_payload(), full.checkpoint_payload()); |
|
1372 |
+ |
} |
|
1373 |
+ |
|
|
1374 |
+ |
#[test] |
|
1375 |
+ |
#[should_panic(expected = "was not loaded")] |
|
1376 |
+ |
fn a_history_only_state_never_reads_an_unloaded_graph_as_empty() { |
|
1377 |
+ |
let f = NodeId::random(); |
|
1378 |
+ |
let mut st = State::new_history_only(); |
|
1379 |
+ |
st.add(change( |
|
1380 |
+ |
vec![], |
|
1381 |
+ |
vec![TreeOp::AddNode { |
|
1382 |
+ |
node: f, |
|
1383 |
+ |
kind: NodeKind::File, |
|
1384 |
+ |
mode: Mode::TEXT, |
|
1385 |
+ |
parent: NodeId::ROOT, |
|
1386 |
+ |
name: "a".into(), |
|
1387 |
+ |
}], |
|
1388 |
+ |
vec![Edit::Insert { |
|
1389 |
+ |
node: f, |
|
1390 |
+ |
after: AtomRef::Start, |
|
1391 |
+ |
before: AtomRef::End, |
|
1392 |
+ |
lines: vec![b"a\n".to_vec()], |
|
1393 |
+ |
}], |
|
1394 |
+ |
)) |
|
1395 |
+ |
.unwrap(); |
|
1396 |
+ |
st.render_file(&f); |
|
1397 |
+ |
} |
|
1398 |
+ |
|
|
1399 |
+ |
#[test] |
|
1400 |
+ |
#[should_panic(expected = "needs every file graph")] |
|
1401 |
+ |
fn materializing_a_history_only_state_is_refused() { |
|
1402 |
+ |
let f = NodeId::random(); |
|
1403 |
+ |
let mut st = State::new_history_only(); |
|
1404 |
+ |
st.add(change( |
|
1405 |
+ |
vec![], |
|
1406 |
+ |
vec![TreeOp::AddNode { |
|
1407 |
+ |
node: f, |
|
1408 |
+ |
kind: NodeKind::File, |
|
1409 |
+ |
mode: Mode::TEXT, |
|
1410 |
+ |
parent: NodeId::ROOT, |
|
1411 |
+ |
name: "a".into(), |
|
1412 |
+ |
}], |
|
1413 |
+ |
vec![Edit::Insert { |
|
1414 |
+ |
node: f, |
|
1415 |
+ |
after: AtomRef::Start, |
|
1416 |
+ |
before: AtomRef::End, |
|
1417 |
+ |
lines: vec![b"a\n".to_vec()], |
|
1418 |
+ |
}], |
|
1419 |
+ |
)) |
|
1420 |
+ |
.unwrap(); |
|
1421 |
+ |
st.materialize(); |
| 1035 |
1422 |
|
} |
| 1036 |
1423 |
|
|
| 1037 |
1424 |
|
/// Version 2 payloads (spec §11): small graphs inline, large ones by |
|
|
… |
158 unchanged lines |