Skip to main content

fallow_engine/
warm_parse.rs

1//! Parsed modules that a long-lived process keeps across analysis sessions.
2//!
3//! A typed MCP tool call builds a new [`AnalysisSession`] for each call. Each
4//! session loads the persisted parse cache, checks each file against it, and
5//! writes the cache back. A process that installs a [`WarmParseStore`] keeps
6//! the parsed modules of recent sessions in memory. A later session with the
7//! same file list and the same file fingerprints takes its modules from the
8//! store and does no parse work.
9//!
10//! The store is safe to share between sessions with different configs,
11//! because a parse depends only on the file path, the file content, and the
12//! config hash of the persisted parse cache. The key holds all three: the
13//! project root and the cache config hash, the ordered file list (the file ids
14//! follow from it), and one fingerprint per file. The store keeps a module
15//! only when each fingerprint can stand in for the file content without a
16//! content check, which needs a known ctime. On a platform with no ctime,
17//! such as Windows, each session parses through the persisted cache as
18//! before. A fingerprint that did not settle before the kept parse read the
19//! file can belong to a same-length write in the same timestamp tick, so a
20//! later session reads that file and compares its content hash with the kept
21//! module.
22//!
23//! The limit of the store is on the memory of the kept modules. The store
24//! cannot measure that memory, so it makes an estimate from the source size
25//! and the file count. Each kept file list holds its own modules: two lists
26//! that share files, such as a full list and a production list, each count in
27//! full.
28//!
29//! [`AnalysisSession`]: crate::session::AnalysisSession
30
31use std::path::{Path, PathBuf};
32use std::sync::atomic::{AtomicUsize, Ordering};
33use std::sync::{Arc, Mutex, PoisonError, RwLock};
34
35use fallow_types::discover::{DiscoveredFile, FileId};
36use fallow_types::extract::{ModuleInfo, SourceParseDegradation, SourceReadFailure};
37use fallow_types::source_fingerprint::SourceFingerprint;
38
39/// The default number of parsed file lists that a store keeps.
40pub const DEFAULT_MAX_ENTRIES: usize = 4;
41
42/// The default limit on the estimated memory of the kept modules.
43pub const DEFAULT_MAX_RETAINED_BYTES: u64 = 512 * 1024 * 1024;
44
45/// The estimated heap memory of the parsed modules for one byte of source.
46///
47/// The heap of the parsed modules of ten public projects was 4.4 to 10.5
48/// times the source size. The estimate uses a value above the largest ratio,
49/// so the real memory stays below the limit.
50const RETAINED_BYTES_PER_SOURCE_BYTE: u64 = 12;
51
52/// Memory limits of a [`WarmParseStore`].
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct WarmParseLimits {
55    /// The most parsed file lists that the store keeps. The store removes the
56    /// least recently used list first.
57    pub max_entries: usize,
58    /// The limit on the estimated memory, in bytes, of the kept modules of
59    /// all file lists. The estimate is 12 bytes for each source byte, plus
60    /// the size of one module struct for each file. A file list with an
61    /// estimate over this limit is not kept.
62    pub max_retained_bytes: u64,
63}
64
65impl Default for WarmParseLimits {
66    fn default() -> Self {
67        Self {
68            max_entries: DEFAULT_MAX_ENTRIES,
69            max_retained_bytes: DEFAULT_MAX_RETAINED_BYTES,
70        }
71    }
72}
73
74/// The parse work of the sessions that used a store.
75#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
76pub struct WarmParseCounts {
77    /// Parse passes over a full file list.
78    pub parse_runs: usize,
79    /// Files parsed from source.
80    pub modules_parsed: usize,
81    /// Files served from the persisted parse cache.
82    pub disk_cache_hits: usize,
83    /// Files served from the modules in the store.
84    pub modules_reused: usize,
85}
86
87/// Parsed modules kept across analysis sessions in one process.
88#[derive(Debug)]
89pub struct WarmParseStore {
90    limits: WarmParseLimits,
91    entries: Mutex<Vec<WarmEntry>>,
92    parse_runs: AtomicUsize,
93    modules_parsed: AtomicUsize,
94    disk_cache_hits: AtomicUsize,
95    modules_reused: AtomicUsize,
96}
97
98#[derive(Debug)]
99struct WarmEntry {
100    root: PathBuf,
101    cache_config_hash: u64,
102    paths: Vec<PathBuf>,
103    file_ids: Vec<FileId>,
104    fingerprints: Vec<SourceFingerprint>,
105    /// The wall-clock time before the parse read `fingerprints`.
106    read_started_ns: u64,
107    has_complexity: bool,
108    retained_bytes: u64,
109    parse: WarmParse,
110}
111
112/// The identity of one parse: the project, the file list, and the file
113/// fingerprints.
114#[derive(Debug, Clone, Copy)]
115pub(crate) struct WarmParseKey<'a> {
116    pub(crate) root: &'a Path,
117    pub(crate) cache_config_hash: u64,
118    pub(crate) files: &'a [DiscoveredFile],
119    pub(crate) fingerprints: &'a [SourceFingerprint],
120    /// The wall-clock time before the session read `fingerprints`.
121    pub(crate) read_started_ns: u64,
122}
123
124impl WarmParseKey<'_> {
125    /// Whether the store may keep the modules of this parse. Each fingerprint
126    /// must be able to stand in for the file content, which needs a known
127    /// ctime.
128    pub(crate) fn is_reusable(&self) -> bool {
129        self.files.len() == self.fingerprints.len()
130            && self
131                .fingerprints
132                .iter()
133                .all(|fingerprint| fingerprint.is_trustworthy_without_content())
134    }
135
136    /// The estimated heap memory of the parsed modules of this file list.
137    fn retained_bytes(&self) -> u64 {
138        estimated_retained_bytes(self.fingerprints)
139    }
140}
141
142/// The estimated heap memory of the parsed modules of the files with these
143/// fingerprints: 12 bytes for each source byte, plus the size of one module
144/// struct for each file. [`WarmParseLimits::max_retained_bytes`] applies to
145/// this estimate.
146#[must_use]
147pub fn estimated_retained_bytes(fingerprints: &[SourceFingerprint]) -> u64 {
148    let source_bytes: u64 = fingerprints
149        .iter()
150        .map(|fingerprint| fingerprint.file_size)
151        .sum();
152    let module_bytes = u64::try_from(size_of::<ModuleInfo>()).unwrap_or(u64::MAX);
153    let file_count = u64::try_from(fingerprints.len()).unwrap_or(u64::MAX);
154    source_bytes
155        .saturating_mul(RETAINED_BYTES_PER_SOURCE_BYTE)
156        .saturating_add(file_count.saturating_mul(module_bytes))
157}
158
159/// Whether each file of `key` whose fingerprint did not settle before
160/// `kept_read_started_ns` still has the content hash of its kept module.
161///
162/// A same-length write in the same timestamp tick keeps the fingerprint, so
163/// only the content can tell for such a file.
164fn unsettled_files_match(
165    key: &WarmParseKey<'_>,
166    kept_read_started_ns: u64,
167    modules: &[ModuleInfo],
168) -> bool {
169    key.fingerprints
170        .iter()
171        .zip(key.files)
172        .all(|(fingerprint, file)| {
173            fingerprint.is_settled_before(kept_read_started_ns)
174                || crate::session_reuse::module_matches_source(modules, file.id, &file.path)
175        })
176}
177
178/// The output of one parse that a later session can use again.
179#[derive(Debug, Clone)]
180pub(crate) struct WarmParse {
181    pub(crate) modules: Arc<[ModuleInfo]>,
182    pub(crate) read_failures: Arc<[SourceReadFailure]>,
183    pub(crate) parse_degradations: Arc<[SourceParseDegradation]>,
184}
185
186impl WarmEntry {
187    /// Whether the entry is a parse of the same files with the same
188    /// fingerprints. The caller still checks the content of each file that
189    /// did not settle, see [`unsettled_files_match`].
190    fn matches(&self, key: &WarmParseKey<'_>) -> bool {
191        self.matches_files(key) && self.fingerprints == key.fingerprints
192    }
193
194    /// Whether the entry is a parse of the same file list. The kept modules
195    /// carry their file ids, so the ids must also be the same.
196    fn matches_files(&self, key: &WarmParseKey<'_>) -> bool {
197        self.cache_config_hash == key.cache_config_hash
198            && self.root == key.root
199            && self
200                .paths
201                .iter()
202                .eq(key.files.iter().map(|file| &file.path))
203            && self
204                .file_ids
205                .iter()
206                .copied()
207                .eq(key.files.iter().map(|file| file.id))
208    }
209}
210
211impl WarmParseStore {
212    /// Create an empty store with the given memory limits.
213    #[must_use]
214    pub fn new(limits: WarmParseLimits) -> Self {
215        Self {
216            limits,
217            entries: Mutex::new(Vec::new()),
218            parse_runs: AtomicUsize::new(0),
219            modules_parsed: AtomicUsize::new(0),
220            disk_cache_hits: AtomicUsize::new(0),
221            modules_reused: AtomicUsize::new(0),
222        }
223    }
224
225    /// The parse work of the sessions that used this store.
226    #[must_use]
227    pub fn counts(&self) -> WarmParseCounts {
228        WarmParseCounts {
229            parse_runs: self.parse_runs.load(Ordering::Relaxed),
230            modules_parsed: self.modules_parsed.load(Ordering::Relaxed),
231            disk_cache_hits: self.disk_cache_hits.load(Ordering::Relaxed),
232            modules_reused: self.modules_reused.load(Ordering::Relaxed),
233        }
234    }
235
236    /// The number of parsed file lists in the store.
237    #[must_use]
238    pub fn len(&self) -> usize {
239        self.lock().len()
240    }
241
242    /// Whether the store keeps no parsed file list.
243    #[must_use]
244    pub fn is_empty(&self) -> bool {
245        self.lock().is_empty()
246    }
247
248    /// The kept parse for `key`, when it has complexity or the caller needs
249    /// none. A hit becomes the most recently used entry.
250    pub(crate) fn get(&self, key: &WarmParseKey<'_>, need_complexity: bool) -> Option<WarmParse> {
251        if !key.is_reusable() {
252            return None;
253        }
254        let entries = self.lock();
255        let entry = entries
256            .iter()
257            .find(|entry| entry.matches(key) && (entry.has_complexity || !need_complexity))?;
258        let kept_read_started_ns = entry.read_started_ns;
259        let parse = entry.parse.clone();
260        drop(entries);
261        // Read the files outside the store lock, so another session does not
262        // wait on this disk work.
263        if !unsettled_files_match(key, kept_read_started_ns, &parse.modules) {
264            return None;
265        }
266        let mut entries = self.lock();
267        if let Some(position) = entries
268            .iter()
269            .position(|entry| Arc::ptr_eq(&entry.parse.modules, &parse.modules))
270        {
271            let mut entry = entries.remove(position);
272            // The content of each unsettled file was read after the read
273            // start of `key`, so the fingerprints prove the kept modules from
274            // that time on. A later hit then needs no read for these files.
275            entry.read_started_ns = entry.read_started_ns.max(key.read_started_ns);
276            entries.push(entry);
277        }
278        drop(entries);
279        self.modules_reused
280            .fetch_add(parse.modules.len(), Ordering::Relaxed);
281        Some(parse)
282    }
283
284    /// Keep the parse for `key`. It replaces an older parse of the same file
285    /// list, and the least recently used entries leave the store until it is
286    /// within its limits.
287    pub(crate) fn put(&self, key: &WarmParseKey<'_>, has_complexity: bool, parse: WarmParse) {
288        let retained_bytes = key.retained_bytes();
289        let keep = key.is_reusable()
290            && self.limits.max_entries > 0
291            && retained_bytes <= self.limits.max_retained_bytes;
292        let entry = keep.then(|| WarmEntry {
293            root: key.root.to_path_buf(),
294            cache_config_hash: key.cache_config_hash,
295            paths: key.files.iter().map(|file| file.path.clone()).collect(),
296            file_ids: key.files.iter().map(|file| file.id).collect(),
297            fingerprints: key.fingerprints.to_vec(),
298            read_started_ns: key.read_started_ns,
299            has_complexity,
300            retained_bytes,
301            parse,
302        });
303
304        let mut entries = self.lock();
305        entries.retain(|entry| !entry.matches_files(key));
306        entries.extend(entry);
307        let mut total_bytes: u64 = entries.iter().map(|entry| entry.retained_bytes).sum();
308        while entries.len() > self.limits.max_entries
309            || total_bytes > self.limits.max_retained_bytes
310        {
311            let removed = entries.remove(0);
312            total_bytes -= removed.retained_bytes;
313        }
314        drop(entries);
315    }
316
317    /// Count one parse pass over a full file list.
318    pub(crate) fn record_parse(&self, cache_misses: usize, cache_hits: usize) {
319        self.parse_runs.fetch_add(1, Ordering::Relaxed);
320        self.modules_parsed
321            .fetch_add(cache_misses, Ordering::Relaxed);
322        self.disk_cache_hits
323            .fetch_add(cache_hits, Ordering::Relaxed);
324    }
325
326    fn lock(&self) -> std::sync::MutexGuard<'_, Vec<WarmEntry>> {
327        self.entries.lock().unwrap_or_else(PoisonError::into_inner)
328    }
329}
330
331static INSTALLED: RwLock<Option<Arc<WarmParseStore>>> = RwLock::new(None);
332
333/// Make `store` the store of each session that this process creates from now
334/// on. `None` removes the store, so new sessions parse as before.
335///
336/// Only a long-lived process that runs many analyses of the same project
337/// installs a store, such as the MCP server. A session keeps the store that
338/// was installed when the session was created.
339pub fn install(store: Option<Arc<WarmParseStore>>) {
340    *INSTALLED.write().unwrap_or_else(PoisonError::into_inner) = store;
341}
342
343/// The store that new sessions use, if a process installed one.
344#[must_use]
345pub fn installed() -> Option<Arc<WarmParseStore>> {
346    INSTALLED
347        .read()
348        .unwrap_or_else(PoisonError::into_inner)
349        .clone()
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355    use fallow_types::source_fingerprint::TIMESTAMP_SETTLE_WINDOW_NS;
356
357    fn files(paths: &[&str]) -> Vec<DiscoveredFile> {
358        paths
359            .iter()
360            .enumerate()
361            .map(|(index, path)| DiscoveredFile {
362                id: FileId(u32::try_from(index).expect("small index")),
363                path: PathBuf::from(path),
364                size_bytes: 1,
365            })
366            .collect()
367    }
368
369    fn fingerprints(count: usize, size: u64) -> Vec<SourceFingerprint> {
370        (0..count)
371            .map(|index| SourceFingerprint::with_ctime(10 + index as u64, 20, size))
372            .collect()
373    }
374
375    fn parse() -> WarmParse {
376        WarmParse {
377            modules: Arc::from(Vec::new()),
378            read_failures: Arc::from(Vec::new()),
379            parse_degradations: Arc::from(Vec::new()),
380        }
381    }
382
383    fn key<'a>(
384        root: &'a Path,
385        files: &'a [DiscoveredFile],
386        fingerprints: &'a [SourceFingerprint],
387    ) -> WarmParseKey<'a> {
388        WarmParseKey {
389            root,
390            cache_config_hash: 7,
391            files,
392            fingerprints,
393            read_started_ns: u64::MAX,
394        }
395    }
396
397    #[test]
398    fn a_hit_needs_the_same_files_and_fingerprints() {
399        let store = WarmParseStore::new(WarmParseLimits::default());
400        let root = Path::new("/project");
401        let listed = files(&["/project/a.ts", "/project/b.ts"]);
402        let marks = fingerprints(2, 5);
403        store.put(&key(root, &listed, &marks), true, parse());
404
405        assert!(store.get(&key(root, &listed, &marks), true).is_some());
406
407        let mut edited = marks.clone();
408        edited[1].mtime_ns += 1;
409        assert!(store.get(&key(root, &listed, &edited), false).is_none());
410
411        let added = files(&["/project/a.ts", "/project/b.ts", "/project/c.ts"]);
412        assert!(
413            store
414                .get(&key(root, &added, &fingerprints(3, 5)), false)
415                .is_none()
416        );
417
418        let mut other_config = key(root, &listed, &marks);
419        other_config.cache_config_hash = 8;
420        assert!(store.get(&other_config, false).is_none());
421    }
422
423    #[test]
424    fn a_parse_without_complexity_does_not_serve_a_request_for_it() {
425        let store = WarmParseStore::new(WarmParseLimits::default());
426        let root = Path::new("/project");
427        let listed = files(&["/project/a.ts"]);
428        let marks = fingerprints(1, 5);
429        store.put(&key(root, &listed, &marks), false, parse());
430
431        assert!(store.get(&key(root, &listed, &marks), true).is_none());
432        assert!(store.get(&key(root, &listed, &marks), false).is_some());
433    }
434
435    #[test]
436    fn fingerprints_without_ctime_are_not_kept() {
437        let store = WarmParseStore::new(WarmParseLimits::default());
438        let root = Path::new("/project");
439        let listed = files(&["/project/a.ts"]);
440        let marks = [SourceFingerprint::new(10, 5)];
441        store.put(&key(root, &listed, &marks), true, parse());
442
443        assert!(store.is_empty());
444        assert!(store.get(&key(root, &listed, &marks), false).is_none());
445    }
446
447    #[test]
448    fn a_fingerprint_inside_the_settle_window_needs_the_kept_content() {
449        let project = tempfile::tempdir().expect("project");
450        let source = project.path().join("a.ts");
451        std::fs::write(&source, "export const a = 1;\n").expect("write source");
452        let store = WarmParseStore::new(WarmParseLimits::default());
453        let listed = files(&[source.to_str().expect("utf-8 path")]);
454        let marks = [SourceFingerprint::with_ctime(10, 20, 5)];
455        let recent = WarmParseKey {
456            read_started_ns: 20,
457            ..key(project.path(), &listed, &marks)
458        };
459        store.put(&recent, true, parse());
460
461        assert_eq!(store.len(), 1);
462        assert!(
463            store.get(&recent, false).is_none(),
464            "a readable file without a matching kept module is not reused"
465        );
466        let settled = WarmParseKey {
467            read_started_ns: u64::MAX,
468            ..recent
469        };
470        store.put(&settled, true, parse());
471        assert!(
472            store.get(&settled, false).is_some(),
473            "a settled fingerprint needs no content check"
474        );
475    }
476
477    /// A real file and a parse of it, so the kept module has the content hash
478    /// of the file.
479    fn parsed_file(dir: &Path, source: &str) -> (Vec<DiscoveredFile>, WarmParse) {
480        let path = dir.join("a.ts");
481        std::fs::write(&path, source).expect("write source");
482        let listed = files(&[path.to_str().expect("utf-8 path")]);
483        let module = fallow_extract::parse_single_file(&listed[0]).expect("parse source");
484        let parse = WarmParse {
485            modules: Arc::from(vec![module]),
486            ..parse()
487        };
488        (listed, parse)
489    }
490
491    #[test]
492    fn an_unsettled_fingerprint_with_the_kept_content_is_reused() {
493        let project = tempfile::tempdir().expect("project");
494        let (listed, kept) = parsed_file(project.path(), "export const a = 1;\n");
495        let marks = [SourceFingerprint::with_ctime(10, 20, 5)];
496        let recent = WarmParseKey {
497            read_started_ns: 20,
498            ..key(project.path(), &listed, &marks)
499        };
500        let store = WarmParseStore::new(WarmParseLimits::default());
501        store.put(&recent, true, kept);
502
503        assert!(store.get(&recent, false).is_some());
504    }
505
506    /// A hit that checked the content of an unsettled file proves the
507    /// fingerprint from the read start of that hit on. The entry takes that
508    /// read start, so a later hit needs no content read.
509    #[test]
510    fn a_verified_hit_settles_the_entry() {
511        let project = tempfile::tempdir().expect("project");
512        let (listed, kept) = parsed_file(project.path(), "export const a = 1;\n");
513        let marks = [SourceFingerprint::with_ctime(10, 20, 5)];
514        let recent = WarmParseKey {
515            read_started_ns: 20,
516            ..key(project.path(), &listed, &marks)
517        };
518        let store = WarmParseStore::new(WarmParseLimits::default());
519        store.put(&recent, true, kept);
520
521        let later = WarmParseKey {
522            read_started_ns: 20 + TIMESTAMP_SETTLE_WINDOW_NS,
523            ..recent
524        };
525        assert!(
526            store.get(&later, false).is_some(),
527            "the kept content matches"
528        );
529        assert_eq!(
530            store.lock()[0].read_started_ns,
531            later.read_started_ns,
532            "the entry takes the read start of the verified hit"
533        );
534
535        // A different source with the same size and fingerprints. A settled
536        // entry does not read the file, so only the stored read start can
537        // explain a hit here.
538        std::fs::write(&listed[0].path, "export const b = 1;\n").expect("rewrite source");
539        assert!(
540            store.get(&later, false).is_some(),
541            "a settled entry needs no content read"
542        );
543    }
544
545    #[test]
546    fn a_new_parse_of_the_same_files_replaces_the_old_one() {
547        let store = WarmParseStore::new(WarmParseLimits::default());
548        let root = Path::new("/project");
549        let listed = files(&["/project/a.ts"]);
550        let before = fingerprints(1, 5);
551        let after = fingerprints(1, 6);
552        store.put(&key(root, &listed, &before), true, parse());
553        store.put(&key(root, &listed, &after), true, parse());
554
555        assert_eq!(store.len(), 1);
556        assert!(store.get(&key(root, &listed, &after), true).is_some());
557    }
558
559    #[test]
560    fn the_least_recently_used_entry_leaves_first() {
561        let store = WarmParseStore::new(WarmParseLimits {
562            max_entries: 2,
563            max_retained_bytes: u64::MAX,
564        });
565        let marks = fingerprints(1, 5);
566        let first = files(&["/first/a.ts"]);
567        let second = files(&["/second/a.ts"]);
568        let third = files(&["/third/a.ts"]);
569        store.put(&key(Path::new("/first"), &first, &marks), true, parse());
570        store.put(&key(Path::new("/second"), &second, &marks), true, parse());
571        assert!(
572            store
573                .get(&key(Path::new("/first"), &first, &marks), true)
574                .is_some()
575        );
576        store.put(&key(Path::new("/third"), &third, &marks), true, parse());
577
578        assert_eq!(store.len(), 2);
579        assert!(
580            store
581                .get(&key(Path::new("/second"), &second, &marks), true)
582                .is_none()
583        );
584        assert!(
585            store
586                .get(&key(Path::new("/first"), &first, &marks), true)
587                .is_some()
588        );
589    }
590
591    #[test]
592    fn the_default_limit_counts_the_memory_of_the_kept_modules() {
593        let store = WarmParseStore::new(WarmParseLimits::default());
594        let large = files(&["/large/a.ts"]);
595        store.put(
596            &key(
597                Path::new("/large"),
598                &large,
599                &fingerprints(1, 64 * 1024 * 1024),
600            ),
601            true,
602            parse(),
603        );
604        assert!(
605            store.is_empty(),
606            "the modules of 64 MiB of source take more memory than the default limit"
607        );
608
609        let medium = files(&["/medium/a.ts"]);
610        store.put(
611            &key(
612                Path::new("/medium"),
613                &medium,
614                &fingerprints(1, 16 * 1024 * 1024),
615            ),
616            true,
617            parse(),
618        );
619        assert_eq!(store.len(), 1);
620    }
621
622    #[test]
623    fn a_list_with_other_file_ids_is_not_served() {
624        let store = WarmParseStore::new(WarmParseLimits::default());
625        let root = Path::new("/project");
626        let listed = files(&["/project/a.ts", "/project/b.ts"]);
627        let marks = fingerprints(2, 5);
628        store.put(&key(root, &listed, &marks), true, parse());
629
630        let mut renumbered = listed.clone();
631        renumbered[0].id = FileId(7);
632        assert!(store.get(&key(root, &renumbered, &marks), false).is_none());
633        assert!(store.get(&key(root, &listed, &marks), false).is_some());
634    }
635
636    #[test]
637    fn the_memory_limit_bounds_the_store() {
638        let small = files(&["/small/a.ts"]);
639        let small_marks = fingerprints(1, 6);
640        let small_key = key(Path::new("/small"), &small, &small_marks);
641        let store = WarmParseStore::new(WarmParseLimits {
642            max_entries: 8,
643            max_retained_bytes: small_key.retained_bytes(),
644        });
645        let large = files(&["/large/a.ts", "/large/b.ts"]);
646        store.put(&small_key, true, parse());
647        store.put(
648            &key(Path::new("/large"), &large, &fingerprints(2, 6)),
649            true,
650            parse(),
651        );
652        assert_eq!(store.len(), 1, "a list over the limit is not kept");
653
654        let other = files(&["/other/a.ts"]);
655        store.put(
656            &key(Path::new("/other"), &other, &fingerprints(1, 6)),
657            true,
658            parse(),
659        );
660        assert_eq!(
661            store.len(),
662            1,
663            "the older list leaves to keep the sum within the limit"
664        );
665        assert!(
666            store
667                .get(&key(Path::new("/other"), &other, &fingerprints(1, 6)), true)
668                .is_some()
669        );
670    }
671}