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