1use std::{
8 fs::{self, File},
9 io::{self, Read},
10 path::{Path, PathBuf},
11};
12
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15use supercov_contracts::EVIDENCE_ARCHIVE_SCHEMA_VERSION;
16
17use crate::query_index::{QUERY_INDEX_SCHEMA_VERSION, QueryIndexIdentity};
18use crate::{
19 coverage_index::{CoverageIndex, CoverageIndexError, coverage_index_sections},
20 coverage_report::{
21 ArchiveReportRequest, CoverageReport, ExitCodeInput, ReportError, analyze_coverage_archive,
22 },
23 evidence_archive::read_archive_schema_version,
24 query_index::{QueryIndex, QueryIndexError, write_query_index},
25};
26
27const MAX_RUN_METADATA_BYTES: u64 = 1024 * 1024;
28pub const RUST_ANALYSIS_ABI_VERSION: u32 = 1;
29pub const RUST_QUERY_PRODUCER_ABI_VERSION: u32 = 4;
30pub const RUST_QUERY_INDEX_FILE: &str = "query-index.v1.bin";
31pub const STATEMENT_SPANS_FILE: &str = "statement-spans.json";
33pub const ASSESSMENT_INPUT_FILE: &str = "assessment-input.json.gz";
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase", deny_unknown_fields)]
38pub struct RunFingerprint {
39 pub algorithm: String,
40 pub source: String,
41 pub tests: String,
42 pub dependencies: String,
43 pub configuration: String,
44 pub instrumenter: String,
45 pub execution: String,
46 pub combined: String,
47 pub source_files: usize,
48 pub test_files: usize,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53pub struct GitIntegrity {
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub revision: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub branch: Option<String>,
59 pub dirty: bool,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase", deny_unknown_fields)]
64pub struct RunIntegrity {
65 pub schema_version: u32,
66 pub instrumenter_version: String,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub git: Option<GitIntegrity>,
69 pub fingerprint: RunFingerprint,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub stale: Option<bool>,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub stale_reasons: Option<Vec<String>>,
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase", deny_unknown_fields)]
78pub struct RunTimings {
79 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
80 pub initialization_ms: f64,
81 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
82 pub workspace_preparation_ms: f64,
83 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
84 pub adapter_setup_ms: f64,
85 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
86 pub instrumented_build_ms: f64,
87 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
88 pub test_command_ms: f64,
89 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
90 pub evidence_publication_ms: f64,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase", deny_unknown_fields)]
95pub struct InstrumentedBuildCache {
96 pub key: String,
97 pub reused: bool,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase", deny_unknown_fields)]
102pub struct RawEvidenceMetadata {
103 pub schema_version: u32,
104 pub format: String,
105 pub file: String,
106 pub files: usize,
107 pub uncompressed_bytes: u64,
108 pub compressed_bytes: u64,
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase", deny_unknown_fields)]
113pub struct RunMetadata {
114 pub id: String,
115 pub started_at: String,
116 #[serde(serialize_with = "crate::coverage_analysis::serialize_javascript_number")]
117 pub duration_ms: f64,
118 pub command: Vec<String>,
119 pub test_exit_code: Option<i32>,
120 pub integrity: RunIntegrity,
121 pub raw_evidence: RawEvidenceMetadata,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub isolated_build: Option<bool>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub instrumented_build_cache: Option<InstrumentedBuildCache>,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub timings: Option<RunTimings>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub merged: Option<bool>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub parents: Option<Vec<String>>,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub source_roots: Option<Vec<String>>,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub copy: Option<usize>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub merged_across: Option<Vec<String>>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub recorded_by: Option<String>,
155}
156
157#[cfg(test)]
158pub(crate) fn create_analyzable_test_run(root: &Path, id: &str) -> PathBuf {
159 create_analyzable_run(root, id, None)
160}
161
162#[cfg(test)]
167pub(crate) fn create_run_wide_test_run(root: &Path, id: &str) -> PathBuf {
168 create_analyzable_run(root, id, Some(crate::coverage_report::ATTRIBUTION_RUN_WIDE))
169}
170
171#[cfg(test)]
175pub(crate) fn create_partial_test_run(root: &Path, id: &str) -> PathBuf {
176 create_analyzable_run(root, id, Some(crate::coverage_report::ATTRIBUTION_PARTIAL))
177}
178
179#[cfg(test)]
180fn create_analyzable_run(root: &Path, id: &str, attribution: Option<&str>) -> PathBuf {
181 use crate::{
182 coverage_analysis::{McdcVector, PointKind},
183 coverage_report::{
184 BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, DecisionSnapshot,
185 PointMeta, RawTestResult, RuntimeSnapshot, TestProvenance,
186 },
187 evidence_archive::{EvidenceArchiveEntry, write_archive},
188 };
189
190 let directory = root.join(".supercov/runs").join(id);
191 fs::create_dir_all(&directory).unwrap();
192 let decision = DecisionMeta {
193 id: "decision".into(),
194 file: "src/app.js".into(),
195 line: 1,
196 column: 0,
197 source: "left && right".into(),
198 conditions: vec!["left".into(), "right".into()],
199 kind: "if".into(),
200 };
201 let manifest = CoverageManifest {
202 unmeasured: Vec::new(),
203 decisions: vec![decision.clone()],
204 points: vec![PointMeta {
205 id: "statement".into(),
206 kind: PointKind::Statement,
207 file: "src/app.js".into(),
208 line: 1,
209 column: 0,
210 source: "work();".into(),
211 label: None,
212 }],
213 branches: vec![BranchMeta {
214 id: "branch".into(),
215 kind: "if".into(),
216 file: "src/app.js".into(),
217 line: 1,
218 column: 0,
219 source: "if (left && right)".into(),
220 alternatives: vec![
221 BranchAlternativeMeta {
222 id: "branch:true".into(),
223 label: "true".into(),
224 },
225 BranchAlternativeMeta {
226 id: "branch:false".into(),
227 label: "false".into(),
228 },
229 ],
230 }],
231 limitations: vec![],
232 scope: None,
233 assertion_sites: Vec::new(),
234 };
235 let result = RawTestResult {
236 test_id: Some("test".into()),
237 scope: None,
238 test: "test".into(),
239 test_file: Some("tests/app.test.js".into()),
240 title: None,
241 retry: Some(0),
242 status: Some("passed".into()),
243 expected_status: None,
244 flaky: false,
245 provenance: TestProvenance {
246 runner: "node:test".into(),
247 kind: "unit".into(),
248 project: None,
249 source: "test-fixture".into(),
250 },
251 role: "test".into(),
252 attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
253 phases: vec![],
254 runtime: vec![RuntimeSnapshot {
255 decisions: vec![DecisionSnapshot {
256 meta: decision,
257 vectors: vec![
258 McdcVector {
259 values: vec![Some(false), Some(false)],
260 outcome: false,
261 },
262 McdcVector {
263 values: vec![Some(false), Some(true)],
264 outcome: false,
265 },
266 McdcVector {
267 values: vec![Some(true), Some(false)],
268 outcome: false,
269 },
270 McdcVector {
271 values: vec![Some(true), Some(true)],
272 outcome: true,
273 },
274 ],
275 }],
276 hits: vec![
277 "statement".into(),
278 "branch:true".into(),
279 "branch:false".into(),
280 ],
281 events: vec![],
282 logicals: vec![],
283 phase_id: None,
284 }],
285 browser: vec![],
286 server: vec![],
287 };
288 let results = if attribution == Some(crate::coverage_report::ATTRIBUTION_PARTIAL) {
289 let mut partial = result.clone();
291 partial.attribution = crate::coverage_report::ATTRIBUTION_PARTIAL.into();
292 vec![partial]
293 } else if attribution == Some(crate::coverage_report::ATTRIBUTION_RUN_WIDE) {
294 let mut unclaimed = result.clone();
295 unclaimed.test = "run".into();
296 unclaimed.test_id = Some("run".into());
297 unclaimed.test_file = None;
298 unclaimed.role = "background".into();
299 let mut parallel = result.clone();
300 parallel.attribution = crate::coverage_report::ATTRIBUTION_RUN_WIDE.into();
301 parallel.runtime = vec![RuntimeSnapshot {
303 decisions: vec![],
304 hits: vec![],
305 events: vec![],
306 logicals: vec![],
307 phase_id: None,
308 }];
309 vec![unclaimed, parallel]
310 } else {
311 vec![result]
312 };
313 let mut entries = vec![
314 EvidenceArchiveEntry {
315 path: "coverage-model.json".into(),
316 contents: serde_json::to_vec(&serde_json::json!({
317 "schemaVersion": 1,
318 "language": "javascript",
319 "variant": "fixture-v1",
320 "name": "Fixture model",
321 "completenessMeaning": "Every fixture obligation was observed.",
322 "measured": ["fixture obligations"],
323 "notMeasured": []
324 }))
325 .unwrap(),
326 },
327 EvidenceArchiveEntry {
328 path: "frontend.json".into(),
329 contents: serde_json::to_vec(&serde_json::json!({
330 "protocolVersion": 2,
331 "frontendId": "javascript",
332 "frontendVersion": "fixture-v1",
333 "language": "javascript",
334 "structuralSource": "owned-probes",
335 "runners": [{
336 "runner": "node:test",
337 "executionModel": "serial-in-process",
338 "attribution": {
339 "run": "exact",
340 "worker": "unavailable",
341 "test": "exact",
342 "retry": "exact",
343 "phase": "exact",
344 "action": "exact",
345 "assertion": "exact"
346 },
347 "limitations": [{
348 "id": "fixture-worker-unavailable",
349 "scopes": ["worker"],
350 "reason": "The fixture intentionally has no worker identity"
351 }]
352 }],
353 "structuralLimitations": []
354 }))
355 .unwrap(),
356 },
357 EvidenceArchiveEntry {
358 path: "manifest.json".into(),
359 contents: serde_json::to_vec(&manifest).unwrap(),
360 },
361 ];
362 for (at, result) in results.iter().enumerate() {
363 entries.push(EvidenceArchiveEntry {
364 path: format!(
368 "worker{}/mcdc.json",
369 if at == 0 {
370 String::new()
371 } else {
372 format!("-{at}")
373 }
374 ),
375 contents: serde_json::to_vec(result).unwrap(),
376 });
377 }
378 let archive = write_archive(entries, &directory.join("evidence.raw.gz")).unwrap();
379 let digest = |character: char| std::iter::repeat_n(character, 64).collect::<String>();
380 let metadata = RunMetadata {
381 id: id.into(),
382 started_at: id.into(),
383 duration_ms: 1.0,
384 command: vec!["node".into(), "--test".into()],
385 test_exit_code: Some(0),
386 integrity: RunIntegrity {
387 schema_version: 2,
388 instrumenter_version: "test".into(),
389 git: None,
390 fingerprint: RunFingerprint {
391 algorithm: "sha256".into(),
392 source: digest('a'),
393 tests: digest('b'),
394 dependencies: digest('c'),
395 configuration: digest('d'),
396 instrumenter: digest('e'),
397 execution: digest('f'),
398 combined: digest('0'),
399 source_files: 1,
400 test_files: 1,
401 },
402 stale: None,
403 stale_reasons: None,
404 },
405 raw_evidence: RawEvidenceMetadata {
406 schema_version: archive.schema_version,
407 format: archive.format.into(),
408 file: archive.file.into(),
409 files: archive.files,
410 uncompressed_bytes: archive.uncompressed_bytes,
411 compressed_bytes: archive.compressed_bytes,
412 },
413 isolated_build: Some(true),
414 instrumented_build_cache: None,
415 timings: None,
416 merged: None,
417 parents: None,
418 source_roots: None,
419 copy: None,
420 merged_across: None,
421 recorded_by: None,
422 };
423 fs::write(
424 directory.join("run.json"),
425 serde_json::to_vec_pretty(&metadata).unwrap(),
426 )
427 .unwrap();
428 directory
429}
430
431#[derive(Debug, Clone, PartialEq)]
432pub struct StoredRun {
433 pub id: String,
434 pub directory: PathBuf,
435 pub evidence_path: PathBuf,
436 pub metadata_path: PathBuf,
437 pub query_index_path: PathBuf,
438 pub metadata: RunMetadata,
439}
440
441#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
442#[serde(rename_all = "camelCase")]
443pub struct RejectedRun {
444 pub entry: String,
445 pub reason: String,
446}
447
448#[derive(Debug, Clone, PartialEq)]
449pub struct RunInventory {
450 pub runs: Vec<StoredRun>,
451 pub rejected: Vec<RejectedRun>,
452}
453
454#[derive(Debug)]
455pub enum RunStoreError {
456 Io(io::Error),
457 UnsafeStore(PathBuf),
458 NoRuns,
459 RunNotFound(String),
460 InvalidRun(&'static str),
461}
462
463impl From<io::Error> for RunStoreError {
464 fn from(value: io::Error) -> Self {
465 Self::Io(value)
466 }
467}
468
469impl std::fmt::Display for RunStoreError {
470 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471 match self {
472 Self::Io(error) => write!(formatter, "{error}"),
473 Self::UnsafeStore(path) => {
474 write!(formatter, "unsafe run-store path: {}", path.display())
475 }
476 Self::NoRuns => write!(formatter, "no local coverage runs"),
477 Self::RunNotFound(selector) => write!(formatter, "coverage run not found: {selector}"),
478 Self::InvalidRun(reason) => write!(formatter, "invalid coverage run: {reason}"),
479 }
480 }
481}
482
483impl std::error::Error for RunStoreError {}
484
485fn regular_file(path: &Path) -> Result<fs::Metadata, RunStoreError> {
486 let metadata = fs::symlink_metadata(path)?;
487 if !metadata.file_type().is_file() {
488 return Err(RunStoreError::UnsafeStore(path.to_owned()));
489 }
490 Ok(metadata)
491}
492
493pub(crate) fn valid_run_id(value: &str) -> bool {
494 !value.is_empty()
495 && value != "."
496 && value != ".."
497 && !value
498 .chars()
499 .any(|character| matches!(character, '/' | '\\' | '\0') || character.is_control())
500}
501
502fn valid_hex_digest(value: &str, length: usize) -> bool {
503 value.len() == length
504 && value
505 .bytes()
506 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
507}
508
509fn valid_sha256(value: &str) -> bool {
510 valid_hex_digest(value, 64)
511}
512
513fn validate_integrity(integrity: &RunIntegrity) -> Result<(), RunStoreError> {
514 let fingerprint = &integrity.fingerprint;
515 if fingerprint.algorithm != "sha256"
516 || ![
517 &fingerprint.source,
518 &fingerprint.tests,
519 &fingerprint.dependencies,
520 &fingerprint.configuration,
521 &fingerprint.instrumenter,
522 &fingerprint.execution,
523 &fingerprint.combined,
524 ]
525 .into_iter()
526 .all(|digest| valid_sha256(digest))
527 {
528 return Err(RunStoreError::InvalidRun("integrity fingerprint"));
529 }
530 if let Some(git) = &integrity.git
531 && git.revision.as_ref().is_some_and(|revision| {
532 !valid_hex_digest(revision, 40) && !valid_hex_digest(revision, 64)
533 })
534 {
535 return Err(RunStoreError::InvalidRun("git revision"));
536 }
537 Ok(())
538}
539
540fn read_metadata(path: &Path) -> Result<RunMetadata, RunStoreError> {
541 let metadata = regular_file(path)?;
542 if metadata.len() > MAX_RUN_METADATA_BYTES {
543 return Err(RunStoreError::InvalidRun("run metadata is too large"));
544 }
545 let capacity =
546 usize::try_from(metadata.len()).map_err(|_| RunStoreError::InvalidRun("metadata size"))?;
547 let mut bytes = Vec::with_capacity(capacity);
548 File::open(path)?
549 .take(MAX_RUN_METADATA_BYTES + 1)
550 .read_to_end(&mut bytes)?;
551 let metadata: RunMetadata = serde_json::from_slice(&bytes)
552 .map_err(|_| RunStoreError::InvalidRun("run metadata JSON"))?;
553 validate_integrity(&metadata.integrity)?;
554 Ok(metadata)
555}
556
557fn load_run(directory: &Path, entry: &str) -> Result<StoredRun, RunStoreError> {
558 if !valid_run_id(entry) {
559 return Err(RunStoreError::InvalidRun("run directory name"));
560 }
561 let directory_metadata = fs::symlink_metadata(directory)?;
562 if !directory_metadata.file_type().is_dir() {
563 return Err(RunStoreError::UnsafeStore(directory.to_owned()));
564 }
565 let metadata_path = directory.join("run.json");
566 let metadata = read_metadata(&metadata_path)?;
567 if metadata.id != entry {
568 return Err(RunStoreError::InvalidRun(
569 "metadata ID differs from directory",
570 ));
571 }
572 if metadata.raw_evidence.schema_version != EVIDENCE_ARCHIVE_SCHEMA_VERSION
573 || metadata.raw_evidence.format != "framed+gzip"
574 || metadata.raw_evidence.file != "evidence.raw.gz"
575 || metadata.raw_evidence.files == 0
576 {
577 return Err(RunStoreError::InvalidRun("raw evidence metadata"));
578 }
579 let evidence_path = directory.join("evidence.raw.gz");
580 let evidence_metadata = regular_file(&evidence_path)?;
581 if evidence_metadata.len() != metadata.raw_evidence.compressed_bytes {
582 return Err(RunStoreError::InvalidRun("raw evidence length"));
583 }
584 if read_archive_schema_version(&evidence_path)
585 .map_err(|_| RunStoreError::InvalidRun("raw evidence archive"))?
586 != metadata.raw_evidence.schema_version
587 {
588 return Err(RunStoreError::InvalidRun("raw evidence schema mismatch"));
589 }
590 Ok(StoredRun {
591 id: entry.into(),
592 directory: directory.to_owned(),
593 evidence_path,
594 metadata_path,
595 query_index_path: directory.join(RUST_QUERY_INDEX_FILE),
596 metadata,
597 })
598}
599
600pub fn discover_runs(project_root: &Path) -> Result<RunInventory, RunStoreError> {
601 let store = project_root.join(".supercov").join("runs");
602 let store_metadata = match fs::symlink_metadata(&store) {
603 Ok(metadata) => metadata,
604 Err(error) if error.kind() == io::ErrorKind::NotFound => {
605 return Ok(RunInventory {
606 runs: Vec::new(),
607 rejected: Vec::new(),
608 });
609 }
610 Err(error) => return Err(error.into()),
611 };
612 if !store_metadata.file_type().is_dir() {
613 return Err(RunStoreError::UnsafeStore(store));
614 }
615 let mut runs = Vec::new();
616 let mut rejected = Vec::new();
617 for entry in fs::read_dir(&store)? {
618 let entry = match entry {
619 Ok(entry) => entry,
620 Err(error) => {
621 rejected.push(RejectedRun {
622 entry: "<unreadable>".into(),
623 reason: error.to_string(),
624 });
625 continue;
626 }
627 };
628 let name = entry.file_name().to_string_lossy().into_owned();
629 match load_run(&entry.path(), &name) {
630 Ok(run) => runs.push(run),
631 Err(error) => rejected.push(RejectedRun {
632 entry: name,
633 reason: error.to_string(),
634 }),
635 }
636 }
637 runs.sort_by(|left, right| {
638 right
639 .metadata
640 .started_at
641 .cmp(&left.metadata.started_at)
642 .then_with(|| right.id.cmp(&left.id))
643 });
644 rejected.sort_by(|left, right| left.entry.cmp(&right.entry));
645 Ok(RunInventory { runs, rejected })
646}
647
648pub fn select_run<'a>(
649 inventory: &'a RunInventory,
650 selector: Option<&str>,
651) -> Result<&'a StoredRun, RunStoreError> {
652 if inventory.runs.is_empty() {
653 return Err(RunStoreError::NoRuns);
654 }
655 if selector.is_none() || selector == Some("latest") {
656 return Ok(&inventory.runs[0]);
657 }
658 let selector = selector.expect("checked selector");
659 inventory
660 .runs
661 .iter()
662 .find(|run| run.id == selector)
663 .or_else(|| {
664 inventory
665 .runs
666 .iter()
667 .find(|run| run.id.starts_with(selector))
668 })
669 .ok_or_else(|| RunStoreError::RunNotFound(selector.into()))
670}
671
672#[derive(Debug, Clone, PartialEq, Eq)]
673pub struct IntegrityComparison {
674 pub stale: bool,
675 pub reasons: Vec<String>,
676}
677
678pub fn compare_run_integrity(
679 stored: Option<&RunIntegrity>,
680 current: &RunIntegrity,
681) -> IntegrityComparison {
682 let Some(stored) = stored else {
683 return IntegrityComparison {
684 stale: true,
685 reasons: vec!["run predates integrity fingerprints".into()],
686 };
687 };
688 if std::env::var_os("SUPERCOV_DEBUG_INTEGRITY").is_some() {
689 eprintln!("[integrity] stored : {stored:?}");
690 eprintln!("[integrity] current: {current:?}");
691 }
692 let mut reasons = Vec::new();
693 if stored.schema_version != current.schema_version {
694 reasons.push("coverage schema changed".into());
695 }
696 if stored.instrumenter_version != current.instrumenter_version {
702 reasons.push("instrumenter contract changed".into());
703 }
704 if stored.fingerprint.source != current.fingerprint.source {
705 reasons.push("instrumented source changed".into());
706 }
707 if stored.fingerprint.tests != current.fingerprint.tests {
708 reasons.push("test files changed".into());
709 }
710 if stored.fingerprint.dependencies != current.fingerprint.dependencies {
711 reasons.push("dependencies or lockfile changed".into());
712 }
713 if stored.fingerprint.configuration != current.fingerprint.configuration {
714 reasons.push("test/build configuration changed".into());
715 }
716 if reasons.is_empty() && stored.fingerprint.execution != current.fingerprint.execution {
717 reasons.push("execution environment changed".into());
718 }
719 IntegrityComparison {
720 stale: !reasons.is_empty(),
721 reasons,
722 }
723}
724
725fn file_sha256(path: &Path) -> Result<([u8; 32], u64), RunStoreError> {
726 let metadata = regular_file(path)?;
727 let mut file = File::open(path)?;
728 let mut hash = Sha256::new();
729 let mut buffer = [0_u8; 128 * 1024];
730 loop {
731 let read = file.read(&mut buffer)?;
732 if read == 0 {
733 break;
734 }
735 hash.update(&buffer[..read]);
736 }
737 Ok((hash.finalize().into(), metadata.len()))
738}
739
740fn domain_hash(domain: &str, version: u32) -> [u8; 32] {
741 let mut hash = Sha256::new();
742 hash.update(domain.as_bytes());
743 hash.update([0]);
744 hash.update(version.to_le_bytes());
745 hash.update([0]);
746 hash.update(env!("SUPERCOV_ENGINE_SOURCE_SHA256").as_bytes());
747 hash.finalize().into()
748}
749
750pub fn query_index_identity(run: &StoredRun) -> Result<QueryIndexIdentity, RunStoreError> {
751 let (evidence_sha256, evidence_bytes) = file_sha256(&run.evidence_path)?;
752 Ok(QueryIndexIdentity {
753 evidence_sha256,
754 evidence_bytes,
755 analysis_sha256: domain_hash("supercov-analysis", RUST_ANALYSIS_ABI_VERSION),
756 producer_sha256: domain_hash(
757 "supercov-query-producer",
758 RUST_QUERY_PRODUCER_ABI_VERSION ^ QUERY_INDEX_SCHEMA_VERSION,
759 ),
760 archive_schema_version: run.metadata.raw_evidence.schema_version,
761 })
762}
763
764#[derive(Debug)]
765pub enum RunIndexError {
766 RunStore(RunStoreError),
767 QueryIndex(QueryIndexError),
768 CoverageIndex(CoverageIndexError),
769 Report(ReportError),
770 Metadata(serde_json::Error),
771 EvidenceChanged,
772}
773
774impl std::fmt::Display for RunIndexError {
775 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
776 match self {
777 Self::RunStore(error) => write!(formatter, "{error}"),
778 Self::QueryIndex(error) => write!(formatter, "{error}"),
779 Self::CoverageIndex(error) => write!(formatter, "{error}"),
780 Self::Report(ReportError::NoEvidence(_)) => {
781 write!(formatter, "no coverage evidence was published")
782 }
783 Self::Report(error) => write!(formatter, "coverage analysis failed: {error:?}"),
784 Self::Metadata(error) => write!(formatter, "run integrity is invalid: {error}"),
785 Self::EvidenceChanged => write!(formatter, "evidence changed while indexing"),
786 }
787 }
788}
789
790impl std::error::Error for RunIndexError {}
791
792impl From<RunStoreError> for RunIndexError {
793 fn from(value: RunStoreError) -> Self {
794 Self::RunStore(value)
795 }
796}
797
798impl From<QueryIndexError> for RunIndexError {
799 fn from(value: QueryIndexError) -> Self {
800 Self::QueryIndex(value)
801 }
802}
803
804impl From<CoverageIndexError> for RunIndexError {
805 fn from(value: CoverageIndexError) -> Self {
806 Self::CoverageIndex(value)
807 }
808}
809
810impl From<ReportError> for RunIndexError {
811 fn from(value: ReportError) -> Self {
812 Self::Report(value)
813 }
814}
815
816impl From<serde_json::Error> for RunIndexError {
817 fn from(value: serde_json::Error) -> Self {
818 Self::Metadata(value)
819 }
820}
821
822fn open_validated_query_index(
823 path: &Path,
824 identity: &QueryIndexIdentity,
825) -> Result<QueryIndex, RunIndexError> {
826 let index = QueryIndex::open(path, identity)?;
827 index.verify_all()?;
828 CoverageIndex::new(&index)?;
829 Ok(index)
830}
831
832fn open_written_query_index(
841 path: &Path,
842 identity: &QueryIndexIdentity,
843) -> Result<QueryIndex, RunIndexError> {
844 let index = QueryIndex::open(path, identity)?;
845 CoverageIndex::new(&index)?;
846 Ok(index)
847}
848
849pub fn open_existing_query_index(run: &StoredRun) -> Result<Option<QueryIndex>, RunIndexError> {
851 match fs::symlink_metadata(&run.query_index_path) {
852 Ok(_) => {
853 open_validated_query_index(&run.query_index_path, &query_index_identity(run)?).map(Some)
854 }
855 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
856 Err(error) => Err(RunStoreError::Io(error).into()),
857 }
858}
859
860pub fn analyze_stored_run(run: &StoredRun) -> Result<CoverageReport, RunIndexError> {
862 Ok(analyze_coverage_archive(&ArchiveReportRequest {
863 archive_path: run.evidence_path.clone(),
864 run_id: run.id.clone(),
865 generated_at: run.metadata.started_at.clone(),
866 integrity: Some(serde_json::to_value(&run.metadata.integrity)?),
867 test_exit_code: ExitCodeInput::Present(run.metadata.test_exit_code),
868 })?)
869}
870
871pub fn statement_spans(report: &CoverageReport) -> Vec<serde_json::Value> {
880 report
881 .view
882 .points
883 .iter()
884 .filter(|p| p.meta.kind == crate::coverage_analysis::PointKind::Statement)
885 .filter_map(|p| {
886 let extra = p.meta.source.matches('\n').count();
887 (extra > 0).then(|| {
888 serde_json::json!({
889 "file": p.meta.file, "line": p.meta.line,
890 "end": p.meta.line + extra, "covered": p.covered,
891 })
892 })
893 })
894 .collect()
895}
896
897pub fn read_statement_spans(run: &StoredRun) -> Option<Vec<serde_json::Value>> {
899 let bytes = fs::read(run.directory.join(STATEMENT_SPANS_FILE)).ok()?;
900 serde_json::from_slice(&bytes).ok()
901}
902
903pub(crate) fn write_statement_spans(run: &StoredRun, report: &CoverageReport) -> io::Result<()> {
904 let bytes = serde_json::to_vec(&statement_spans(report)).map_err(io::Error::other)?;
905 fs::write(run.directory.join(STATEMENT_SPANS_FILE), bytes)
906}
907
908pub fn read_assessment_input(
910 run: &StoredRun,
911) -> Option<crate::assertion_coverage::AssessmentInput> {
912 let file = fs::File::open(run.directory.join(ASSESSMENT_INPUT_FILE)).ok()?;
913 serde_json::from_reader(flate2::read::GzDecoder::new(io::BufReader::new(file))).ok()
914}
915
916pub(crate) fn write_assessment_input(run: &StoredRun, report: &CoverageReport) -> io::Result<()> {
917 use std::io::Write;
918 let bytes = serde_json::to_vec(&crate::assertion_coverage::assessment_input(report))
919 .map_err(io::Error::other)?;
920 let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
921 encoder.write_all(&bytes)?;
922 fs::write(run.directory.join(ASSESSMENT_INPUT_FILE), encoder.finish()?)
923}
924
925pub(crate) fn write_query_index_from(
926 run: &StoredRun,
927 report: &CoverageReport,
928) -> Result<(), RunIndexError> {
929 let identity = query_index_identity(run)?;
930 let sections = coverage_index_sections(report)?;
931 write_query_index(§ions, &identity, &run.query_index_path)?;
932 Ok(())
933}
934
935pub fn open_or_rebuild_query_index(run: &StoredRun) -> Result<QueryIndex, RunIndexError> {
942 let identity = query_index_identity(run)?;
943 if let Ok(index) = open_validated_query_index(&run.query_index_path, &identity) {
944 return Ok(index);
945 }
946
947 let report = analyze_stored_run(run)?;
948 let sections = coverage_index_sections(&report)?;
949 if query_index_identity(run)? != identity {
950 return Err(RunIndexError::EvidenceChanged);
951 }
952 write_query_index(§ions, &identity, &run.query_index_path)?;
953 let index = open_written_query_index(&run.query_index_path, &identity)?;
954 if query_index_identity(run)? != identity {
955 return Err(RunIndexError::EvidenceChanged);
956 }
957 Ok(index)
958}
959
960#[cfg(test)]
961mod tests {
962 use std::{
963 fs,
964 time::{SystemTime, UNIX_EPOCH},
965 };
966
967 use crate::evidence_archive::{EvidenceArchiveEntry, read_archive, write_archive};
968
969 use super::*;
970
971 fn temporary_directory(label: &str) -> PathBuf {
972 let nonce = SystemTime::now()
973 .duration_since(UNIX_EPOCH)
974 .unwrap()
975 .as_nanos();
976 let path = std::env::temp_dir().join(format!(
977 "supercov-run-store-{label}-{}-{nonce}",
978 std::process::id()
979 ));
980 fs::create_dir_all(&path).unwrap();
981 path
982 }
983
984 fn digest(character: char) -> String {
985 std::iter::repeat_n(character, 64).collect()
986 }
987
988 fn integrity() -> RunIntegrity {
989 RunIntegrity {
990 schema_version: 2,
991 instrumenter_version: "2.0.0".into(),
992 git: Some(GitIntegrity {
993 revision: Some(std::iter::repeat_n('a', 40).collect()),
994 branch: None,
995 dirty: false,
996 }),
997 fingerprint: RunFingerprint {
998 algorithm: "sha256".into(),
999 source: digest('a'),
1000 tests: digest('b'),
1001 dependencies: digest('c'),
1002 configuration: digest('d'),
1003 instrumenter: digest('e'),
1004 execution: digest('f'),
1005 combined: digest('0'),
1006 source_files: 1,
1007 test_files: 1,
1008 },
1009 stale: None,
1010 stale_reasons: None,
1011 }
1012 }
1013
1014 fn create_run(root: &Path, id: &str) -> PathBuf {
1015 let directory = root.join(".supercov/runs").join(id);
1016 fs::create_dir_all(&directory).unwrap();
1017 let archive = write_archive(
1018 vec![
1019 EvidenceArchiveEntry {
1020 path: "coverage-model.json".into(),
1021 contents: br#"{"schemaVersion":1,"language":"fixture","variant":"fixture-v1","name":"Fixture model","completenessMeaning":"Fixture archive identity only.","measured":["fixture"],"notMeasured":[]}"#.to_vec(),
1022 },
1023 EvidenceArchiveEntry {
1024 path: "frontend.json".into(),
1025 contents: br#"{"protocolVersion":2,"frontendId":"fixture","frontendVersion":"fixture-v1","language":"fixture","structuralSource":"owned-probes","runners":[{"runner":"fixture","executionModel":"serial-in-process","attribution":{"run":"exact","worker":"unavailable","test":"unavailable","retry":"unavailable","phase":"unavailable","action":"unavailable","assertion":"unavailable"},"limitations":[{"id":"fixture-identities-unavailable","scopes":["worker","test","retry","phase","action","assertion"],"reason":"This store-only fixture has no execution evidence"}]}],"structuralLimitations":[]}"#.to_vec(),
1026 },
1027 EvidenceArchiveEntry {
1028 path: "manifest.json".into(),
1029 contents: b"{}".to_vec(),
1030 },
1031 ],
1032 &directory.join("evidence.raw.gz"),
1033 )
1034 .unwrap();
1035 let metadata = RunMetadata {
1036 id: id.into(),
1037 started_at: id.into(),
1038 duration_ms: 1.0,
1039 command: vec!["npm".into(), "test".into()],
1040 test_exit_code: Some(0),
1041 integrity: integrity(),
1042 raw_evidence: RawEvidenceMetadata {
1043 schema_version: archive.schema_version,
1044 format: archive.format.into(),
1045 file: archive.file.into(),
1046 files: archive.files,
1047 uncompressed_bytes: archive.uncompressed_bytes,
1048 compressed_bytes: archive.compressed_bytes,
1049 },
1050 isolated_build: Some(true),
1051 instrumented_build_cache: None,
1052 timings: None,
1053 merged: None,
1054 parents: None,
1055 source_roots: None,
1056 copy: None,
1057 merged_across: None,
1058 recorded_by: None,
1059 };
1060 fs::write(
1061 directory.join("run.json"),
1062 serde_json::to_vec_pretty(&metadata).unwrap(),
1063 )
1064 .unwrap();
1065 directory
1066 }
1067
1068 fn create_indexable_run(root: &Path) -> StoredRun {
1069 create_analyzable_test_run(root, "test-run");
1070 discover_runs(root).unwrap().runs.remove(0)
1071 }
1072
1073 fn create_indexable_python_run(root: &Path) -> StoredRun {
1074 let directory = create_analyzable_test_run(root, "python-run");
1075 let evidence_path = directory.join("evidence.raw.gz");
1076 let mut entries = read_archive(&evidence_path).unwrap();
1077 for entry in &mut entries {
1078 if entry.path.ends_with("/mcdc.json") {
1079 let mut result: serde_json::Value =
1080 serde_json::from_slice(&entry.contents).unwrap();
1081 result["provenance"]["runner"] = "pytest".into();
1082 result["provenance"]["kind"] = "unit".into();
1083 result["testFile"] = "tests/test_app.py".into();
1084 entry.contents = serde_json::to_vec(&result).unwrap();
1085 }
1086 }
1087 entries
1088 .iter_mut()
1089 .find(|entry| entry.path == "frontend.json")
1090 .unwrap()
1091 .contents = serde_json::to_vec(&serde_json::json!({
1092 "protocolVersion": 2,
1093 "frontendId": "python",
1094 "frontendVersion": "python-owned-v1",
1095 "language": "python",
1096 "structuralSource": "owned-probes",
1097 "runners": [{
1098 "runner": "pytest",
1099 "executionModel": "serial-in-process",
1100 "attribution": {
1101 "run": "exact",
1102 "worker": "unavailable",
1103 "test": "exact",
1104 "retry": "exact",
1105 "phase": "exact",
1106 "action": "exact",
1107 "assertion": "exact"
1108 },
1109 "limitations": [{
1110 "id": "test-fixture-worker-unavailable",
1111 "scopes": ["worker"],
1112 "reason": "The persisted-run fixture intentionally has no worker identity"
1113 }]
1114 }],
1115 "structuralLimitations": []
1116 }))
1117 .unwrap();
1118 entries
1119 .iter_mut()
1120 .find(|entry| entry.path == "coverage-model.json")
1121 .unwrap()
1122 .contents = serde_json::to_vec(&serde_json::json!({
1123 "schemaVersion": 1,
1124 "language": "python",
1125 "variant": "all",
1126 "name": "python-owned-control-flow",
1127 "completenessMeaning": "Every declared owned-probe obligation was observed.",
1128 "measured": ["owned statements", "owned decisions"],
1129 "notMeasured": []
1130 }))
1131 .unwrap();
1132 let archive = write_archive(entries, &evidence_path).unwrap();
1133 let metadata_path = directory.join("run.json");
1134 let mut metadata: RunMetadata =
1135 serde_json::from_slice(&fs::read(&metadata_path).unwrap()).unwrap();
1136 metadata.raw_evidence = RawEvidenceMetadata {
1137 schema_version: archive.schema_version,
1138 format: archive.format.into(),
1139 file: archive.file.into(),
1140 files: archive.files,
1141 uncompressed_bytes: archive.uncompressed_bytes,
1142 compressed_bytes: archive.compressed_bytes,
1143 };
1144 fs::write(metadata_path, serde_json::to_vec_pretty(&metadata).unwrap()).unwrap();
1145 discover_runs(root).unwrap().runs.remove(0)
1146 }
1147
1148 #[test]
1149 fn discovers_valid_runs_in_reverse_order_and_selects_exact_prefix_or_latest() {
1150 let root = temporary_directory("discovery");
1151 create_run(&root, "2026-08-24T00-00-00-000Z");
1152 create_run(&root, "2026-08-25T00-00-00-000Z");
1153 create_run(&root, "zz-custom-old-run");
1154 let custom_metadata = root.join(".supercov/runs/zz-custom-old-run/run.json");
1155 let mut custom: serde_json::Value =
1156 serde_json::from_slice(&fs::read(&custom_metadata).unwrap()).unwrap();
1157 custom["startedAt"] = "2026-08-23T00:00:00.000Z".into();
1158 fs::write(
1159 &custom_metadata,
1160 serde_json::to_vec_pretty(&custom).unwrap(),
1161 )
1162 .unwrap();
1163 let inventory = discover_runs(&root).unwrap();
1164 assert!(inventory.rejected.is_empty());
1165 assert_eq!(
1166 inventory
1167 .runs
1168 .iter()
1169 .map(|run| run.id.as_str())
1170 .collect::<Vec<_>>(),
1171 [
1172 "2026-08-25T00-00-00-000Z",
1173 "2026-08-24T00-00-00-000Z",
1174 "zz-custom-old-run"
1175 ]
1176 );
1177 assert_eq!(
1178 select_run(&inventory, None).unwrap().id,
1179 inventory.runs[0].id
1180 );
1181 assert_eq!(
1182 select_run(&inventory, Some("2026-08-24")).unwrap().id,
1183 "2026-08-24T00-00-00-000Z"
1184 );
1185 assert!(matches!(
1186 select_run(&inventory, Some("missing")),
1187 Err(RunStoreError::RunNotFound(_))
1188 ));
1189 fs::remove_dir_all(root).unwrap();
1190 }
1191
1192 #[test]
1193 fn reports_damaged_entries_without_hiding_valid_runs() {
1194 let root = temporary_directory("rejected");
1195 create_run(&root, "valid");
1196 let mismatched = create_run(&root, "mismatched");
1197 let mut metadata: serde_json::Value =
1198 serde_json::from_slice(&fs::read(mismatched.join("run.json")).unwrap()).unwrap();
1199 metadata["id"] = "different".into();
1200 fs::write(
1201 mismatched.join("run.json"),
1202 serde_json::to_vec(&metadata).unwrap(),
1203 )
1204 .unwrap();
1205 let corrupt = create_run(&root, "wrong-length");
1206 fs::write(corrupt.join("evidence.raw.gz"), b"truncated").unwrap();
1207 let schema_mismatch = create_run(&root, "schema-mismatch");
1208 let metadata_path = schema_mismatch.join("run.json");
1209 let mut metadata: serde_json::Value =
1210 serde_json::from_slice(&fs::read(&metadata_path).unwrap()).unwrap();
1211 metadata["rawEvidence"]["schemaVersion"] = 2.into();
1212 fs::write(&metadata_path, serde_json::to_vec(&metadata).unwrap()).unwrap();
1213
1214 let inventory = discover_runs(&root).unwrap();
1215 assert_eq!(inventory.runs.len(), 1);
1216 assert_eq!(inventory.runs[0].id, "valid");
1217 assert_eq!(inventory.rejected.len(), 3);
1218 assert!(inventory.rejected[0].reason.contains("metadata ID"));
1219 assert!(
1220 inventory
1221 .rejected
1222 .iter()
1223 .any(|run| run.reason.contains("raw evidence metadata"))
1224 );
1225 assert!(
1226 inventory
1227 .rejected
1228 .iter()
1229 .any(|run| run.reason.contains("raw evidence length"))
1230 );
1231 fs::remove_dir_all(root).unwrap();
1232 }
1233
1234 #[cfg(unix)]
1235 #[test]
1236 fn refuses_linked_run_directories_and_files() {
1237 use std::os::unix::fs::symlink;
1238
1239 let root = temporary_directory("links");
1240 let target = create_run(&root, "target");
1241 symlink(&target, root.join(".supercov/runs/linked-run")).unwrap();
1242 let linked_metadata = create_run(&root, "linked-metadata");
1243 fs::remove_file(linked_metadata.join("run.json")).unwrap();
1244 symlink(target.join("run.json"), linked_metadata.join("run.json")).unwrap();
1245 let linked_evidence = create_run(&root, "linked-evidence");
1246 fs::remove_file(linked_evidence.join("evidence.raw.gz")).unwrap();
1247 symlink(
1248 target.join("evidence.raw.gz"),
1249 linked_evidence.join("evidence.raw.gz"),
1250 )
1251 .unwrap();
1252
1253 let inventory = discover_runs(&root).unwrap();
1254 assert_eq!(inventory.runs.len(), 1);
1255 assert_eq!(inventory.runs[0].id, "target");
1256 assert_eq!(inventory.rejected.len(), 3);
1257 assert!(
1258 inventory
1259 .rejected
1260 .iter()
1261 .all(|rejected| rejected.reason.contains("unsafe run-store path"))
1262 );
1263 fs::remove_dir_all(root).unwrap();
1264 }
1265
1266 #[test]
1267 fn compares_integrity_in_stable_contract_order_and_binds_identity_to_evidence() {
1268 let root = temporary_directory("identity");
1269 create_run(&root, "run");
1270 let inventory = discover_runs(&root).unwrap();
1271 let run = &inventory.runs[0];
1272 let first = query_index_identity(run).unwrap();
1273 fs::write(&run.evidence_path, b"different bytes").unwrap();
1274 let second = query_index_identity(run).unwrap();
1275 assert_ne!(first.evidence_sha256, second.evidence_sha256);
1276 assert_ne!(first.evidence_bytes, second.evidence_bytes);
1277 assert_eq!(first.analysis_sha256, second.analysis_sha256);
1278 assert_eq!(env!("SUPERCOV_ENGINE_SOURCE_SHA256").len(), 64);
1279
1280 let mut current = integrity();
1281 current.schema_version += 1;
1282 current.instrumenter_version = "javascript-v2".into();
1283 current.fingerprint.source = digest('2');
1284 current.fingerprint.tests = digest('3');
1285 current.fingerprint.dependencies = digest('4');
1286 current.fingerprint.configuration = digest('5');
1287 assert_eq!(
1288 compare_run_integrity(Some(&integrity()), ¤t).reasons,
1289 [
1290 "coverage schema changed",
1291 "instrumenter contract changed",
1292 "instrumented source changed",
1293 "test files changed",
1294 "dependencies or lockfile changed",
1295 "test/build configuration changed",
1296 ]
1297 );
1298
1299 let mut rebuilt = integrity();
1304 rebuilt.fingerprint.instrumenter = digest('1');
1305 assert!(!compare_run_integrity(Some(&integrity()), &rebuilt).stale);
1306 fs::remove_dir_all(root).unwrap();
1307 }
1308
1309 #[test]
1310 fn lazily_builds_reuses_and_repairs_a_fully_authenticated_typed_index() {
1311 let root = temporary_directory("lazy-index");
1312 let run = create_indexable_run(&root);
1313 assert!(!run.query_index_path.exists());
1314
1315 {
1316 let index = open_or_rebuild_query_index(&run).unwrap();
1317 index.verify_all().unwrap();
1318 CoverageIndex::new(&index).unwrap();
1319 }
1320 let canonical = fs::read(&run.query_index_path).unwrap();
1321 assert!(canonical.len() > crate::query_index::QUERY_INDEX_HEADER_SIZE);
1322 {
1323 let index = open_or_rebuild_query_index(&run).unwrap();
1324 index.verify_all().unwrap();
1325 }
1326 assert_eq!(fs::read(&run.query_index_path).unwrap(), canonical);
1327
1328 let mut corrupt = canonical.clone();
1329 let offset = crate::query_index::QUERY_INDEX_HEADER_SIZE + 8;
1330 corrupt[offset] ^= 0xff;
1331 fs::write(&run.query_index_path, corrupt).unwrap();
1332 {
1333 let index = open_or_rebuild_query_index(&run).unwrap();
1334 index.verify_all().unwrap();
1335 }
1336 assert_eq!(fs::read(&run.query_index_path).unwrap(), canonical);
1337 fs::remove_dir_all(root).unwrap();
1338 }
1339
1340 #[test]
1341 fn an_index_opened_without_the_full_check_still_refuses_a_corrupt_page() {
1342 let root = temporary_directory("written-index");
1347 let run = create_indexable_python_run(&root);
1348 let identity = query_index_identity(&run).unwrap();
1349 drop(open_or_rebuild_query_index(&run).unwrap());
1350 let lines = QueryIndex::open(&run.query_index_path, &identity)
1351 .unwrap()
1352 .descriptor(crate::coverage_index::SECTION_LINES)
1353 .unwrap();
1354 assert!(lines.length > 0, "the fixture has line records to corrupt");
1355
1356 let mut bytes = fs::read(&run.query_index_path).unwrap();
1357 bytes[lines.offset as usize] ^= 0xff;
1358 fs::write(&run.query_index_path, bytes).unwrap();
1359
1360 let index = open_written_query_index(&run.query_index_path, &identity)
1361 .expect("opening does not hash every page");
1362 assert!(
1363 matches!(
1364 index.bytes(crate::coverage_index::SECTION_LINES, 0, 1),
1365 Err(crate::query_index::QueryIndexError::CorruptPage { .. })
1366 ),
1367 "the corrupt page is refused when it is read"
1368 );
1369 assert!(
1370 index.verify_all().is_err(),
1371 "and a full check still finds it"
1372 );
1373 fs::remove_dir_all(root).unwrap();
1374 }
1375
1376 #[test]
1377 fn indexes_the_declared_language_model() {
1378 let root = temporary_directory("v3-index");
1379 let run = create_indexable_python_run(&root);
1380 assert_eq!(
1381 run.metadata.raw_evidence.schema_version,
1382 EVIDENCE_ARCHIVE_SCHEMA_VERSION
1383 );
1384 assert_eq!(
1385 query_index_identity(&run).unwrap().archive_schema_version,
1386 EVIDENCE_ARCHIVE_SCHEMA_VERSION
1387 );
1388 let index = open_or_rebuild_query_index(&run).unwrap();
1389 index.verify_all().unwrap();
1390 let coverage_index = CoverageIndex::new(&index).unwrap();
1391 assert_eq!(
1392 coverage_index.model().unwrap().name,
1393 "python-owned-control-flow"
1394 );
1395 let summary = coverage_index
1396 .summary(crate::coverage_index::CoverageViewId::All)
1397 .unwrap();
1398 assert_eq!(summary.lines.percentage, 100.0);
1399 fs::remove_dir_all(root).unwrap();
1400 }
1401
1402 #[cfg(unix)]
1403 #[test]
1404 fn atomically_replaces_a_linked_disposable_index_without_touching_its_target() {
1405 use std::os::unix::fs::symlink;
1406
1407 let root = temporary_directory("linked-index");
1408 let run = create_indexable_run(&root);
1409 let outside = root.join("outside");
1410 fs::write(&outside, b"user data").unwrap();
1411 symlink(&outside, &run.query_index_path).unwrap();
1412
1413 let index = open_or_rebuild_query_index(&run).unwrap();
1414 index.verify_all().unwrap();
1415 assert_eq!(fs::read(outside).unwrap(), b"user data");
1416 assert!(
1417 fs::symlink_metadata(&run.query_index_path)
1418 .unwrap()
1419 .file_type()
1420 .is_file()
1421 );
1422 fs::remove_dir_all(root).unwrap();
1423 }
1424}