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