1use crate::{
3 assertion_inputs::ARCHIVE_PATH,
4 assertion_map::{self as model, *},
5 coverage_report::{
6 ArchiveReportRequest, CoverageReport, ExitCodeInput, analyze_coverage_archive,
7 },
8 evidence_archive::read_archive,
9 lifecycle::atomic_write,
10 run_store::{RunFingerprint, RunMetadata, StoredRun, discover_runs},
11};
12use serde_json::{Value, json};
13use sha2::{Digest, Sha256};
14use std::{
15 collections::{BTreeMap, BTreeSet},
16 fs,
17 path::Path,
18};
19
20pub const MAP_FILE: &str = "assertions.json";
21pub const STATE_FILE: &str = "assertions.state.json";
22const REPORT_CACHE_FILE: &str = "assertions.report.cache.json";
23pub struct RunManifest {
24 pub manifest: InputManifest,
25 pub evidence_digest: String,
26 legacy_digest: Option<String>,
27 pub statement_exclusions: Vec<Value>,
28}
29pub struct RunInputs {
30 pub inputs: Inputs,
31 pub stored: RunManifest,
32}
33impl std::ops::Deref for RunInputs {
34 type Target = RunManifest;
35 fn deref(&self) -> &RunManifest {
36 &self.stored
37 }
38}
39pub fn load_inputs(root: &Path, run: &StoredRun) -> Result<RunInputs, String> {
40 let stored = load_manifest(run)?;
41 let inputs = crate::assertion_inputs::current_sources(root, &stored.manifest)?;
42 Ok(RunInputs { inputs, stored })
43}
44pub fn load_manifest(run: &StoredRun) -> Result<RunManifest, String> {
45 load_optional_manifest(run)?.ok_or_else(|| {
46 "This older run has no assertion manifest. Run tests once with this version of Supercov"
47 .into()
48 })
49}
50fn load_optional_manifest(run: &StoredRun) -> Result<Option<RunManifest>, String> {
51 if run.metadata.merged == Some(true) {
52 return Err(
53 "Use a single run for assertion maps; merged runs have multiple input manifests".into(),
54 );
55 }
56 let bytes = fs::read(&run.evidence_path).map_err(|e| e.to_string())?;
57 let evidence_digest = format!("{:x}", Sha256::digest(&bytes));
58 let entries = read_archive(&run.evidence_path).map_err(|e| e.to_string())?;
59 let Some(input) = entries.iter().find(|e| e.path == ARCHIVE_PATH) else {
60 return Ok(None);
61 };
62 let value: Value = serde_json::from_slice(&input.contents).map_err(|e| e.to_string())?;
63 let (manifest, legacy_digest) = match value["schemaVersion"].as_u64() {
64 Some(2) => (
65 serde_json::from_value::<InputManifest>(value).map_err(|e| e.to_string())?,
66 None,
67 ),
68 Some(1) => {
69 let legacy: Inputs = serde_json::from_value(value).map_err(|e| e.to_string())?;
72 if legacy
73 .assertions
74 .iter()
75 .any(|s| s.at.offset(&legacy.files).is_none())
76 {
77 return Err("Invalid legacy assertion inputs".into());
78 }
79 (legacy.manifest(), Some(digest(&legacy)))
80 }
81 _ => return Err("Unsupported assertion input schema".into()),
82 };
83 if manifest.files.iter().any(|(p, f)| {
84 !local_path(p) || f.sha256.len() != 64 || !f.sha256.bytes().all(|b| b.is_ascii_hexdigit())
85 }) || manifest.assertions.iter().any(|s| {
86 !manifest.files.contains_key(&s.at.file)
87 || s.at.line == 0
88 || s.at.column == 0
89 || s.at.text.is_empty()
90 }) {
91 return Err("Invalid assertion input manifest".into());
92 }
93 if fs::read(&run.evidence_path).map_err(|e| e.to_string())? != bytes {
94 return Err("Run archive changed during read".into());
95 }
96 Ok(Some(RunManifest {
97 manifest,
98 evidence_digest,
99 legacy_digest,
100 statement_exclusions: entries
101 .iter()
102 .find(|e| e.path == "statement-exclusions.json")
103 .map(|entry| serde_json::from_slice(&entry.contents).map_err(|e| e.to_string()))
104 .transpose()?
105 .unwrap_or_default(),
106 }))
107}
108pub fn load(run: &StoredRun, input: &RunManifest) -> Result<(AssertionMap, State), String> {
109 let read = |file: &str| {
110 fs::read(run.directory.join(file))
111 .map_err(|e| format!("{file}: {e}; new test runs create assertion maps automatically"))
112 };
113 let map = model::parse_stored(&read(MAP_FILE)?).map_err(|e| format!("{MAP_FILE}: {e}"))?;
114 let state = model::parse_state(
115 &read(STATE_FILE)?,
116 &map,
117 &input.manifest,
118 &input.evidence_digest,
119 input.legacy_digest.as_deref(),
120 )?;
121 Ok((map, state))
122}
123fn write_json(
124 root: &Path,
125 run: &StoredRun,
126 name: &str,
127 value: &impl serde::Serialize,
128) -> Result<(), String> {
129 let mut bytes = serde_json::to_vec_pretty(value).map_err(|e| e.to_string())?;
130 bytes.push(b'\n');
131 atomic_write(root, &run.directory.join(name), &bytes).map_err(|e| e.to_string())
132}
133pub(crate) fn prepare_publication(
137 root: &Path,
138 directory: &Path,
139 metadata: &RunMetadata,
140) -> Result<(), String> {
141 if metadata.merged == Some(true) {
142 return Ok(());
143 }
144 let run = StoredRun {
145 id: metadata.id.clone(),
146 directory: directory.into(),
147 evidence_path: directory.join("evidence.raw.gz"),
148 metadata_path: directory.join("run.json"),
149 query_index_path: directory.join(crate::run_store::RUST_QUERY_INDEX_FILE),
150 metadata: metadata.clone(),
151 };
152 let Some(input) = load_optional_manifest(&run)? else {
153 return Ok(());
154 };
155 if directory.join(MAP_FILE).exists() || directory.join(STATE_FILE).exists() {
157 return Err("Refusing to replace an existing assertion map or review state".into());
158 }
159 let current = crate::assertion_inputs::current_sources(root, &input.manifest);
160 let inventory = discover_runs(root).map_err(|e| e.to_string())?;
161 let mut inheritance = Inheritance::default();
162 let mut inherited = None;
163 for previous in &inventory.runs {
164 if previous.id == run.id
165 || previous.metadata.merged == Some(true)
166 || previous.metadata.command != metadata.command
167 || (!previous.directory.join(MAP_FILE).exists()
168 && !previous.directory.join(STATE_FILE).exists())
169 {
170 continue;
171 }
172 let attempt = (|| {
173 let old = load_manifest(previous)?;
174 if old.manifest.language != input.manifest.language {
175 return Ok(None);
176 }
177 let (map, state) = load(previous, &old)?;
178 let a = &previous.metadata.integrity.fingerprint;
179 let b = &metadata.integrity.fingerprint;
180 let delta = context_delta(
181 a,
182 b,
183 &old.manifest.context_digest,
184 &input.manifest.context_digest,
185 );
186 let context_changed = delta.execution;
187 let dependencies_changed = delta.dependencies;
188 let current = match ¤t {
189 Ok(current) => current,
190 Err(reason) => {
191 let (mut next, mut next_state) =
195 seed_manifest(&input.manifest, &input.evidence_digest);
196 next.retired_assertions = map.retired_assertions;
197 for assertion in map.assertions {
198 if let Some(site) =
199 next.assertions.iter_mut().find(|a| a.at == assertion.at)
200 {
201 *site = assertion;
202 } else {
203 next.retired_assertions.push(Retired {
204 assertion,
205 reason: reason.clone(),
206 });
207 }
208 }
209 let mut reserved = next
210 .retired_assertions
211 .iter()
212 .map(|r| r.assertion.id.clone())
213 .chain(
214 next.assertions
215 .iter()
216 .filter(|a| !a.flows.is_empty())
217 .map(|a| a.id.clone()),
218 )
219 .collect::<BTreeSet<_>>();
220 for assertion in next.assertions.iter_mut().filter(|a| a.flows.is_empty()) {
221 while !reserved.insert(assertion.id.clone()) {
222 assertion.id.push('_');
223 }
224 }
225 invalidate(&mut next_state, &next, reason);
226 add_change(
227 &mut next_state,
228 None,
229 None,
230 None,
231 reason.clone(),
232 BTreeSet::new(),
233 );
234 return Ok(Some((next, next_state)));
235 }
236 };
237 model::carry(
238 &map,
239 &state,
240 &old.manifest,
241 current,
242 &input.evidence_digest,
243 context_changed,
244 )
245 .map(|(next, mut next_state)| {
246 if dependencies_changed {
247 add_change(
248 &mut next_state,
249 None,
250 Some(a.dependencies.clone()),
251 Some(b.dependencies.clone()),
252 "installed dependencies changed".into(),
253 BTreeSet::new(),
254 );
255 }
256 Some((next, next_state))
257 })
258 })();
259 match attempt {
260 Ok(Some(pair)) => {
261 inheritance.from = Some(previous.id.clone());
262 inherited = Some(pair);
263 break;
264 }
265 Ok(None) => (),
266 Err(reason) => inheritance.skipped.push(SkippedMap {
267 run: previous.id.clone(),
268 reason,
269 }),
270 }
271 }
272 let (map, mut state) =
273 inherited.unwrap_or_else(|| seed_manifest(&input.manifest, &input.evidence_digest));
274 if !inheritance.skipped.is_empty() {
277 invalidate(
278 &mut state,
279 &map,
280 "newer assertion map could not be reused; inspect inherited claims",
281 );
282 }
283 if let Err(reason) = current {
284 invalidate(&mut state, &map, &reason);
285 add_change(&mut state, None, None, None, reason, BTreeSet::new());
286 }
287 state.inheritance = Some(inheritance);
288 write_json(root, &run, MAP_FILE, &map)?;
289 write_json(root, &run, STATE_FILE, &state)?;
290 Ok(())
291}
292struct ContextDelta {
294 execution: bool,
297 dependencies: bool,
300}
301
302fn context_delta(
317 a: &RunFingerprint,
318 b: &RunFingerprint,
319 old_context: &str,
320 new_context: &str,
321) -> ContextDelta {
322 ContextDelta {
323 execution: a.configuration != b.configuration || old_context != new_context,
324 dependencies: a.dependencies != b.dependencies,
325 }
326}
327
328pub fn coverage(run: &StoredRun) -> Result<CoverageReport, String> {
329 analyze_coverage_archive(&ArchiveReportRequest {
330 archive_path: run.evidence_path.clone(),
331 run_id: run.id.clone(),
332 generated_at: run.metadata.started_at.clone(),
333 integrity: None,
334 test_exit_code: ExitCodeInput::Present(run.metadata.test_exit_code),
335 })
336 .map_err(|e| format!("{e:?}"))
337}
338fn byte_column(source: &str, line: usize, column: usize, language: &str) -> Option<usize> {
339 if line == 0 {
340 return None;
341 }
342 let line = source.lines().nth(line - 1)?;
343 if language != "javascript" {
344 return column.checked_add(1);
346 }
347 if column == 0 {
348 return None;
349 }
350 let mut units = 0;
351 for (byte, ch) in line.char_indices() {
352 if units == column - 1 {
353 return Some(byte + 1);
354 }
355 units += ch.len_utf16();
356 }
357 (units == column - 1).then_some(line.len() + 1)
358}
359fn phase_location<'a>(
360 phase: &'a crate::coverage_report::CoveragePhase,
361 inputs: &Inputs,
362) -> Option<(&'a str, usize, usize)> {
363 if phase.kind != "assertion" || phase.status.as_deref() != Some("passed") {
364 return None;
365 }
366 let source = phase
367 .operation
368 .strip_prefix("Rust assertion at ")
369 .or(phase.source.as_deref());
370 let location = source?;
371 let mut parts = location.rsplitn(3, ':');
372 let column = parts.next()?.parse::<usize>().ok()?;
373 let line = parts.next()?.parse::<usize>().ok()?;
374 let file = parts.next()?;
375 let column = byte_column(inputs.files.get(file)?, line, column, &inputs.language)?;
376 Some((file, line, column))
377}
378
379pub fn report(root: &Path, run: &StoredRun) -> Result<Value, String> {
382 report_with_detail(root, run, None)
383}
384pub fn assertion(root: &Path, run: &StoredRun, id: &str) -> Result<Value, String> {
386 report_with_detail(root, run, Some(id))
387}
388fn report_with_detail(root: &Path, run: &StoredRun, id: Option<&str>) -> Result<Value, String> {
389 let input = load_inputs(root, run)?;
390 let (map, state) = load(run, &input)?;
391 let cache_key = digest(&(
392 env!("SUPERCOV_ENGINE_SOURCE_SHA256"),
393 &run.id,
394 run.metadata.test_exit_code,
395 &map,
396 &state,
397 &input.evidence_digest,
398 ));
399 let cache_path = run.directory.join(REPORT_CACHE_FILE);
400 let mut report = read_report_cache(&cache_path, &cache_key).unwrap_or_else(|| Value::Null);
401 if report.is_null() {
402 let coverage = coverage(run)?;
403 report = assess(
404 &map,
405 &state,
406 &input.inputs,
407 &coverage,
408 run.metadata.test_exit_code == Some(0),
409 );
410 report["excludedStatements"] = json!(input.statement_exclusions);
411 report["summary"]["excludedStatements"] = json!(input.statement_exclusions.len());
412 let cached = json!({"key":cache_key,"digest":digest(&report),"report":report});
415 let _ = write_json(root, run, REPORT_CACHE_FILE, &cached);
416 }
417 if let Some(id) = id {
418 let matches = report["assertions"]
419 .as_array()
420 .unwrap()
421 .iter()
422 .filter(|a| a["id"] == id)
423 .collect::<Vec<_>>();
424 let mut row = match matches.as_slice() {
425 [row] => (*row).clone(),
426 [] => {
427 return Err(format!(
428 "Unknown assertion ID: {id}; use runs {} assertions to list IDs",
429 run.id
430 ));
431 }
432 _ => {
433 return Err(format!(
434 "Ambiguous assertion ID: {id}; repair duplicate IDs in assertions.json"
435 ));
436 }
437 };
438 if let Some(authored) = map.assertions.iter().find(|a| a.id == id) {
439 for (flow, authored) in row["flows"]
440 .as_array_mut()
441 .unwrap()
442 .iter_mut()
443 .zip(&authored.flows)
444 {
445 let assessment = flow.as_object().unwrap().clone();
446 *flow = serde_json::to_value(authored).map_err(|e| e.to_string())?;
447 flow.as_object_mut().unwrap().extend(assessment);
448 }
449 }
450 report["assertion"] = row;
451 }
452 report["inheritance"] = json!(state.inheritance);
453 report["revision"] = json!(digest(&(&map, &state, &input.evidence_digest)));
454 Ok(report)
455}
456
457fn read_report_cache(path: &Path, key: &str) -> Option<Value> {
458 if fs::metadata(path).ok()?.len() > 256 * 1024 * 1024 {
459 return None;
460 }
461 let cached: Value = serde_json::from_slice(&fs::read(path).ok()?).ok()?;
462 let report = &cached["report"];
463 (cached["key"] == key
464 && cached["digest"] == digest(report)
465 && report["summary"].is_object()
466 && report["assertions"].is_array())
467 .then(|| report.clone())
468}
469
470pub fn assess(
471 map: &AssertionMap,
472 state: &State,
473 inputs: &Inputs,
474 coverage: &CoverageReport,
475 passed: bool,
476) -> Value {
477 let manifest = inputs.manifest();
478 let validation = model::validation(map, state, inputs);
479 let errors = validation["errors"]
480 .as_array()
481 .unwrap()
482 .iter()
483 .map(|e| e.as_str().unwrap().to_owned())
484 .collect::<Vec<_>>();
485 let pending_changes = validation["changes"]
486 .as_array()
487 .unwrap()
488 .iter()
489 .filter(|c| c["current"] != true)
490 .count();
491 let view = &coverage.filters.passed;
492 let tests = view
493 .tests
494 .iter()
495 .filter(|t| t.role == "test")
496 .map(|t| (&t.id, t))
497 .collect::<BTreeMap<_, _>>();
498 let measured_statements = view
499 .points
500 .iter()
501 .filter(|p| p.measured && p.meta.kind == crate::coverage_analysis::PointKind::Statement)
502 .collect::<Vec<_>>();
503 let all_points = coverage
504 .view
505 .points
506 .iter()
507 .map(|p| (&p.meta.id, p))
508 .collect::<BTreeMap<_, _>>();
509 let all_tests = coverage
510 .view
511 .tests
512 .iter()
513 .map(|t| (&t.id, t))
514 .collect::<BTreeMap<_, _>>();
515 let mut by_file = BTreeMap::<&str, Vec<(usize, &crate::coverage_report::PointResult)>>::new();
516 let mut by_line = BTreeMap::<(String, usize), Vec<&crate::coverage_report::PointResult>>::new();
517 for point in &measured_statements {
518 by_line
519 .entry((point.meta.file.clone(), point.meta.line))
520 .or_default()
521 .push(point);
522 if let Some(text) = inputs.files.get(&point.meta.file)
523 && let Some(column) =
524 byte_column(text, point.meta.line, point.meta.column, &inputs.language)
525 {
526 let at = Anchor {
527 file: point.meta.file.clone(),
528 line: point.meta.line,
529 column,
530 text: point.meta.source.clone(),
531 };
532 if let Some(start) = at.offset(&inputs.files) {
533 by_file
534 .entry(&point.meta.file)
535 .or_default()
536 .push((start, point));
537 }
538 }
539 }
540 for points in by_file.values_mut() {
541 points.sort_by_key(|(pos, _)| *pos);
542 }
543 let mut rows = Vec::new();
544 let mut claimed_points = BTreeSet::new();
545 let mut credited_points = BTreeSet::new();
546 let mut point_flows = BTreeMap::<String, BTreeSet<String>>::new();
547 let mut draft_flows = 0;
548 let mut stale_flows = 0;
549 let mut invalid_flows = 0;
550 let mut current_flows = 0;
551 let mut credit_flows = 0;
552 let mut line_assertions = BTreeMap::<(String, usize), BTreeSet<String>>::new();
553 let identities_valid = state.inputs_digest == inputs.identity()
556 && errors.iter().all(|e| {
557 !e.contains("duplicate") && !e.contains("schema") && !e.contains("assertion ID")
558 });
559 let mut phases_by_location = BTreeMap::new();
562 for p in &view.phases {
563 if !tests.contains_key(&p.test) {
564 continue;
565 }
566 if let Some(location) = phase_location(&p.phase, inputs) {
567 phases_by_location
568 .entry(location)
569 .or_insert_with(Vec::new)
570 .push(p);
571 }
572 }
573 let mut inventory = BTreeMap::<&Anchor, BTreeSet<&str>>::new();
574 for site in &inputs.assertions {
575 inventory
576 .entry(&site.at)
577 .or_default()
578 .insert(&site.operation);
579 }
580 let mapped_anchors = map
581 .assertions
582 .iter()
583 .map(|a| &a.at)
584 .collect::<BTreeSet<_>>();
585 let mut used_ids = map
586 .assertions
587 .iter()
588 .map(|a| a.id.clone())
589 .chain(
590 map.retired_assertions
591 .iter()
592 .map(|r| r.assertion.id.clone()),
593 )
594 .collect::<BTreeSet<_>>();
595 let missing = seed(inputs, "")
596 .0
597 .assertions
598 .into_iter()
599 .filter(|a| !mapped_anchors.contains(&a.at))
600 .map(|mut a| {
601 while !used_ids.insert(a.id.clone()) {
602 a.id.push('_');
603 }
604 a
605 })
606 .collect::<Vec<_>>();
607 for (a, in_map) in map
608 .assertions
609 .iter()
610 .map(|a| (a, true))
611 .chain(missing.iter().map(|a| (a, false)))
612 {
613 let witnesses = phases_by_location
614 .get(&(a.at.file.as_str(), a.at.line, a.at.column))
615 .into_iter()
616 .flatten()
617 .filter(|p| {
620 inputs.language != "javascript"
621 || inventory
622 .get(&a.at)
623 .is_some_and(|operations| operations.contains(p.phase.operation.as_str()))
624 })
625 .map(|p| p.test.clone())
626 .collect::<BTreeSet<_>>();
627 let mut flows = Vec::new();
628 for f in &a.flows {
629 let dirty = model::reasons_for_manifest(a, f, map, state, inputs, &manifest);
630 let valid = model::validate_flow(f, &inputs.files).is_empty()
631 && a.at.offset(&inputs.files).is_some();
632 let freshness = if f.basis.is_none() {
633 draft_flows += 1;
634 "draft"
635 } else if f.basis.as_deref()
636 != Some(model::expected_basis(a, f, map, state, &manifest).as_str())
637 {
638 stale_flows += 1;
639 "stale"
640 } else {
641 "current"
642 };
643 if !valid {
644 invalid_flows += 1;
645 }
646 if dirty.is_empty() {
647 current_flows += 1;
648 }
649 let mut resolved = BTreeSet::new();
652 let mut selectors = Vec::new();
653 for selector in &f.applies_to {
654 let matches = coverage
655 .view
656 .tests
657 .iter()
658 .filter(|t| {
659 t.file.as_deref() == Some(selector.file.as_str()) && t.name == selector.name
660 })
661 .collect::<Vec<_>>();
662 let status = match matches.as_slice() {
663 [test] if witnesses.contains(&test.id) && tests.contains_key(&test.id) => {
664 resolved.insert(test.id.clone());
665 "observed"
666 }
667 [] => "missing",
668 [_] => "unobserved",
669 _ => "ambiguous",
670 };
671 selectors.push(json!({"file":selector.file,"name":selector.name,"status":status,
672 "outcomes":matches.iter().map(|t| &t.outcome).collect::<Vec<_>>(),
673 "reason":match matches.as_slice() {
674 [] => "No test execution record matches this selector. The test may be disabled or outside this run.",
675 [test] if test.outcome != "passed" => "The selected test has no passing outcome.",
676 [_] if status == "unobserved" => "The test passed but this assertion occurrence was not recorded. Its branch may not have run, or attribution may be missing.",
677 [_] => "A passing assertion occurrence matches this test.",
678 _ => "Multiple test identities match this selector."
679 }}));
680 }
681 let applicable = resolved;
682 let eligible = passed && identities_valid && dirty.is_empty() && !applicable.is_empty();
683 let mut blockers = Vec::new();
684 if !passed {
685 blockers.push("run did not pass");
686 }
687 if !identities_valid {
688 blockers.push("invalid map identities");
689 }
690 if !dirty.is_empty() {
691 blockers.push("flow requires review or reference repair");
692 }
693 if applicable.is_empty() {
694 blockers.push("no matching passing assertion occurrence for appliesTo");
695 }
696 if eligible {
697 credit_flows += 1;
698 }
699 let mut lines = BTreeSet::new();
700 let mut node_credit = Vec::new();
701 for node in &f.nodes {
702 let claimed = f.counts_as_asserted.contains(&node.id);
703 let start = node.at.offset(&inputs.files);
704 let mut matched = Vec::new();
705 if let Some(start) = start
706 && let Some(points) = by_file.get(node.at.file.as_str())
707 {
708 let first = points.partition_point(|(pos, _)| *pos < start);
709 for (_, point) in points[first..].iter().take_while(|(pos, _)| *pos == start) {
710 if point.meta.source == node.at.text {
713 matched.push(*point);
714 }
715 }
716 }
717 let matching_tests = matched
718 .iter()
719 .filter(|p| p.covered)
720 .flat_map(|p| p.tests.iter())
721 .filter(|test| applicable.contains(*test))
722 .collect::<BTreeSet<_>>();
723 let credited = claimed && eligible && !matching_tests.is_empty();
724 let mut reasons = Vec::new();
725 let mut reason = |code: &str, message: String| {
726 reasons.push(json!({"code":code,"message":message}));
727 };
728 if !claimed {
729 reason(
730 "context_only",
731 "The agent included this node as context, not in countsAsAsserted.".into(),
732 );
733 } else if credited {
734 reason("same_test_execution", "Current agent claim, passing assertion, and statement execution in the same selected test.".into());
735 } else {
736 if start.is_none() {
737 reason(
738 "invalid_source_anchor",
739 "The node's source anchor does not match the run's current source."
740 .into(),
741 );
742 } else if matched.is_empty() {
743 reason("no_measured_statement", "The node does not exactly identify a measured production statement in this run.".into());
744 }
745 if !passed {
746 reason("run_failed", "The test run did not pass.".into());
747 }
748 if !identities_valid {
749 reason("invalid_map_identity", "Map identities or managed input state are invalid; see validation errors.".into());
750 }
751 if !dirty.is_empty() {
752 reason(
753 "flow_needs_attention",
754 format!(
755 "Flow needs investigation: {}",
756 dirty.iter().cloned().collect::<Vec<_>>().join("; ")
757 ),
758 );
759 }
760 if applicable.is_empty() {
761 reason("no_passing_assertion", "No selected test has a matching passing occurrence of this assertion; see flow selectors.".into());
762 }
763 if !matched.is_empty() {
764 if !matched.iter().any(|p| p.covered) {
765 let executed = matched
766 .iter()
767 .filter_map(|p| all_points.get(&p.meta.id))
768 .filter(|p| p.covered)
769 .collect::<Vec<_>>();
770 if !executed.is_empty() {
771 let setup = executed
772 .iter()
773 .flat_map(|p| &p.tests)
774 .any(|id| all_tests.get(id).is_some_and(|t| t.role == "setup"));
775 reason(if setup {"shared_setup_execution"} else {"execution_outside_passing_tests"},
776 if setup {"Execution was recorded in a separate setup scope. Shared setup is not automatically credited to consuming tests."} else {"Execution was recorded outside passing tests (for example module initialization, background work or a failed test). It cannot establish same-test execution."}.into());
777 }
778 reason(
779 "no_passing_execution",
780 "No execution evidence from passing tests for this statement."
781 .into(),
782 );
783 } else if !applicable.is_empty() && matching_tests.is_empty() {
784 if matched
785 .iter()
786 .flat_map(|p| &p.tests)
787 .any(|id| all_tests.get(id).is_some_and(|t| t.role == "setup"))
788 {
789 reason("shared_setup_execution", "Execution was recorded in a separate setup scope. Shared setup is not automatically credited to consuming tests.".into());
790 }
791 reason("no_same_test_execution", "No execution evidence attributed to a selected passing test for this assertion.".into());
792 }
793 }
794 }
795 node_credit.push(json!({
796 "nodeId":node.id,
797 "location":{"file":node.at.file,"line":node.at.line,"column":node.at.column},
798 "status":if !claimed {"context"} else if credited {"credited"} else {"notCredited"},
799 "statementIds":matched.iter().map(|p| &p.meta.id).collect::<Vec<_>>(),
800 "matchingTests":matching_tests,
801 "reasons":reasons,
802 }));
803 for point in matched.into_iter().filter(|_| claimed) {
804 claimed_points.insert(point.meta.id.clone());
805 if eligible
806 && point.covered
807 && point.tests.iter().any(|t| applicable.contains(t))
808 {
809 credited_points.insert(point.meta.id.clone());
810 point_flows
811 .entry(point.meta.id.clone())
812 .or_default()
813 .insert(flow_key(a, f));
814 lines.insert((node.at.file.clone(), point.meta.line));
815 line_assertions
816 .entry((node.at.file.clone(), point.meta.line))
817 .or_default()
818 .insert(a.id.clone());
819 }
820 }
821 }
822 flows.push(json!({"id":f.id,"freshness":freshness,"valid":valid,"current":dirty.is_empty(),"expectedBasis":model::expected_basis(a,f,map,state,&manifest),"selectors":selectors,"questions":f.questions,"reasons":dirty,"eligible":eligible,"blockers":blockers,"matchingTests":applicable,"creditedStatementLines":lines,"nodeCredit":node_credit}));
823 }
824 let observation = if !witnesses.is_empty() {
825 "Passing assertion occurrence recorded."
826 } else if flows
827 .iter()
828 .flat_map(|f| f["selectors"].as_array().into_iter().flatten())
829 .any(|s| s["status"] == "missing")
830 {
831 "No passing occurrence. Some selected tests have no execution record (for example disabled tests or tests outside this run)."
832 } else if flows
833 .iter()
834 .flat_map(|f| f["selectors"].as_array().into_iter().flatten())
835 .any(|s| {
836 s["outcomes"]
837 .as_array()
838 .is_some_and(|outcomes| outcomes.iter().any(|o| o == "skipped"))
839 })
840 {
841 "No passing occurrence. Selected tests include skipped/TODO executions."
842 } else {
843 "No passing occurrence recorded. The assertion may be in an untaken branch or its execution attribution may be missing; inspect the selected tests."
844 };
845 rows.push(json!({"observation":observation,"id":a.id,"at":a.at,"questions":a.questions,"inMap":in_map,"observes":a.observes,"operations":inventory.get(&a.at).cloned().unwrap_or_default(),"observedPassingTests":witnesses,"flows":flows}));
846 }
847 let denominator = coverage
855 .view
856 .lines
857 .iter()
858 .filter(|l| l.measured)
859 .map(|l| (l.file.clone(), l.line))
860 .filter(|location| by_line.contains_key(location))
861 .collect::<BTreeSet<_>>();
862 let mut credited = BTreeSet::new();
863 let mut declared = BTreeSet::new();
864 for location in &denominator {
865 let statements = by_line.get(location).map(Vec::as_slice).unwrap_or(&[]);
866 if !statements.is_empty()
867 && statements
868 .iter()
869 .all(|p| claimed_points.contains(&p.meta.id))
870 {
871 declared.insert(location.clone());
872 }
873 if !statements.is_empty()
874 && statements
875 .iter()
876 .all(|p| credited_points.contains(&p.meta.id))
877 {
878 credited.insert(location.clone());
879 }
880 }
881 let missing_inventory = inputs
882 .assertions
883 .iter()
884 .filter(|s| !map.assertions.iter().any(|a| a.at == s.at))
885 .count();
886 let (status, reason) = if !passed || !identities_valid {
887 ("unavailable", "Run failed or map identities are invalid")
888 } else if pending_changes > 0 {
889 ("pending", "Source changes need impact assessment")
890 } else if measured_statements.is_empty() {
891 ("notApplicable", "No measured statements")
892 } else if credit_flows > 0 {
893 (
894 "available",
895 "Agent-assessed statements; mapping completeness is unknown",
896 )
897 } else if map.assertions.iter().all(|a| a.flows.is_empty()) {
898 ("notAssessed", "No recorded flow explanations")
899 } else {
900 (
901 "pending",
902 "No current flow with matching passing assertion evidence",
903 )
904 };
905 let assertions_without_current_explanation = rows
906 .iter()
907 .filter(|a| {
908 inventory.keys().any(|at| json!(at) == a["at"])
909 && a["observedPassingTests"]
910 .as_array()
911 .is_some_and(|v| !v.is_empty())
912 && a["flows"]
913 .as_array()
914 .is_none_or(|v| !v.iter().any(|f| f["eligible"] == true))
915 })
916 .count();
917 let total = denominator.len();
918 let statements = measured_statements.iter().map(|p| {
919 let at = inputs.files.get(&p.meta.file)
920 .and_then(|text| byte_column(text, p.meta.line, p.meta.column, &inputs.language))
921 .map(|column| Anchor { file: p.meta.file.clone(), line: p.meta.line, column, text: p.meta.source.clone() })
922 .filter(|at| at.offset(&inputs.files).is_some());
923 let all = all_points.get(&p.meta.id);
924 json!({"id":p.meta.id,"file":p.meta.file,"line":p.meta.line,"at":at,"covered":p.covered,"tests":p.tests,"declared":claimed_points.contains(&p.meta.id),"asserted":credited_points.contains(&p.meta.id),"flows":point_flows.get(&p.meta.id).cloned().unwrap_or_default(),
925 "executionEvidence":{"anyExecution":all.is_some_and(|p| p.covered),"passingTests":p.tests.iter().filter(|id| tests.contains_key(id)).collect::<Vec<_>>(),
926 "outsidePassingTests":all.into_iter().flat_map(|p| &p.tests).filter(|id| !tests.contains_key(id)).collect::<Vec<_>>()}})
927 }).collect::<Vec<_>>();
928 json!({"basis":"agent-assessed; passing assertion identity and same-test execution required; not mutation resistance",
929 "summary":{"status":status,"reason":reason,"pendingChanges":pending_changes,"metric":"measured statements","statements":{"asserted":credited_points.len(),"declared":claimed_points.len(),"total":measured_statements.len(),"percentage":if status != "available" { None } else {Some(credited_points.len() as f64 * 100.0 / measured_statements.len() as f64)}},"assertions":map.assertions.len(),"inventoryAssertions":inputs.assertions.len(),"missingInventoryAssertions":missing_inventory,
930 "assertionsWithFlows":rows.iter().filter(|a| a["flows"].as_array().is_some_and(|f| !f.is_empty())).count(),
931 "assertionsWithoutFlows":rows.iter().filter(|a| a["flows"].as_array().is_none_or(Vec::is_empty)).count(),
932 "observedAssertionsWithoutCurrentExplanation":assertions_without_current_explanation,
933 "questions":map.assertions.iter().map(|a| a.questions.len()+a.flows.iter().map(|f| f.questions.len()).sum::<usize>()).sum::<usize>(),
934 "currentFlows":current_flows,"draftFlows":draft_flows,"staleFlows":stale_flows,"invalidFlows":invalid_flows,"eligibleFlows":credit_flows,"retiredAssertions":map.retired_assertions.len(),
935 "unobservedAssertions":rows.iter().filter(|a| a["observedPassingTests"].as_array().is_none_or(Vec::is_empty)).count(),
936 "inventoryFailures":inputs.limitations.iter().filter(|s| s.starts_with("Inventory unavailable for ")).count(),
937 "unanchoredStatements":statements.iter().filter(|s| s["at"].is_null()).count(),
938 "runPassed":passed,
939 "lines":{"asserted":credited.len(),"declared":declared.len(),"total":total,"percentage":if total==0 || status != "available" {None} else {Some(credited.len() as f64 * 100.0 / total as f64)}}},
940 "assertions":rows,"statements":statements,"tests":coverage.view.tests.iter().map(|t| json!({"id":t.id,"file":t.file,"name":t.name,"role":t.role,"outcome":t.outcome,"provenance":t.provenance,
941 "hasExecutionEvidence":!t.hits.is_empty() || !t.decisions.is_empty() || !t.lines.is_empty()})).collect::<Vec<_>>(),
942 "creditedLines":credited.iter().map(|loc| json!({"file":loc.0,"line":loc.1,"assertions":line_assertions.get(loc)})).collect::<Vec<_>>(),
943 "unassertedLines":denominator.difference(&credited).map(|(f,l)| json!({"file":f,"line":l})).collect::<Vec<_>>(),
944 "changes":validation["changes"],"validationErrors":errors,"advisories":model::advisories(map),"limitations":inputs.limitations})
945}
946
947#[cfg(test)]
948mod tests {
949 use super::*;
950 fn fingerprint() -> RunFingerprint {
951 RunFingerprint {
952 algorithm: "sha256".into(),
953 source: "source".into(),
954 tests: "tests".into(),
955 dependencies: "dependencies".into(),
956 configuration: "configuration".into(),
957 instrumenter: "instrumenter".into(),
958 execution: "execution".into(),
959 combined: "combined".into(),
960 source_files: 1,
961 test_files: 1,
962 }
963 }
964
965 #[test]
966 fn upgrading_supercov_does_not_make_every_map_stale() {
967 let before = fingerprint();
972 let mut after = fingerprint();
973 after.instrumenter = "a new supercov".into();
974 let delta = context_delta(&before, &after, "context", "context");
975 assert!(!delta.execution);
976 assert!(!delta.dependencies);
977 }
978
979 #[test]
980 fn a_dependency_upgrade_is_reported_without_invalidating_anything() {
981 let before = fingerprint();
985 let mut after = fingerprint();
986 after.dependencies = "an upgraded lockfile".into();
987 let delta = context_delta(&before, &after, "context", "context");
988 assert!(delta.dependencies);
989 assert!(!delta.execution, "an upgrade must not invalidate flows");
990 }
991
992 #[test]
993 fn what_actually_executes_still_invalidates_every_flow() {
994 let before = fingerprint();
997 let mut after = fingerprint();
998 after.configuration = "a different tsconfig".into();
999 assert!(context_delta(&before, &after, "context", "context").execution);
1000 assert!(
1001 context_delta(&before, &fingerprint(), "context", "another context").execution,
1002 "declared context variables still count"
1003 );
1004 assert!(!context_delta(&before, &fingerprint(), "context", "context").execution);
1005 }
1006
1007 #[test]
1008 fn report_cache_requires_exact_revision_and_intact_payload() {
1009 let directory =
1010 std::env::temp_dir().join(format!("supercov-report-cache-{}", std::process::id()));
1011 fs::create_dir_all(&directory).unwrap();
1012 let path = directory.join(REPORT_CACHE_FILE);
1013 let report = json!({"summary":{"statements":{"asserted":4,"percentage":97.02842377260981}},"assertions":[]});
1014 let mut cache = json!({"key":"revision-one","digest":digest(&report),"report":report});
1015 fs::write(&path, serde_json::to_vec(&cache).unwrap()).unwrap();
1016 assert_eq!(read_report_cache(&path, "revision-one"), Some(report));
1017 assert!(read_report_cache(&path, "revision-two").is_none());
1018 cache["report"]["summary"]["statements"]["asserted"] = json!(100);
1019 fs::write(&path, serde_json::to_vec(&cache).unwrap()).unwrap();
1020 assert!(read_report_cache(&path, "revision-one").is_none());
1021 fs::write(&path, "interrupted write").unwrap();
1022 assert!(read_report_cache(&path, "revision-one").is_none());
1023 fs::remove_dir_all(directory).unwrap();
1024 }
1025 use crate::evidence_archive::{EvidenceArchiveEntry, write_archive};
1026
1027 #[test]
1028 fn legacy_maps_import_without_the_old_checkout_and_require_review() {
1029 let root = std::env::temp_dir().join(format!("supercov-legacy-map-{}", std::process::id()));
1030 fs::create_dir_all(&root).unwrap();
1031 let source = "import assert from 'node:assert/strict'; assert.equal(1, 1);\n";
1032 fs::write(root.join("test.js"), source).unwrap();
1033 let old =
1034 crate::assertion_inputs::capture(&root, "javascript", ["test.js".into()]).unwrap();
1035 let directory = crate::run_store::create_analyzable_test_run(&root, "legacy");
1036 let path = directory.join("evidence.raw.gz");
1037 let mut entries = read_archive(&path).unwrap();
1038 entries.push(EvidenceArchiveEntry {
1039 path: ARCHIVE_PATH.into(),
1040 contents: serde_json::to_vec(&old).unwrap(),
1041 });
1042 let archive = write_archive(entries, &path).unwrap();
1043 let metadata_path = directory.join("run.json");
1044 let mut metadata: RunMetadata =
1045 serde_json::from_slice(&fs::read(&metadata_path).unwrap()).unwrap();
1046 metadata.raw_evidence.files = archive.files;
1047 metadata.raw_evidence.compressed_bytes = archive.compressed_bytes;
1048 metadata.raw_evidence.uncompressed_bytes = archive.uncompressed_bytes;
1049 fs::write(&metadata_path, serde_json::to_vec(&metadata).unwrap()).unwrap();
1050 let run = discover_runs(&root).unwrap().runs.remove(0);
1051 let stored = load_manifest(&run).unwrap();
1052 let (map, _) = seed(&old, &stored.evidence_digest);
1053 let legacy_map = json!({"schemaVersion":1,"assertions":[{"id":map.assertions[0].id,"at":map.assertions[0].at,"analysis":"mapped","observes":[],"flows":[{
1054 "id":"constant","explanation":"The assertion checks the constant one.","appliesTo":[],"nodes":[],"edges":[],"countsAsAsserted":[],"watch":[{"kind":"span","at":map.assertions[0].at}]
1055 }]}]});
1056 let legacy_state = json!({"schemaVersion":1,"inputsDigest":digest(&old),"evidenceDigest":stored.evidence_digest,"reviews":{},"scopeReview":[]});
1057 write_json(&root, &run, MAP_FILE, &legacy_map).unwrap();
1058 write_json(&root, &run, STATE_FILE, &legacy_state).unwrap();
1059 let map = model::parse_stored(&serde_json::to_vec(&legacy_map).unwrap()).unwrap();
1060 let map_bytes = fs::read(directory.join(MAP_FILE)).unwrap();
1061 let state_bytes = fs::read(directory.join(STATE_FILE)).unwrap();
1062
1063 fs::write(root.join("test.js"), format!("\n{source}")).unwrap();
1064 assert!(load_inputs(&root, &run).is_err());
1065 let (imported, state) = load(&run, &stored).unwrap();
1066 let new =
1067 crate::assertion_inputs::capture(&root, "javascript", ["test.js".into()]).unwrap();
1068 let (next, state) =
1069 carry(&imported, &state, &stored.manifest, &new, "new-run", false).unwrap();
1070 assert_eq!(next.assertions[0].id, map.assertions[0].id);
1071 assert_eq!(
1072 next.assertions[0].flows[0].explanation,
1073 map.assertions[0].flows[0].explanation
1074 );
1075 assert!(next.assertions[0].flows[0].basis.is_none());
1076 assert!(!next.assertions[0].flows[0].questions.is_empty());
1077 assert_eq!(fs::read(directory.join(MAP_FILE)).unwrap(), map_bytes);
1078 assert_eq!(fs::read(directory.join(STATE_FILE)).unwrap(), state_bytes);
1079
1080 let next_directory = crate::run_store::create_analyzable_test_run(&root, "next");
1083 let mut entries = read_archive(&path).unwrap();
1084 entries
1085 .iter_mut()
1086 .find(|e| e.path == ARCHIVE_PATH)
1087 .unwrap()
1088 .contents = serde_json::to_vec(&old.manifest()).unwrap();
1089 let raw = write_archive(entries, &next_directory.join("evidence.raw.gz")).unwrap();
1090 let mut next_metadata = metadata.clone();
1091 next_metadata.id = "next".into();
1092 next_metadata.started_at = "next".into();
1093 next_metadata.raw_evidence.compressed_bytes = raw.compressed_bytes;
1094 next_metadata.raw_evidence.uncompressed_bytes = raw.uncompressed_bytes;
1095 fs::write(
1096 next_directory.join("run.json"),
1097 serde_json::to_vec(&next_metadata).unwrap(),
1098 )
1099 .unwrap();
1100 fs::remove_file(root.join("test.js")).unwrap();
1101 prepare_publication(&root, &next_directory, &next_metadata).unwrap();
1102 let next_run = discover_runs(&root)
1103 .unwrap()
1104 .runs
1105 .into_iter()
1106 .find(|r| r.id == "next")
1107 .unwrap();
1108 let next_manifest = load_manifest(&next_run).unwrap();
1109 let (pending, pending_state) = load(&next_run, &next_manifest).unwrap();
1110 assert_eq!(pending.assertions[0], map.assertions[0]);
1111 assert!(!pending_state.changes.is_empty());
1112 assert!(
1113 !pending_state.flows
1114 [&flow_key(&pending.assertions[0], &pending.assertions[0].flows[0])]
1115 .reasons
1116 .is_empty()
1117 );
1118 assert!(load_inputs(&root, &next_run).is_err());
1119 assert_eq!(fs::read(directory.join(MAP_FILE)).unwrap(), map_bytes);
1120 assert_eq!(fs::read(directory.join(STATE_FILE)).unwrap(), state_bytes);
1121
1122 let mut corrupt = state.clone();
1123 corrupt.evidence_digest = "wrong".into();
1124 write_json(&root, &run, STATE_FILE, &corrupt).unwrap();
1125 assert!(load(&run, &stored).is_err());
1126 fs::remove_dir_all(root).unwrap();
1127 }
1128}