Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_austenite/tests/memo.rs

24.2 KiB, 42 runs

created by r1870400018:58623, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! The incremental memo, proved on the native recompile path.
2//!
3//! The master goal is to swap Austenite in for Daimond's Typst-wasm compiler; the blocker is that a
4//! straight swap re-authors and re-emits the whole document on every keystroke (~15x the live-view
5//! latency). The [`memo`](oxedyne_fe2o3_austenite::memo) layer caches the two costly content-addressed
6//! stages -- block authoring and page emit -- so an edit recomputes only what changed. These tests hold
7//! that memo to the one standard that matters: the memo must never change a single output byte. A warm
8//! recompile after an edit must be byte-identical to a cold compile of the edited source, while all but
9//! the one edited block hit the authoring cache and every page outside the edit's pagination cascade hits
10//! the emit cache.
11
12use oxedyne_fe2o3_austenite::bib::Bibliography;
13use oxedyne_fe2o3_austenite::compile::{
14 author_and_run_memo,
15 Assembled,
16};
17use oxedyne_fe2o3_austenite::doc::{
18 Block,
19 Segment,
20};
21use oxedyne_fe2o3_austenite::emit::svg;
22use oxedyne_fe2o3_austenite::fonts::{
23 self,
24 FaceResolver,
25};
26use oxedyne_fe2o3_austenite::memo::Memo;
27use oxedyne_fe2o3_austenite::page::PageGeometry;
28use oxedyne_fe2o3_austenite::theme::Theme;
29
30use oxedyne_fe2o3_core::prelude::*;
31
32use std::sync::Arc;
33use std::time::Instant;
34
35/// A deterministic body of prose paragraphs. Each is a few sentences of varying-length words, enough to
36/// break into several lines and paginate over many pages, so a mid-document edit has both earlier pages
37/// to leave untouched and later pages to cascade. `edit_at` names a paragraph to rewrite, standing in for
38/// the one edit a keystroke makes; `None` is the untouched source.
39fn build_doc(n: usize, edit_at: Option<usize>) -> Vec<Block> {
40 let mut blocks = Vec::with_capacity(n);
41 for k in 0..n {
42 blocks.push(Block::paragraph(paragraph_text(k, edit_at == Some(k))));
43 }
44 blocks
45}
46
47/// One paragraph's text, keyed by its index so the corpus is stable run to run and, crucially, unique per
48/// paragraph: each opens with its own ordinal word. Uniqueness matters to the hit-count assertions -- the
49/// authoring memo is content-addressed, so two paragraphs of identical text would legitimately share one
50/// cache entry (a real and desirable property: a moved or duplicated paragraph reuses its authored nodes),
51/// which would blur "one edit, one miss". An edited paragraph swaps in a different but similarly shaped
52/// body, so it genuinely re-breaks and re-paginates from that point -- the honest case for the cascade.
53fn paragraph_text(k: usize, edited: bool) -> String {
54 let words = [
55 "typesetting", "streams", "a", "document", "through", "two", "passes", "of", "the", "driver",
56 "while", "the", "ledger", "resolves", "each", "anchor", "into", "the", "page", "it", "landed",
57 "on", "and", "the", "breaker", "sets", "every", "justified", "line", "to", "the", "measure",
58 "without", "a", "single", "floating", "point", "along", "the", "way", "so", "the", "build",
59 "stays", "byte", "identical", "from", "one", "run", "to", "the", "very", "next", "one", "here",
60 ];
61 let seed = if edited { k * 7 + 3 } else { k * 5 + 1 };
62 let count = 34 + (seed % 20);
63 // The unique ordinal opener, distinct for every paragraph and for an edited one against its original.
64 let mut s = fmt!("Paragraph{}{}.", k, if edited { "edited" } else { "" });
65 for i in 0..count {
66 s.push(' ');
67 s.push_str(words[(seed + i * 3) % words.len()]);
68 }
69 s.push('.');
70 s
71}
72
73/// Authors, runs and emits a document to a vector of per-page SVG strings, threading the memo (when given)
74/// through both the authoring stage and the page emit, exactly as the native `--watch` path would. `None`
75/// is the plain, un-memoised compile -- the honest cold baseline, and the byte reference every memo path
76/// is held against. `bib` threads a marked bibliography through exactly as a real compile does (see
77/// `book.rs:662-665`), so a `#cite` segment resolves and the cited set folds into the memo's global
78/// fingerprint; plain callers with no citations pass `None`.
79fn compile_pages(blocks: Vec<Block>, bib: Option<Bibliography>, memo: Option<&mut Memo>) -> Outcome<Vec<String>> {
80 let fonts = Arc::new(res!(fonts::libertinus()));
81 let geom = PageGeometry::a4();
82 let assembled = Assembled {
83 blocks,
84 fonts,
85 geom,
86 style: Theme::default(),
87 title: String::new(),
88 faces: FaceResolver::default(),
89 front: None,
90 bib,
91 };
92 let mut memo = memo;
93 let rendered = res!(author_and_run_memo(assembled, memo.as_deref_mut()));
94 let mut svgs = Vec::with_capacity(rendered.out.pages.len());
95 for page in &rendered.out.pages {
96 let svg = match memo.as_deref_mut() {
97 Some(m) => res!(svg::render_page_memo(page, m)),
98 None => res!(svg::render_page(page)),
99 };
100 svgs.push(svg);
101 }
102 // The caller closes the generation once the pages are emitted, so the page cache survives to be reused.
103 if let Some(m) = memo.as_deref_mut() {
104 m.sweep();
105 }
106 Ok(svgs)
107}
108
109/// The heart of the gate: a warm recompile after a one-paragraph edit is byte-identical to a cold compile
110/// of the edited source, every unedited block hits the authoring cache, and every page the edit does not
111/// reach hits the emit cache.
112#[test]
113fn warm_recompile_is_byte_identical_after_a_one_paragraph_edit() -> Outcome<()> {
114 let n = 40;
115 let edit_at = 34; // late, so many earlier pages fall outside the pagination cascade
116
117 // The cold references: the original source and the edited source, each compiled with no memo at all.
118 let cold_orig = res!(compile_pages(build_doc(n, None), None, None));
119 let cold_edited = res!(compile_pages(build_doc(n, Some(edit_at)), None, None));
120
121 // Prime a memo on the original, then recompile the edited source warm against it.
122 let mut memo = Memo::new();
123 let warm_orig = res!(compile_pages(build_doc(n, None), None, Some(&mut memo)));
124 assert_eq!(warm_orig, cold_orig,
125 "a first, cold compile through the memo must be byte-identical to a compile with no memo");
126 // After the priming compile every block was a miss (the cache was empty) and every page was rendered.
127 assert_eq!(memo.block_hits, 0, "the priming compile fills the cache, so nothing hits");
128 assert_eq!(memo.block_misses as usize, n, "every block is authored on the priming compile");
129
130 let warm_edited = res!(compile_pages(build_doc(n, Some(edit_at)), None, Some(&mut memo)));
131
132 // (1) Byte identity -- the one property the memo must never break.
133 assert_eq!(warm_edited, cold_edited,
134 "a warm recompile after an edit must be byte-identical to a cold compile of the edited source");
135
136 // (2) The authoring cache: only the edited block misses; every other block splices its cached nodes.
137 assert_eq!(memo.block_misses, 1,
138 "only the one edited block should miss the authoring cache, found {} misses", memo.block_misses);
139 assert_eq!(memo.block_hits as usize, n - 1,
140 "every block but the edited one should hit the authoring cache, found {} hits", memo.block_hits);
141
142 // (3) The emit cache: every page the edit does not reach must hit. A page is provably outside the
143 // cascade exactly when its cold SVG is unchanged by the edit; that is a rigorous lower bound on the
144 // page hits the warm compile must score, since such a page's body frame is byte-identical to the one
145 // the priming compile cached.
146 let untouched = cold_orig.iter().zip(cold_edited.iter()).filter(|(a, b)| a == b).count();
147 assert!(untouched > 0, "the edit must be late enough to leave earlier pages untouched");
148 assert!(memo.page_hits as usize >= untouched,
149 "every page outside the cascade must hit the emit cache: {} hits < {} untouched pages",
150 memo.page_hits, untouched);
151 assert_eq!((memo.page_hits + memo.page_misses) as usize, warm_edited.len(),
152 "every page is either a hit or a miss");
153 assert!(memo.page_misses >= 1, "the edited region must re-render at least one page");
154
155 eprintln!(
156 "[memo] {} blocks over {} pages: warm edit -> block {}/{} hit, page {}/{} hit ({} untouched)",
157 n, warm_edited.len(), memo.block_hits, n - 1,
158 memo.page_hits, warm_edited.len(), untouched);
159 Ok(())
160}
161
162/// A warm recompile over a document of headings and paragraphs -- where a chapter heading keeps the first
163/// line of the paragraph it introduces, consuming two source blocks as one authored unit -- must still be
164/// byte-identical to a cold compile of the edited source. This exercises the splice of a cached heading
165/// unit on a hit, the path plain prose never reaches.
166#[test]
167fn warm_recompile_is_byte_identical_with_headings() -> Outcome<()> {
168 let doc = |edit: bool| -> Vec<Block> {
169 let mut v = Vec::new();
170 for c in 0..4 {
171 v.push(Block::heading(1, fmt!("Chapter{}", c)));
172 for p in 0..6 {
173 let k = c * 6 + p;
174 // Edit one paragraph in the third chapter, well clear of any heading's kept first line.
175 v.push(Block::paragraph(paragraph_text(k, edit && k == 15)));
176 }
177 }
178 v
179 };
180 let cold_edited = res!(compile_pages(doc(true), None, None));
181 let mut memo = Memo::new();
182 let _ = res!(compile_pages(doc(false), None, Some(&mut memo))); // prime on the original
183 let warm = res!(compile_pages(doc(true), None, Some(&mut memo)));
184 assert_eq!(warm, cold_edited,
185 "a warm recompile with chapter headings must be byte-identical to a cold compile of the edit");
186 assert!(memo.block_misses >= 1, "the edited paragraph must miss");
187 assert!(memo.block_hits >= 1, "the unedited blocks, headings included, must hit");
188 Ok(())
189}
190
191/// A cold compile with the memo present-but-empty must match a compile with no memo, page for page, over
192/// a document exercising more than plain prose -- headings, a list and a code block -- so the byte-identity
193/// guarantee is not resting on paragraphs alone.
194#[test]
195fn a_cold_memo_compile_matches_a_no_memo_compile_on_mixed_blocks() -> Outcome<()> {
196 let blocks = || vec![
197 Block::heading(1, "The Streaming Driver"),
198 Block::paragraph(paragraph_text(0, false)),
199 Block::paragraph(paragraph_text(1, false)),
200 Block::heading(2, "Line Breaking"),
201 Block::paragraph(paragraph_text(2, false)),
202 Block::list(false, vec![], false),
203 Block::code(vec!["let x = 1;".to_string(), "let y = x + 1;".to_string()]),
204 Block::paragraph(paragraph_text(3, false)),
205 ];
206 let no_memo = res!(compile_pages(blocks(), None, None));
207 let mut memo = Memo::new();
208 let with_memo = res!(compile_pages(blocks(), None, Some(&mut memo)));
209 assert_eq!(no_memo, with_memo,
210 "a present-but-cold memo must not change one output byte over mixed block kinds");
211 Ok(())
212}
213
214/// The measurement the swap rests on: cold (no memo) versus warm (one-paragraph edit through the memo)
215/// wall time on a document of a few hundred pages. Not an assertion -- machines differ -- but it prints
216/// the speedup the live-view loop would see. Run with `--nocapture` to read it.
217#[test]
218fn measure_cold_versus_warm_recompile() -> Outcome<()> {
219 let n = 260;
220 let edit_at = 130;
221
222 // Cold: a full compile with no memo, the latency a straight swap would pay on every edit.
223 let t0 = Instant::now();
224 let cold = res!(compile_pages(build_doc(n, None), None, None));
225 let cold_ms = t0.elapsed().as_secs_f64() * 1000.0;
226
227 // Prime the memo on the original document (the first open of the document, not the edit loop).
228 let mut memo = Memo::new();
229 let _ = res!(compile_pages(build_doc(n, None), None, Some(&mut memo)));
230
231 // Warm: the edit-and-re-render a keystroke triggers.
232 let t1 = Instant::now();
233 let warm = res!(compile_pages(build_doc(n, Some(edit_at)), None, Some(&mut memo)));
234 let warm_ms = t1.elapsed().as_secs_f64() * 1000.0;
235
236 let cold_ref = res!(compile_pages(build_doc(n, Some(edit_at)), None, None));
237 assert_eq!(warm, cold_ref, "the measured warm recompile must still be byte-identical");
238
239 eprintln!(
240 "[memo] {} pages: cold {:.1} ms, warm {:.1} ms, speedup {:.1}x (block {}/{} hit, page {}/{} hit)",
241 cold.len(), cold_ms, warm_ms, cold_ms / warm_ms.max(0.001),
242 memo.block_hits, n - 1, memo.page_hits, warm.len());
243 Ok(())
244}
245
246// ┌───────────────────────────────────────────────────────────────────────────┐
247// │ THE LEDGER GATE: pagination-shifting edits against folios, cites, glossary │
248// └───────────────────────────────────────────────────────────────────────────┘
249//
250// The tests above hold the memo to byte-identity over plain prose and headings, where an edit only ever
251// lengthens or shortens text. The gate below tests the sharper claim: that byte-identity survives an edit
252// that shifts which PAGE later content lands on (so folios printed in the furniture move) and an edit that
253// moves WHICH block first uses a glossary term (so the term's bold-italic first-use markup moves). Both are
254// ledger facts -- resolved after the driver's pages converge -- that the memo must never freeze stale.
255
256/// Three entries, two of them Zuboff works dated 2019, so the reference list and the in-text label both
257/// exercise the year-suffix disambiguation (`2019a`/`2019b`) the way a real bibliography does.
258const LEDGER_BIB: &str = r#"
259@book{scott1976,
260 author = {Scott, James C.},
261 title = {The Moral Economy of the Peasant},
262 year = {1976},
263 publisher = {Yale University Press}
264}
265@book{zuboff2019a,
266 author = {Zuboff, Shoshana},
267 title = {The Age of Surveillance Capitalism},
268 year = {2019},
269 publisher = {PublicAffairs}
270}
271@book{zuboff2019b,
272 author = {Zuboff, Shoshana},
273 title = {Big Other},
274 year = {2019},
275 publisher = {Profile Books}
276}
277"#;
278
279/// Parses [`LEDGER_BIB`] and marks `cited` as cited, mirroring `book.rs:662-665` -- the bibliography must
280/// enter the assembled doc with its cited set already settled, not built up as the body walk goes.
281fn ledger_bib(cited: &[&str]) -> Outcome<Bibliography> {
282 let mut bib = res!(Bibliography::parse(LEDGER_BIB));
283 for k in cited {
284 bib.mark_cited(k);
285 }
286 Ok(bib)
287}
288
289/// The three ledger-fixture variants. `Orig` and `Shift` differ only in the edit site's length; `Orig` and
290/// `DropFirstUse` differ only in whether the first chapter's rich paragraph carries the glossary segment.
291#[derive(Clone, Copy, PartialEq, Eq, Debug)]
292enum Variant { Orig, Shift, DropFirstUse }
293
294/// The sentence the edit site repeats under `Shift`. Forty repeats is long enough to add a whole extra
295/// page and push every later block -- the rest of chapter one, the two rich paragraphs, chapter two and
296/// the back matter -- onto later pages than they land on under `Orig`.
297fn edit_sentence() -> &'static str {
298 "the ledger resolves this very anchor into the folio it lands on once the driver's second pass converges"
299}
300
301/// The one middle paragraph the ledger fixture edits. `shift` repeats its sentence forty times instead of
302/// once; nothing else about the fixture changes, so this is the sole block whose content differs.
303fn edit_site(shift: bool) -> Block {
304 let reps = if shift { 40 } else { 1 };
305 let mut s = String::from("EditSite.");
306 for _ in 0..reps {
307 s.push(' ');
308 s.push_str(edit_sentence());
309 s.push('.');
310 }
311 Block::paragraph(s)
312}
313
314/// Chapter one's rich paragraph: a glossary first use (omitted under `DropFirstUse`), a citation, an index
315/// marker, a footnote and a margin note -- the one paragraph exercising every ledger-facing segment kind.
316/// Everything but the glossary segment is fixed across variants, so a miss traces to exactly that change.
317fn rich_paragraph_one(drop_first_use: bool) -> Block {
318 let mut segments = vec![Segment::text("The driver's ledger resolves ")];
319 if !drop_first_use {
320 segments.push(Segment::glossary("memoisation", "memoisation"));
321 segments.push(Segment::text(" as "));
322 }
323 segments.push(Segment::text("the technique that caches "));
324 segments.push(Segment::cite(vec!["scott1976".to_string()]));
325 segments.push(Segment::text("'s peasant economy against a repeated anchor, recording it "));
326 segments.push(Segment::index("Ledger anchor", None, false, vec![Segment::text("Ledger anchor")]));
327 segments.push(Segment::text(" once per pass"));
328 segments.push(Segment::footnote(vec![Segment::text("A pass converges when no anchor moves twice.")]));
329 segments.push(Segment::text(", and marks it "));
330 segments.push(Segment::margin_note("R1", vec!["R1".to_string()]));
331 segments.push(Segment::text("in the outside margin."));
332 Block::rich(segments)
333}
334
335/// The paragraph carrying the term's second textual mention (the first paragraph's own topic sentence
336/// leads into it): always carries the glossary term (a later use under `Orig`/`Shift`, the FIRST use under
337/// `DropFirstUse`), a two-key citation exercising the Zuboff year-suffix pair, an index marker, a footnote
338/// and a margin note. Fixed across all three variants; kept directly adjacent to `rich_paragraph_one` (see
339/// `ledger_doc`) so `DropFirstUse`'s effect on the memo is isolated to exactly these two blocks.
340fn rich_paragraph_two() -> Block {
341 Block::rich(vec![
342 Segment::text("The account returns to "),
343 Segment::glossary("memoisation", "memoisation"),
344 Segment::text(" once more, this time citing "),
345 Segment::cite(vec!["zuboff2019a".to_string(), "zuboff2019b".to_string()]),
346 Segment::text(" on the surveillance ledger, indexing "),
347 Segment::index("Surveillance ledger", None, false, vec![Segment::text("Surveillance ledger")]),
348 Segment::text(" and noting"),
349 Segment::footnote(vec![Segment::text("Both works share the one 2019 imprint year.")]),
350 Segment::text(" the citation in "),
351 Segment::margin_note("R2", vec!["R2".to_string()]),
352 Segment::text("the margin."),
353 ])
354}
355
356/// The ledger fixture: a chapter of leading filler enough to paginate over several pages before the edit
357/// site, the edit site itself, more filler, the two rich paragraphs, a second chapter heading and its own
358/// filler, then a reverse claim index and back-matter index to close it. `paragraph_text`'s indices are
359/// unique per paragraph and identical across every variant, so only the edit site and (under
360/// `DropFirstUse`) the two rich paragraphs can ever differ.
361///
362/// The two rich paragraphs sit directly adjacent, with no heading or filler between them. That is
363/// deliberate, not merely tidy: the block-authoring memo folds the glossary first-use `seen` set into
364/// *every* block's key (`Authoring::seen_hash`, `doc.rs:1266`), so once the term's first use moves from
365/// the first rich paragraph to the second, any block sitting between them would key on a `seen` state that
366/// differs from the one it was primed under -- a correct, safe, but non-minimal miss, since that block's
367/// own rendered output never reads the term. Keeping the pair adjacent isolates `DropFirstUse`'s effect to
368/// exactly the two blocks that do read it, which is what `warm_recompile_reauthors_a_later_glossary_use_*`
369/// holds the memo to.
370fn ledger_doc(variant: Variant) -> Vec<Block> {
371 let drop_first_use = variant == Variant::DropFirstUse;
372 let shift = variant == Variant::Shift;
373 let mut blocks = Vec::new();
374
375 blocks.push(Block::heading(1, "Chapter One"));
376 for k in 0..24 {
377 blocks.push(Block::paragraph(paragraph_text(k, false)));
378 }
379 blocks.push(edit_site(shift)); // the one edited paragraph, well after several leading pages
380 for k in 24..30 {
381 blocks.push(Block::paragraph(paragraph_text(k, false)));
382 }
383 blocks.push(rich_paragraph_one(drop_first_use));
384 blocks.push(rich_paragraph_two());
385
386 blocks.push(Block::heading(1, "Chapter Two"));
387 for k in 30..40 {
388 blocks.push(Block::paragraph(paragraph_text(k, false)));
389 }
390
391 blocks.push(Block::ClaimIndex);
392 blocks.push(Block::Index);
393 blocks
394}
395
396/// THE gate: a warm recompile after an edit that shifts pagination -- moving every later folio, the second
397/// chapter's rich paragraph and the back matter onto later pages -- must still be byte-identical to a cold
398/// compile of the shifted source, while only the one edited block misses the authoring cache.
399#[test]
400fn warm_recompile_is_byte_identical_when_pagination_shifts_ledger_facts() -> Outcome<()> {
401 let full_cites = ["scott1976", "zuboff2019a", "zuboff2019b"];
402
403 let cold_orig = res!(compile_pages(ledger_doc(Variant::Orig), Some(res!(ledger_bib(&full_cites))), None));
404 let cold_shift = res!(compile_pages(ledger_doc(Variant::Shift), Some(res!(ledger_bib(&full_cites))), None));
405
406 // Precondition: the edit really does shift pagination -- the tail page differs and the document grows.
407 // A test that does not move the folios proves nothing about the ledger, only about plain byte diffing.
408 assert_ne!(cold_orig.last(), cold_shift.last(),
409 "the shifted edit must move the final page's content -- the precondition the whole gate rests on");
410 assert!(cold_shift.len() > cold_orig.len(),
411 "the shifted edit must add at least one page, found {} vs {} pages",
412 cold_shift.len(), cold_orig.len());
413
414 // Prime on the original, then warm-compile the shift.
415 let mut memo = Memo::new();
416 let _ = res!(compile_pages(ledger_doc(Variant::Orig), Some(res!(ledger_bib(&full_cites))), Some(&mut memo)));
417 let warm_shift = res!(compile_pages(ledger_doc(Variant::Shift), Some(res!(ledger_bib(&full_cites))), Some(&mut memo)));
418
419 // (1) Byte identity -- THE gate.
420 assert_eq!(warm_shift, cold_shift,
421 "a warm recompile after a pagination-shifting edit must be byte-identical to the cold shifted compile");
422
423 // (2) Only the edited paragraph misses; the rich paragraphs, headings and back-matter blocks all hit,
424 // even though every one of them now lands on a different page than it did under `Orig`.
425 assert_eq!(memo.block_misses, 1,
426 "only the one edited block should miss the authoring cache, found {} misses", memo.block_misses);
427
428 // (3) The emit cache: the cascade must actually reach several pages, and at least the leading page(s)
429 // before the edit site must still hit.
430 assert!(memo.page_misses >= 3,
431 "the pagination cascade must re-render at least three pages, found {} misses", memo.page_misses);
432 assert!(memo.page_hits >= 1,
433 "at least one page before the edit site must hit the emit cache, found {} hits", memo.page_hits);
434
435 eprintln!(
436 "[ledger] {} -> {} pages: block {} miss, page {}/{} hit",
437 cold_orig.len(), cold_shift.len(), memo.block_misses, memo.page_hits, memo.page_hits + memo.page_misses);
438 Ok(())
439}
440
441/// Removing the first chapter's glossary segment pushes the term's first use into the second chapter's
442/// rich paragraph, which must now render its bold-italic markup instead of the plain text it rendered
443/// under `Orig` -- a warm recompile must reauthor that later paragraph, not serve its stale plain form.
444#[test]
445fn warm_recompile_reauthors_a_later_glossary_use_when_the_first_use_is_removed() -> Outcome<()> {
446 let full_cites = ["scott1976", "zuboff2019a", "zuboff2019b"];
447
448 let cold_drop = res!(compile_pages(ledger_doc(Variant::DropFirstUse), Some(res!(ledger_bib(&full_cites))), None));
449
450 let mut memo = Memo::new();
451 let _ = res!(compile_pages(ledger_doc(Variant::Orig), Some(res!(ledger_bib(&full_cites))), Some(&mut memo)));
452 let warm_drop = res!(compile_pages(ledger_doc(Variant::DropFirstUse), Some(res!(ledger_bib(&full_cites))), Some(&mut memo)));
453
454 assert_eq!(warm_drop, cold_drop,
455 "a warm recompile that drops the first glossary use must be byte-identical to the cold compile");
456 // Rich paragraph one misses (its own content changed, the segment is gone) and rich paragraph two
457 // misses (its content is unchanged, but it now enters with a different glossary `seen` state, so it
458 // must re-author its bold-italic first use rather than splice back its cached plain-text nodes).
459 assert_eq!(memo.block_misses, 2,
460 "exactly the two rich paragraphs should miss, found {} misses", memo.block_misses);
461 Ok(())
462}
463
464/// Citing a new work the memo was not primed with must clear the whole cache: the cited set folds into the
465/// memo's global fingerprint (`memo_fingerprint`, via the bibliography's `Debug`), so a document that has
466/// not changed one block still misses everywhere once what it cites has changed.
467#[test]
468fn a_new_citation_clears_the_memo() -> Outcome<()> {
469 let blocks = || ledger_doc(Variant::Orig);
470
471 let cold_two_cites = res!(compile_pages(blocks(), Some(res!(ledger_bib(&["scott1976", "zuboff2019a"]))), None));
472
473 let mut memo = Memo::new();
474 let _ = res!(compile_pages(blocks(), Some(res!(ledger_bib(&["scott1976"]))), Some(&mut memo)));
475 let warm = res!(compile_pages(blocks(), Some(res!(ledger_bib(&["scott1976", "zuboff2019a"]))), Some(&mut memo)));
476
477 assert_eq!(warm, cold_two_cites,
478 "a warm recompile after a citation-set change must be byte-identical to a cold compile with that set");
479 assert_eq!(memo.block_hits, 0,
480 "a changed cited set must clear the global fingerprint and miss every block, found {} hits",
481 memo.block_hits);
482 Ok(())
483}