1use std::collections::BTreeMap;
2use std::fs;
3use std::io;
4use std::path::{Component, Path, PathBuf};
5use std::time::{Duration, SystemTime};
6
7use rusqlite::{Connection, OpenFlags};
8use serde_json::json;
9
10const ROOT_KEYED_COPY_DISK_FLOOR_NUMERATOR: u64 = 3;
11const ROOT_KEYED_COPY_DISK_FLOOR_DENOMINATOR: u64 = 2;
12const SQLITE_SUFFIXES: [&str; 4] = [".sqlite-wal", ".sqlite-shm", ".sqlite-journal", ".sqlite"];
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum LegacyPartitionKind {
16 Callgraph,
17 Inspect,
18}
19
20impl LegacyPartitionKind {
21 pub fn as_str(self) -> &'static str {
22 match self {
23 Self::Callgraph => "callgraph",
24 Self::Inspect => "inspect",
25 }
26 }
27}
28
29#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct LegacyPartitionInventoryEntry {
31 pub harness: String,
32 pub kind: LegacyPartitionKind,
33 pub key: String,
34 pub path: PathBuf,
37 pub bytes: u64,
38 pub callgraph_pointer_mtime: Option<SystemTime>,
39 pub inspect_tier2_last_full_run: Option<i64>,
40}
41
42#[derive(Clone, Debug, Eq, PartialEq)]
43pub struct LegacyHarnessDuplication {
44 pub harness: String,
45 pub partitions: usize,
46 pub bytes: u64,
47}
48
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub struct DiskFloorDecision {
51 pub source_bytes: u64,
52 pub available_bytes: u64,
53 pub required_bytes: u64,
54}
55
56impl DiskFloorDecision {
57 pub fn allows_copy(self) -> bool {
58 self.available_bytes >= self.required_bytes
59 }
60
61 pub fn should_skip_copy(self) -> bool {
62 !self.allows_copy()
63 }
64
65 pub fn warning_message(self, source: &Path, target: &Path) -> String {
66 format!(
67 "Skipping root-keyed cache copy from {} into {}: free disk ({}) is below the required 1.5× floor ({} for {} source bytes).",
68 source.display(),
69 target.display(),
70 self.available_bytes,
71 self.required_bytes,
72 self.source_bytes
73 )
74 }
75
76 pub fn configure_warning(self, source: &Path, target: &Path) -> serde_json::Value {
77 json!({
78 "kind": "root_keyed_disk_floor",
79 "source_path": source.display().to_string(),
80 "target_path": target.display().to_string(),
81 "bytes_source": self.source_bytes,
82 "bytes_free": self.available_bytes,
83 "bytes_required": self.required_bytes,
84 "message": self.warning_message(source, target),
85 })
86 }
87}
88
89pub fn required_root_keyed_copy_free_bytes(source_bytes: u64) -> u64 {
90 source_bytes
91 .saturating_mul(ROOT_KEYED_COPY_DISK_FLOOR_NUMERATOR)
92 .saturating_add(ROOT_KEYED_COPY_DISK_FLOOR_DENOMINATOR - 1)
93 / ROOT_KEYED_COPY_DISK_FLOOR_DENOMINATOR
94}
95
96pub fn evaluate_root_keyed_copy_disk_floor(
97 source_bytes: u64,
98 available_bytes: u64,
99) -> DiskFloorDecision {
100 DiskFloorDecision {
101 source_bytes,
102 available_bytes,
103 required_bytes: required_root_keyed_copy_free_bytes(source_bytes),
104 }
105}
106
107pub fn available_disk_for(path: &Path) -> io::Result<u64> {
111 #[cfg(unix)]
112 {
113 use std::ffi::CString;
114 use std::os::unix::ffi::OsStrExt;
115
116 let probe = existing_ancestor(path);
117 let c_path = CString::new(probe.as_os_str().as_bytes())
118 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains NUL byte"))?;
119 let mut stat = std::mem::MaybeUninit::<libc::statvfs>::uninit();
120 let result = unsafe { libc::statvfs(c_path.as_ptr(), stat.as_mut_ptr()) };
121 if result != 0 {
122 return Err(io::Error::last_os_error());
123 }
124 let stat = unsafe { stat.assume_init() };
125 Ok((stat.f_bavail as u64).saturating_mul(stat.f_frsize as u64))
126 }
127
128 #[cfg(windows)]
129 {
130 let _ = path;
131 Ok(u64::MAX)
135 }
136}
137
138pub fn is_legacy_harness_partition_path(storage_root: &Path, candidate: &Path) -> bool {
141 let storage_root = lexical_normalize(storage_root);
142 let candidate = if candidate.is_absolute() {
143 lexical_normalize(candidate)
144 } else {
145 lexical_normalize(&storage_root.join(candidate))
146 };
147
148 let Ok(relative) = candidate.strip_prefix(&storage_root) else {
149 return false;
150 };
151
152 let mut components = relative.components();
153 let Some(Component::Normal(_harness)) = components.next() else {
154 return false;
155 };
156 let Some(Component::Normal(domain)) = components.next() else {
157 return false;
158 };
159
160 matches!(domain.to_str(), Some("callgraph" | "inspect"))
161}
162
163#[track_caller]
164pub fn debug_assert_not_legacy_harness_partition_path(storage_root: &Path, candidate: &Path) {
165 debug_assert!(
166 !is_legacy_harness_partition_path(storage_root, candidate),
167 "new-layout write path must not point into a legacy harness partition: {}",
168 candidate.display()
169 );
170}
171
172pub fn refuse_legacy_partition_write(
173 storage_root: &Path,
174 candidate: &Path,
175 operation: &str,
176) -> io::Result<()> {
177 if is_legacy_harness_partition_path(storage_root, candidate) {
178 return Err(io::Error::new(
179 io::ErrorKind::PermissionDenied,
180 format!(
181 "refusing {operation} into legacy harness partition {}",
182 candidate.display()
183 ),
184 ));
185 }
186 Ok(())
187}
188
189#[track_caller]
190pub fn guard_new_layout_write_path(
191 storage_root: &Path,
192 candidate: &Path,
193 operation: &str,
194) -> io::Result<()> {
195 debug_assert_not_legacy_harness_partition_path(storage_root, candidate);
196 refuse_legacy_partition_write(storage_root, candidate, operation)
197}
198
199pub fn inventory_legacy_partitions(
200 storage_root: &Path,
201) -> io::Result<Vec<LegacyPartitionInventoryEntry>> {
202 let storage_root = lexical_normalize(storage_root);
203 let boundary = match crate::walk_boundary::DeviceBoundary::for_root(&storage_root) {
204 Ok(boundary) => boundary,
205 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
208 Err(error) => return Err(error),
209 };
210 let mut entries = Vec::new();
211 for harness_entry in sorted_read_dir(&storage_root)? {
212 if !harness_entry.file_type()?.is_dir() {
213 continue;
214 }
215 let harness = harness_entry.file_name().to_string_lossy().to_string();
216 let harness_path = harness_entry.path();
217 if !boundary.should_descend(&harness_path)? {
220 crate::slog_warn!(
221 "legacy partition inventory skipped foreign filesystem mount {}",
222 harness_path.display()
223 );
224 continue;
225 }
226 let callgraph_path = harness_path.join(LegacyPartitionKind::Callgraph.as_str());
227 if callgraph_path.is_dir() && boundary.should_descend(&callgraph_path)? {
228 entries.extend(scan_legacy_callgraph_partitions(
229 &harness,
230 &callgraph_path,
231 &boundary,
232 )?);
233 }
234 let inspect_path = harness_path.join(LegacyPartitionKind::Inspect.as_str());
235 if inspect_path.is_dir() && boundary.should_descend(&inspect_path)? {
236 entries.extend(scan_legacy_inspect_partitions(
237 &harness,
238 &inspect_path,
239 &boundary,
240 )?);
241 }
242 }
243 entries.sort_by(|left, right| {
244 left.harness
245 .cmp(&right.harness)
246 .then_with(|| left.kind.as_str().cmp(right.kind.as_str()))
247 .then_with(|| left.key.cmp(&right.key))
248 });
249 Ok(entries)
250}
251
252pub fn summarize_legacy_partition_duplication(
253 storage_root: &Path,
254) -> io::Result<Vec<LegacyHarnessDuplication>> {
255 let mut summaries = BTreeMap::<String, LegacyHarnessDuplication>::new();
256 for entry in inventory_legacy_partitions(storage_root)? {
257 let summary =
258 summaries
259 .entry(entry.harness.clone())
260 .or_insert_with(|| LegacyHarnessDuplication {
261 harness: entry.harness.clone(),
262 partitions: 0,
263 bytes: 0,
264 });
265 summary.partitions += 1;
266 summary.bytes = summary.bytes.saturating_add(entry.bytes);
267 }
268 Ok(summaries.into_values().collect())
269}
270
271#[derive(Clone, Debug, Default)]
272struct PartitionAccumulator {
273 bytes: u64,
274 callgraph_pointer_mtime: Option<SystemTime>,
275 inspect_tier2_last_full_run: Option<i64>,
276}
277
278fn scan_legacy_callgraph_partitions(
279 harness: &str,
280 callgraph_dir: &Path,
281 boundary: &crate::walk_boundary::DeviceBoundary,
282) -> io::Result<Vec<LegacyPartitionInventoryEntry>> {
283 let mut partitions = BTreeMap::<String, PartitionAccumulator>::new();
284 for entry in sorted_read_dir(callgraph_dir)? {
285 let file_type = entry.file_type()?;
286 let name = entry.file_name().to_string_lossy().to_string();
287 if file_type.is_dir() {
288 if !looks_like_partition_key(&name) {
289 continue;
290 }
291 let partition = partitions.entry(name.clone()).or_default();
292 partition.bytes = partition
293 .bytes
294 .saturating_add(tree_size(&entry.path(), boundary)?);
295 if partition.callgraph_pointer_mtime.is_none() {
296 partition.callgraph_pointer_mtime = callgraph_pointer_mtime(callgraph_dir, &name);
297 }
298 continue;
299 }
300
301 let Some(key) = callgraph_partition_key_from_name(&name) else {
302 continue;
303 };
304 let partition = partitions.entry(key.clone()).or_default();
305 partition.bytes = partition.bytes.saturating_add(file_size(&entry.path())?);
306 if name.ends_with(".current") {
307 partition.callgraph_pointer_mtime =
308 entry.metadata().and_then(|meta| meta.modified()).ok();
309 }
310 }
311
312 Ok(partitions
313 .into_iter()
314 .map(|(key, partition)| LegacyPartitionInventoryEntry {
315 harness: harness.to_string(),
316 kind: LegacyPartitionKind::Callgraph,
317 path: callgraph_dir.join(&key),
318 key,
319 bytes: partition.bytes,
320 callgraph_pointer_mtime: partition.callgraph_pointer_mtime,
321 inspect_tier2_last_full_run: None,
322 })
323 .collect())
324}
325
326fn scan_legacy_inspect_partitions(
327 harness: &str,
328 inspect_dir: &Path,
329 boundary: &crate::walk_boundary::DeviceBoundary,
330) -> io::Result<Vec<LegacyPartitionInventoryEntry>> {
331 let mut partitions = BTreeMap::<String, PartitionAccumulator>::new();
332 for entry in sorted_read_dir(inspect_dir)? {
333 let file_type = entry.file_type()?;
334 let name = entry.file_name().to_string_lossy().to_string();
335 if file_type.is_dir() {
336 if !looks_like_partition_key(&name) {
337 continue;
338 }
339 let partition = partitions.entry(name.clone()).or_default();
340 partition.bytes = partition
341 .bytes
342 .saturating_add(tree_size(&entry.path(), boundary)?);
343 if partition.inspect_tier2_last_full_run.is_none() {
344 partition.inspect_tier2_last_full_run =
345 inspect_tier2_last_full_run(inspect_dir, &name);
346 }
347 continue;
348 }
349
350 let Some(key) = inspect_partition_key_from_name(&name) else {
351 continue;
352 };
353 let partition = partitions.entry(key.clone()).or_default();
354 partition.bytes = partition.bytes.saturating_add(file_size(&entry.path())?);
355 }
356
357 Ok(partitions
358 .into_iter()
359 .map(|(key, mut partition)| {
360 if partition.inspect_tier2_last_full_run.is_none() {
361 partition.inspect_tier2_last_full_run =
362 inspect_tier2_last_full_run(inspect_dir, &key);
363 }
364 LegacyPartitionInventoryEntry {
365 harness: harness.to_string(),
366 kind: LegacyPartitionKind::Inspect,
367 path: inspect_dir.join(&key),
368 key,
369 bytes: partition.bytes,
370 callgraph_pointer_mtime: None,
371 inspect_tier2_last_full_run: partition.inspect_tier2_last_full_run,
372 }
373 })
374 .collect())
375}
376
377fn callgraph_pointer_mtime(callgraph_dir: &Path, key: &str) -> Option<SystemTime> {
378 for candidate in [
379 callgraph_dir.join(format!("{key}.current")),
380 callgraph_dir.join(key).join(format!("{key}.current")),
381 ] {
382 if let Ok(modified) = fs::metadata(candidate).and_then(|metadata| metadata.modified()) {
383 return Some(modified);
384 }
385 }
386 None
387}
388
389fn inspect_tier2_last_full_run(inspect_dir: &Path, key: &str) -> Option<i64> {
390 let sqlite_path = [
391 inspect_dir.join(format!("{key}.sqlite")),
392 inspect_dir.join(key).join(format!("{key}.sqlite")),
393 ]
394 .into_iter()
395 .find(|candidate| candidate.is_file())?;
396
397 let conn = Connection::open_with_flags(&sqlite_path, OpenFlags::SQLITE_OPEN_READ_ONLY).ok()?;
398 conn.busy_timeout(Duration::from_millis(500)).ok()?;
399 conn.query_row("SELECT MAX(last_full_run) FROM tier2_meta", [], |row| {
400 row.get::<_, Option<i64>>(0)
401 })
402 .ok()
403 .flatten()
404}
405
406fn callgraph_partition_key_from_name(name: &str) -> Option<String> {
407 if name.contains(".tmp.") {
408 return None;
409 }
410 if let Some(key) = name.strip_suffix(".current") {
411 return looks_like_partition_key(key).then(|| key.to_string());
412 }
413 let base = sqliteish_base_name(name)?;
414 let key = if let Some((candidate, generation)) = base.split_once(".g") {
415 if generation.is_empty() {
416 return None;
417 }
418 candidate
419 } else {
420 base
421 };
422 looks_like_partition_key(key).then(|| key.to_string())
423}
424
425fn inspect_partition_key_from_name(name: &str) -> Option<String> {
426 if name.contains(".tmp.") {
427 return None;
428 }
429 let base = sqliteish_base_name(name)?;
430 looks_like_partition_key(base).then(|| base.to_string())
431}
432
433fn sqliteish_base_name(name: &str) -> Option<&str> {
434 SQLITE_SUFFIXES
435 .iter()
436 .find_map(|suffix| name.strip_suffix(suffix))
437}
438
439fn looks_like_partition_key(value: &str) -> bool {
440 value.len() == 16 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
441}
442
443fn file_size(path: &Path) -> io::Result<u64> {
444 Ok(fs::metadata(path)?.len())
445}
446
447fn tree_size(path: &Path, boundary: &crate::walk_boundary::DeviceBoundary) -> io::Result<u64> {
448 if !path.exists() {
449 return Ok(0);
450 }
451 let metadata = fs::metadata(path)?;
452 if metadata.is_file() {
453 return Ok(metadata.len());
454 }
455 if !metadata.is_dir() {
456 return Ok(0);
457 }
458 if !boundary.should_descend(path)? {
459 crate::slog_warn!(
460 "legacy partition inventory skipped foreign filesystem mount {}",
461 path.display()
462 );
463 return Ok(0);
464 }
465
466 let mut total = 0_u64;
467 for entry in fs::read_dir(path)? {
468 total = total.saturating_add(tree_size(&entry?.path(), boundary)?);
469 }
470 Ok(total)
471}
472
473fn sorted_read_dir(path: &Path) -> io::Result<Vec<fs::DirEntry>> {
474 let entries = match fs::read_dir(path) {
475 Ok(entries) => entries,
476 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
477 Err(error) => return Err(error),
478 };
479 let mut entries = entries.collect::<io::Result<Vec<_>>>()?;
480 entries.sort_by_key(|entry| entry.file_name());
481 Ok(entries)
482}
483
484fn lexical_normalize(path: &Path) -> PathBuf {
485 let mut normalized = PathBuf::new();
486 for component in path.components() {
487 match component {
488 Component::ParentDir => {
489 if !normalized.pop() {
490 normalized.push(component);
491 }
492 }
493 Component::CurDir => {}
494 other => normalized.push(other.as_os_str()),
495 }
496 }
497 normalized
498}
499
500#[cfg(unix)]
501fn existing_ancestor(path: &Path) -> &Path {
502 let mut current = path;
503 while !current.exists() {
504 if let Some(parent) = current.parent() {
505 current = parent;
506 } else {
507 break;
508 }
509 }
510 current
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516 use filetime::FileTime;
517 use rusqlite::params;
518 use std::panic::catch_unwind;
519 use std::time::UNIX_EPOCH;
520 use tempfile::TempDir;
521
522 #[test]
523 fn legacy_partition_guard_matches_exact_domains() {
524 let storage_root = PathBuf::from("/tmp/aft-storage");
525 assert!(is_legacy_harness_partition_path(
526 &storage_root,
527 &storage_root.join("opencode/callgraph/0123456789abcdef.current")
528 ));
529 assert!(is_legacy_harness_partition_path(
530 &storage_root,
531 &storage_root.join("pi/inspect/0123456789abcdef.sqlite")
532 ));
533 assert!(!is_legacy_harness_partition_path(
534 &storage_root,
535 &storage_root.join("opencode/callgraph-old/0123456789abcdef.sqlite")
536 ));
537 assert!(!is_legacy_harness_partition_path(
538 &storage_root,
539 &storage_root.join("index/0123456789abcdef.sqlite")
540 ));
541 assert!(!is_legacy_harness_partition_path(
542 &storage_root,
543 Path::new("/elsewhere/opencode/callgraph/0123456789abcdef.sqlite")
544 ));
545 }
546
547 #[test]
548 fn debug_assert_and_refusal_cover_legacy_write_paths() {
549 let storage_root = PathBuf::from("/tmp/aft-storage");
550 let legacy_target = storage_root.join("opencode/callgraph/0123456789abcdef.sqlite");
551 let panic = catch_unwind(|| {
552 debug_assert_not_legacy_harness_partition_path(&storage_root, &legacy_target);
553 });
554 assert!(panic.is_err());
555
556 let error = refuse_legacy_partition_write(&storage_root, &legacy_target, "publish")
557 .expect_err("legacy write must be refused");
558 assert_eq!(error.kind(), io::ErrorKind::PermissionDenied);
559 assert!(error
560 .to_string()
561 .contains("refusing publish into legacy harness partition"));
562 }
563
564 #[test]
565 fn root_keyed_copy_disk_floor_boundaries() {
566 let exact = evaluate_root_keyed_copy_disk_floor(20, 30);
567 assert_eq!(exact.required_bytes, 30);
568 assert!(exact.allows_copy());
569 assert!(!exact.should_skip_copy());
570
571 let below = evaluate_root_keyed_copy_disk_floor(20, 29);
572 assert!(below.should_skip_copy());
573 assert!(below
574 .warning_message(Path::new("/legacy"), Path::new("/shared"))
575 .contains("1.5× floor"));
576
577 let above = evaluate_root_keyed_copy_disk_floor(20, 31);
578 assert!(above.allows_copy());
579 assert_eq!(
580 below.configure_warning(Path::new("/legacy"), Path::new("/shared"))["kind"],
581 "root_keyed_disk_floor"
582 );
583 }
584
585 #[test]
586 fn inventory_fixture_reports_partition_sizes_and_freshness() {
587 let fixture = write_legacy_inventory_fixture();
588 let storage_root = fixture.temp.path();
589
590 let inventory = inventory_legacy_partitions(storage_root).expect("inventory");
591 assert_eq!(inventory.len(), 2);
592
593 let callgraph = inventory
594 .iter()
595 .find(|entry| entry.kind == LegacyPartitionKind::Callgraph)
596 .expect("callgraph entry");
597 let expected_callgraph_bytes = partition_bytes_on_disk(
598 &storage_root.join("opencode/callgraph"),
599 "0123456789abcdef",
600 callgraph_partition_key_from_name,
601 );
602 assert_eq!(callgraph.harness, "opencode");
603 assert_eq!(callgraph.key, "0123456789abcdef");
604 assert_eq!(
605 callgraph.path,
606 storage_root.join("opencode/callgraph/0123456789abcdef")
607 );
608 assert_eq!(callgraph.bytes, expected_callgraph_bytes);
609 let pointer_secs = callgraph
610 .callgraph_pointer_mtime
611 .expect("pointer mtime")
612 .duration_since(UNIX_EPOCH)
613 .expect("mtime after epoch")
614 .as_secs();
615 assert_eq!(pointer_secs, 1_750_000_000);
616 assert_eq!(callgraph.inspect_tier2_last_full_run, None);
617
618 let inspect = inventory
619 .iter()
620 .find(|entry| entry.kind == LegacyPartitionKind::Inspect)
621 .expect("inspect entry");
622 let minimum_inspect_bytes =
623 file_size(&storage_root.join("pi/inspect/fedcba9876543210.sqlite"))
624 .expect("inspect sqlite size");
625 assert_eq!(inspect.harness, "pi");
626 assert_eq!(inspect.key, "fedcba9876543210");
627 assert_eq!(
628 inspect.path,
629 storage_root.join("pi/inspect/fedcba9876543210")
630 );
631 assert!(inspect.bytes >= minimum_inspect_bytes);
632 assert_eq!(inspect.callgraph_pointer_mtime, None);
633 assert_eq!(inspect.inspect_tier2_last_full_run, Some(250));
634
635 let summary = summarize_legacy_partition_duplication(storage_root).expect("summary");
636 assert_eq!(
637 summary,
638 vec![
639 LegacyHarnessDuplication {
640 harness: "opencode".to_string(),
641 partitions: 1,
642 bytes: expected_callgraph_bytes,
643 },
644 LegacyHarnessDuplication {
645 harness: "pi".to_string(),
646 partitions: 1,
647 bytes: inspect.bytes,
648 },
649 ]
650 );
651 }
652
653 struct LegacyInventoryFixture {
654 temp: TempDir,
655 }
656
657 fn write_legacy_inventory_fixture() -> LegacyInventoryFixture {
658 let temp = tempfile::tempdir().expect("tempdir");
659
660 let callgraph_dir = temp.path().join("opencode/callgraph");
661 fs::create_dir_all(&callgraph_dir).expect("create callgraph dir");
662 fs::write(
663 callgraph_dir.join("0123456789abcdef.current"),
664 b"0123456789abcdef.g1.1.sqlite\n",
665 )
666 .expect("write pointer");
667 fs::write(
668 callgraph_dir.join("0123456789abcdef.g1.1.sqlite"),
669 b"callgraph-db",
670 )
671 .expect("write generation db");
672 fs::write(
673 callgraph_dir.join("0123456789abcdef.g1.1.sqlite-wal"),
674 b"wal",
675 )
676 .expect("write generation wal");
677 fs::write(
678 callgraph_dir.join("0123456789abcdef.current.tmp.123"),
679 b"ignored-temp",
680 )
681 .expect("write ignored temp");
682 filetime::set_file_mtime(
683 callgraph_dir.join("0123456789abcdef.current"),
684 FileTime::from_unix_time(1_750_000_000, 0),
685 )
686 .expect("set pointer mtime");
687
688 let inspect_dir = temp.path().join("pi/inspect");
689 fs::create_dir_all(&inspect_dir).expect("create inspect dir");
690 let sqlite_path = inspect_dir.join("fedcba9876543210.sqlite");
691 let conn = Connection::open(&sqlite_path).expect("open inspect db");
692 conn.execute(
693 "CREATE TABLE tier2_meta (category TEXT NOT NULL, project_key TEXT NOT NULL, last_full_run INTEGER NOT NULL)",
694 [],
695 )
696 .expect("create tier2_meta");
697 conn.execute(
698 "INSERT INTO tier2_meta (category, project_key, last_full_run) VALUES (?1, ?2, ?3)",
699 params!["dead_code", "fedcba9876543210", 100_i64],
700 )
701 .expect("insert first tier2 row");
702 conn.execute(
703 "INSERT INTO tier2_meta (category, project_key, last_full_run) VALUES (?1, ?2, ?3)",
704 params!["duplicates", "fedcba9876543210", 250_i64],
705 )
706 .expect("insert second tier2 row");
707 drop(conn);
708 fs::write(inspect_dir.join("misc.txt"), b"ignored").expect("write ignored file");
709
710 LegacyInventoryFixture { temp }
711 }
712
713 fn partition_bytes_on_disk(
714 domain_dir: &Path,
715 expected_key: &str,
716 key_fn: fn(&str) -> Option<String>,
717 ) -> u64 {
718 sorted_read_dir(domain_dir)
719 .expect("read partition dir")
720 .into_iter()
721 .filter_map(|entry| {
722 let name = entry.file_name().to_string_lossy().to_string();
723 let key = key_fn(&name)?;
724 if key != expected_key {
725 return None;
726 }
727 Some(file_size(&entry.path()).expect("partition file size"))
728 })
729 .sum()
730 }
731}