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