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 source_units::named,
12};
13use serde_json::{Value, json};
14use sha2::{Digest, Sha256};
15use std::{
16 collections::{BTreeMap, BTreeSet},
17 fs,
18 path::Path,
19};
20
21pub const MAP_FILE: &str = "assertions.json";
22pub const STATE_FILE: &str = "assertions.state.json";
23const REPORT_CACHE_FILE: &str = "assertions.report.cache.json";
24const SUMMARY_CACHE_FILE: &str = "assertions.summary.cache.json";
26pub struct RunManifest {
27 pub manifest: InputManifest,
28 pub evidence_digest: String,
29 legacy_digest: Option<String>,
30 pub statement_exclusions: Vec<Value>,
31}
32pub struct RunInputs {
33 pub inputs: Inputs,
34 pub stored: RunManifest,
35}
36impl std::ops::Deref for RunInputs {
37 type Target = RunManifest;
38 fn deref(&self) -> &RunManifest {
39 &self.stored
40 }
41}
42pub fn load_inputs(root: &Path, run: &StoredRun) -> Result<RunInputs, String> {
43 let stored = load_manifest(run)?;
44 let inputs = crate::assertion_inputs::current_sources(root, &stored.manifest)?;
45 Ok(RunInputs { inputs, stored })
46}
47pub fn load_manifest(run: &StoredRun) -> Result<RunManifest, String> {
48 load_optional_manifest(run)?.ok_or_else(|| {
49 "This older run has no assertion manifest. Run tests once with this version of Supercov"
50 .into()
51 })
52}
53fn load_optional_manifest(run: &StoredRun) -> Result<Option<RunManifest>, String> {
54 if run.metadata.merged == Some(true) {
55 return Err(
56 "Use a single run for assertion maps; merged runs have multiple input manifests".into(),
57 );
58 }
59 let bytes = fs::read(&run.evidence_path).map_err(|e| e.to_string())?;
60 let evidence_digest = format!("{:x}", Sha256::digest(&bytes));
61 let entries = read_archive(&run.evidence_path).map_err(|e| e.to_string())?;
62 let Some(input) = entries.iter().find(|e| e.path == ARCHIVE_PATH) else {
63 return Ok(None);
64 };
65 let value: Value = serde_json::from_slice(&input.contents).map_err(|e| e.to_string())?;
66 let (manifest, legacy_digest) = match value["schemaVersion"].as_u64() {
67 Some(2) => (
68 serde_json::from_value::<InputManifest>(value).map_err(|e| e.to_string())?,
69 None,
70 ),
71 Some(1) => {
72 let legacy: Inputs = serde_json::from_value(value).map_err(|e| e.to_string())?;
75 if legacy
76 .assertions
77 .iter()
78 .any(|s| s.at.offset(&legacy.files).is_none())
79 {
80 return Err("Invalid legacy assertion inputs".into());
81 }
82 (legacy.manifest(), Some(digest(&legacy)))
83 }
84 _ => return Err("Unsupported assertion input schema".into()),
85 };
86 if manifest.files.iter().any(|(p, f)| {
87 !local_path(p) || f.sha256.len() != 64 || !f.sha256.bytes().all(|b| b.is_ascii_hexdigit())
88 }) || manifest.assertions.iter().any(|s| {
89 !manifest.files.contains_key(&s.at.file)
90 || s.at.line == 0
91 || s.at.column == 0
92 || s.at.text.is_empty()
93 }) {
94 return Err("Invalid assertion input manifest".into());
95 }
96 if fs::read(&run.evidence_path).map_err(|e| e.to_string())? != bytes {
97 return Err("Run archive changed during read".into());
98 }
99 Ok(Some(RunManifest {
100 manifest,
101 evidence_digest,
102 legacy_digest,
103 statement_exclusions: entries
104 .iter()
105 .find(|e| e.path == "statement-exclusions.json")
106 .map(|entry| serde_json::from_slice(&entry.contents).map_err(|e| e.to_string()))
107 .transpose()?
108 .unwrap_or_default(),
109 }))
110}
111pub fn load(run: &StoredRun, input: &RunManifest) -> Result<(AssertionMap, State), String> {
112 let read = |file: &str| {
113 fs::read(run.directory.join(file))
114 .map_err(|e| format!("{file}: {e}; new test runs create assertion maps automatically"))
115 };
116 let map = model::parse_stored(&read(MAP_FILE)?).map_err(|e| format!("{MAP_FILE}: {e}"))?;
117 let state = model::parse_state(
118 &read(STATE_FILE)?,
119 &map,
120 &input.manifest,
121 &input.evidence_digest,
122 input.legacy_digest.as_deref(),
123 )?;
124 Ok((map, state))
125}
126fn write_json(
127 root: &Path,
128 run: &StoredRun,
129 name: &str,
130 value: &impl serde::Serialize,
131) -> Result<(), String> {
132 let mut bytes = serde_json::to_vec_pretty(value).map_err(|e| e.to_string())?;
133 bytes.push(b'\n');
134 atomic_write(root, &run.directory.join(name), &bytes).map_err(|e| e.to_string())
135}
136pub(crate) fn prepare_publication(
140 root: &Path,
141 directory: &Path,
142 metadata: &RunMetadata,
143) -> Result<(), String> {
144 if metadata.merged == Some(true) {
145 return Ok(());
146 }
147 let run = StoredRun {
148 id: metadata.id.clone(),
149 directory: directory.into(),
150 evidence_path: directory.join("evidence.raw.gz"),
151 metadata_path: directory.join("run.json"),
152 query_index_path: directory.join(crate::run_store::RUST_QUERY_INDEX_FILE),
153 metadata: metadata.clone(),
154 };
155 let Some(input) = load_optional_manifest(&run)? else {
156 return Ok(());
157 };
158 if directory.join(MAP_FILE).exists() || directory.join(STATE_FILE).exists() {
160 return Err("Refusing to replace an existing assertion map or review state".into());
161 }
162 let current = crate::assertion_inputs::current_sources(root, &input.manifest);
163 let inventory = discover_runs(root).map_err(|e| e.to_string())?;
164 let mut inheritance = Inheritance::default();
165 let mut inherited = None;
166 for previous in &inventory.runs {
167 if previous.id == run.id
168 || previous.metadata.merged == Some(true)
169 || previous.metadata.command != metadata.command
170 || (!previous.directory.join(MAP_FILE).exists()
171 && !previous.directory.join(STATE_FILE).exists())
172 {
173 continue;
174 }
175 let attempt = (|| {
176 let old = load_manifest(previous)?;
177 if old.manifest.language != input.manifest.language {
178 return Ok(None);
179 }
180 let (map, state) = load(previous, &old)?;
181 let a = &previous.metadata.integrity.fingerprint;
182 let b = &metadata.integrity.fingerprint;
183 let delta = context_delta(
184 a,
185 b,
186 &old.manifest.context_digest,
187 &input.manifest.context_digest,
188 );
189 let context_changed = delta.execution;
190 let dependencies_changed = delta.dependencies;
191 let current = match ¤t {
192 Ok(current) => current,
193 Err(reason) => {
194 let (mut next, mut next_state) =
198 seed_manifest(&input.manifest, &input.evidence_digest);
199 next.retired_assertions = map.retired_assertions;
200 for assertion in map.assertions {
201 if let Some(site) =
202 next.assertions.iter_mut().find(|a| a.at == assertion.at)
203 {
204 *site = assertion;
205 } else {
206 next.retired_assertions.push(Retired {
207 assertion,
208 reason: reason.clone(),
209 });
210 }
211 }
212 let mut reserved = next
213 .retired_assertions
214 .iter()
215 .map(|r| r.assertion.id.clone())
216 .chain(
217 next.assertions
218 .iter()
219 .filter(|a| !a.flows.is_empty())
220 .map(|a| a.id.clone()),
221 )
222 .collect::<BTreeSet<_>>();
223 for assertion in next.assertions.iter_mut().filter(|a| a.flows.is_empty()) {
224 while !reserved.insert(assertion.id.clone()) {
225 assertion.id.push('_');
226 }
227 }
228 invalidate(&mut next_state, &next, reason);
229 add_change(
230 &mut next_state,
231 None,
232 None,
233 None,
234 reason.clone(),
235 BTreeSet::new(),
236 BTreeSet::new(),
237 );
238 return Ok(Some((next, next_state)));
239 }
240 };
241 model::carry(
242 &map,
243 &state,
244 &old.manifest,
245 current,
246 &input.evidence_digest,
247 context_changed,
248 )
249 .map(|(next, mut next_state)| {
250 if dependencies_changed {
251 add_change(
252 &mut next_state,
253 None,
254 Some(a.dependencies.clone()),
255 Some(b.dependencies.clone()),
256 "installed dependencies changed".into(),
257 BTreeSet::new(),
258 BTreeSet::new(),
259 );
260 }
261 Some((next, next_state))
262 })
263 })();
264 match attempt {
265 Ok(Some(pair)) => {
266 inheritance.from = Some(previous.id.clone());
267 inherited = Some(pair);
268 break;
269 }
270 Ok(None) => (),
271 Err(reason) => inheritance.skipped.push(SkippedMap {
272 run: previous.id.clone(),
273 reason,
274 }),
275 }
276 }
277 let (map, mut state) =
278 inherited.unwrap_or_else(|| seed_manifest(&input.manifest, &input.evidence_digest));
279 if !inheritance.skipped.is_empty() {
282 invalidate(
283 &mut state,
284 &map,
285 "newer assertion map could not be reused; inspect inherited claims",
286 );
287 }
288 state.executions = coverage(&run).ok().and_then(|report| {
292 executions(
293 &report,
294 &input.manifest,
295 current.as_ref().ok().map(|inputs| &inputs.files),
296 )
297 });
298 if let Err(reason) = current {
299 invalidate(&mut state, &map, &reason);
300 add_change(
301 &mut state,
302 None,
303 None,
304 None,
305 reason,
306 BTreeSet::new(),
307 BTreeSet::new(),
308 );
309 }
310 state.inheritance = Some(inheritance);
311 write_json(root, &run, MAP_FILE, &map)?;
312 write_json(root, &run, STATE_FILE, &state)?;
313 Ok(())
314}
315pub fn executions(
321 coverage: &CoverageReport,
322 manifest: &InputManifest,
323 sources: Option<&Files>,
324) -> Option<Executions> {
325 let mut located: std::collections::HashMap<&str, (&str, usize)> =
326 std::collections::HashMap::new();
327 let mut probed: BTreeMap<String, BTreeSet<usize>> = BTreeMap::new();
328 for point in &coverage.view.points {
329 let meta = &point.meta;
330 let Some(code) = manifest
331 .files
332 .get(&meta.file)
333 .and_then(|fingerprint| fingerprint.code.as_ref())
334 else {
335 continue;
336 };
337 let column = if manifest.language == "javascript" {
338 let source = sources?.get(&meta.file)?;
339 let Some(column) = byte_column(source, meta.line, meta.column, &manifest.language)
340 else {
341 continue;
342 };
343 column
344 } else {
345 meta.column + 1
346 };
347 let unit = code.unit_at(meta.line, column);
348 located.insert(meta.id.as_str(), (meta.file.as_str(), unit));
349 if code.units[unit].is_code() {
350 probed.entry(meta.file.clone()).or_default().insert(unit);
351 }
352 }
353 let mut covered: BTreeMap<String, BTreeSet<usize>> = BTreeMap::new();
358 for test in &coverage.view.tests {
359 for hit in &test.hits {
360 if let Some((file, unit)) = located.get(hit.as_str()) {
361 covered.entry((*file).to_owned()).or_default().insert(*unit);
362 }
363 }
364 }
365
366 let mut tests: BTreeMap<TestSelector, Execution> = BTreeMap::new();
367 for test in &coverage.view.tests {
368 let Some(file) = &test.file else {
369 continue;
370 };
371 let selector = TestSelector {
372 file: file.clone(),
373 name: test.name.clone(),
374 };
375 let record = tests.entry(selector.clone()).or_insert_with(|| Execution {
376 test: selector,
377 passed: false,
378 files: BTreeMap::new(),
379 attribution: crate::coverage_report::ATTRIBUTION_EXACT.to_owned(),
380 });
381 record.passed |= test.outcome == "passed";
382 if !crate::coverage_report::coverage_is_complete(&record.attribution) {
386 } else if !crate::coverage_report::coverage_is_complete(&test.attribution) {
388 record.attribution = test.attribution.clone();
389 }
390 for hit in &test.hits {
391 if let Some((file, unit)) = located.get(hit.as_str()) {
392 record
393 .files
394 .entry((*file).to_owned())
395 .or_default()
396 .push(*unit);
397 }
398 }
399 }
400 let mut tests = tests.into_values().collect::<Vec<_>>();
401 for record in &mut tests {
402 for units in record.files.values_mut() {
403 units.sort_unstable();
404 units.dedup();
405 }
406 }
407 Some(Executions {
408 tests,
409 probed: probed
410 .into_iter()
411 .map(|(file, units)| (file, units.into_iter().collect()))
412 .collect(),
413 covered: covered
414 .into_iter()
415 .map(|(file, units)| (file, units.into_iter().collect()))
416 .collect(),
417 })
418}
419pub fn affected_tests(root: &Path, run: &StoredRun) -> Result<Value, String> {
429 let stored = load_manifest(run)?;
430 let (_, state) = load(run, &stored)?;
431 let Some(executions) = &state.executions else {
432 return Err(
433 "This run has no per-test execution record; rerun tests with this version of Supercov"
434 .into(),
435 );
436 };
437 let root = crate::workspace::canonicalize_simplified(root).map_err(|e| e.to_string())?;
438 let manifest = &stored.manifest;
439 let now = manifest
440 .files
441 .keys()
442 .filter_map(|file| {
443 let path = root.join(file);
444 let canonical = crate::workspace::canonicalize_simplified(&path).ok()?;
445 if !canonical.starts_with(&root) || !canonical.is_file() {
446 return None;
447 }
448 let text = fs::read_to_string(canonical).ok()?;
449 Some((file.clone(), FileFingerprint::read(file, &text)))
450 })
451 .collect::<FileManifest>();
452 let changes = manifest
453 .files
454 .keys()
455 .filter_map(|file| {
456 model::file_change(
457 manifest.files.get(file),
458 now.get(file),
459 executions.probed.get(file).map(Vec::as_slice),
460 )
461 .map(|change| (file.as_str(), change))
462 })
463 .collect::<BTreeMap<_, _>>();
464 let files = changes
465 .iter()
466 .filter_map(|(file, change)| {
467 let (kind, detail) = match change {
468 FileChange::Same | FileChange::Added => return None,
469 FileChange::CommentsOnly => ("formatting", None),
470 FileChange::Removed => ("removed", None),
471 FileChange::Bytes => ("changed", None),
472 FileChange::Code {
473 before,
474 after,
475 diff,
476 narrow,
477 } => (
478 if *narrow { "bodies" } else { "declarations" },
479 Some(model::describe(before, after, diff)),
480 ),
481 };
482 Some(json!({"file":file,"change":kind,"detail":detail}))
483 })
484 .collect::<Vec<_>>();
485 let beyond_the_run = changes
499 .iter()
500 .filter_map(|(file, change)| match change {
501 FileChange::Same | FileChange::CommentsOnly | FileChange::Added => None,
502 FileChange::Removed => Some(format!("{file} removed (the run covered code in it)")),
503 FileChange::Bytes => Some(format!("{file} changed (the run covered code in it)")),
504 FileChange::Code { before, diff, .. } => {
505 let code = manifest.files.get(*file).and_then(|f| f.code.as_ref());
510 let reached = executions
511 .covered
512 .get(*file)
513 .map(Vec::as_slice)
514 .unwrap_or_default()
515 .iter()
516 .flat_map(|unit| match code {
517 Some(code) if *unit < code.units.len() => {
518 code.ancestors(*unit).collect::<Vec<_>>()
519 }
520 _ => vec![*unit],
521 })
522 .collect::<BTreeSet<_>>();
523 let hit = diff
524 .changed
525 .iter()
526 .filter(|unit| reached.contains(unit))
527 .filter_map(|unit| before.units.get(*unit))
528 .collect::<Vec<_>>();
529 (!hit.is_empty())
530 .then(|| format!("{file}: {} changed (the run covered it)", named(hit)))
531 }
532 })
533 .collect::<Vec<_>>();
534
535 let mut affected = Vec::new();
536 let mut undetermined = Vec::new();
537 let mut unaffected = Vec::new();
538 for record in &executions.tests {
539 let mut reasons = Vec::new();
540 if !record.passed {
541 reasons.push("did not pass in the run".to_owned());
542 }
543 match changes.get(record.test.file.as_str()) {
544 None | Some(FileChange::Same | FileChange::CommentsOnly | FileChange::Added) => {}
545 Some(FileChange::Removed) => reasons.push("test file removed".to_owned()),
546 Some(FileChange::Bytes) => reasons.push("test file changed".to_owned()),
547 Some(FileChange::Code {
548 before,
549 after,
550 diff,
551 ..
552 }) => reasons.push(format!(
553 "test file changed: {}",
554 model::describe(before, after, diff)
555 )),
556 }
557 for (file, units) in &record.files {
558 let code = manifest.files.get(file).and_then(|f| f.code.as_ref());
559 let ran = units
560 .iter()
561 .flat_map(|unit| match code {
562 Some(code) if *unit < code.units.len() => {
563 code.ancestors(*unit).collect::<Vec<_>>()
564 }
565 _ => vec![*unit],
566 })
567 .collect::<BTreeSet<_>>();
568 match changes.get(file.as_str()) {
569 None | Some(FileChange::Same | FileChange::CommentsOnly | FileChange::Added) => {}
570 Some(FileChange::Removed) => {
571 reasons.push(format!("{file} removed (this test ran code in it)"));
572 }
573 Some(FileChange::Bytes) => {
574 reasons.push(format!("{file} changed (this test ran code in it)"));
575 }
576 Some(FileChange::Code {
577 before,
578 after,
579 diff,
580 narrow,
581 }) => {
582 if *narrow {
583 let hit = diff
584 .changed
585 .iter()
586 .filter(|i| ran.contains(i))
587 .map(|i| &before.units[*i])
588 .collect::<Vec<_>>();
589 if !hit.is_empty() {
590 reasons
591 .push(format!("{file}: {} changed (this test ran it)", named(hit)));
592 }
593 } else {
594 reasons.push(format!(
595 "{file}: {} changed (this test ran code in this file)",
596 model::describe(before, after, diff)
597 ));
598 }
599 }
600 }
601 }
602 if !crate::coverage_report::coverage_is_complete(&record.attribution) {
603 let proven = !reasons.is_empty();
607 if !proven {
611 reasons.extend(beyond_the_run.iter().cloned());
612 reasons.sort();
613 reasons.dedup();
614 }
615 let entry = json!({
616 "file": record.test.file,
617 "name": record.test.name,
618 "reasons": reasons,
619 "attribution": record.attribution,
620 });
621 if proven {
622 affected.push(entry);
623 } else if reasons.is_empty() {
624 unaffected.push(entry);
625 } else {
626 undetermined.push(entry);
627 }
628 continue;
629 }
630 let entry = json!({"file":record.test.file,"name":record.test.name,"reasons":reasons});
631 if reasons.is_empty() {
632 unaffected.push(entry);
633 } else {
634 affected.push(entry);
635 }
636 }
637 Ok(json!({
638 "run": run.id,
639 "affected": affected,
640 "undetermined": undetermined,
647 "unaffected": unaffected,
648 "changedFiles": files,
649 "summary": {
650 "tests": executions.tests.len(),
651 "affected": affected.len(),
652 "undetermined": undetermined.len(),
653 "unaffected": unaffected.len(),
654 "changedFiles": files.len(),
655 },
656 "meaning": "Tests whose recorded execution a change since the run could have reached, and those whose execution nothing recorded: a test that ran alongside others has no coverage of its own, so it is undetermined whenever a change reaches anything the run covered. Run both. A file the run never captured, a dependency or a configuration change is not seen here; see workingTree."
657 }))
658}
659struct ContextDelta {
661 execution: bool,
664 dependencies: bool,
667}
668
669fn context_delta(
684 a: &RunFingerprint,
685 b: &RunFingerprint,
686 old_context: &str,
687 new_context: &str,
688) -> ContextDelta {
689 ContextDelta {
690 execution: a.configuration != b.configuration || old_context != new_context,
691 dependencies: a.dependencies != b.dependencies,
692 }
693}
694
695pub fn coverage(run: &StoredRun) -> Result<CoverageReport, String> {
696 analyze_coverage_archive(&ArchiveReportRequest {
697 archive_path: run.evidence_path.clone(),
698 run_id: run.id.clone(),
699 generated_at: run.metadata.started_at.clone(),
700 integrity: None,
701 test_exit_code: ExitCodeInput::Present(run.metadata.test_exit_code),
702 })
703 .map_err(|e| format!("{e:?}"))
704}
705fn byte_column(source: &str, line: usize, column: usize, language: &str) -> Option<usize> {
706 byte_column_in(
707 source,
708 &crate::assertion_map::line_starts(source),
709 line,
710 column,
711 language,
712 )
713}
714
715fn byte_column_in(
718 source: &str,
719 starts: &[usize],
720 line: usize,
721 column: usize,
722 language: &str,
723) -> Option<usize> {
724 if line == 0 {
725 return None;
726 }
727 let line = crate::assertion_map::line_text(source, starts, line)?;
728 if language != "javascript" {
729 return column.checked_add(1);
731 }
732 if column == 0 {
733 return None;
734 }
735 let mut units = 0;
736 for (byte, ch) in line.char_indices() {
737 if units == column - 1 {
738 return Some(byte + 1);
739 }
740 units += ch.len_utf16();
741 }
742 (units == column - 1).then_some(line.len() + 1)
743}
744fn phase_location<'a>(
745 phase: &'a crate::coverage_report::CoveragePhase,
746 inputs: &Inputs,
747) -> Option<(&'a str, usize, usize)> {
748 if phase.kind != "assertion" || phase.status.as_deref() != Some("passed") {
749 return None;
750 }
751 let source = phase
752 .operation
753 .strip_prefix("Rust assertion at ")
754 .or(phase.source.as_deref());
755 let location = source?;
756 let mut parts = location.rsplitn(3, ':');
757 let column = parts.next()?.parse::<usize>().ok()?;
758 let line = parts.next()?.parse::<usize>().ok()?;
759 let file = parts.next()?;
760 let column = byte_column(inputs.files.get(file)?, line, column, &inputs.language)?;
761 Some((file, line, column))
762}
763
764pub fn report(root: &Path, run: &StoredRun) -> Result<Value, String> {
767 report_with_detail(root, run, None, true)
768}
769pub fn report_summary(root: &Path, run: &StoredRun) -> Result<Value, String> {
772 report_with_detail(root, run, None, false)
773}
774pub fn assertion(root: &Path, run: &StoredRun, id: &str) -> Result<Value, String> {
776 report_with_detail(root, run, Some(id), true)
777}
778fn report_with_detail(
779 root: &Path,
780 run: &StoredRun,
781 id: Option<&str>,
782 detail: bool,
783) -> Result<Value, String> {
784 let input = load_inputs(root, run)?;
785 let (map, state) = load(run, &input)?;
786 let cache_key = digest(&(
787 env!("SUPERCOV_ENGINE_SOURCE_SHA256"),
788 &run.id,
789 run.metadata.test_exit_code,
790 &map,
791 &state,
792 &input.evidence_digest,
793 ));
794 let cache_file = if detail {
795 REPORT_CACHE_FILE
796 } else {
797 SUMMARY_CACHE_FILE
798 };
799 let cache_path = run.directory.join(cache_file);
800 let mut report = read_report_cache(&cache_path, &cache_key).unwrap_or_else(|| Value::Null);
801 if report.is_null() {
802 let coverage = coverage(run)?;
803 report = assess_with(
804 &map,
805 &state,
806 &input.inputs,
807 &coverage,
808 run.metadata.test_exit_code == Some(0),
809 detail,
810 );
811 if detail {
812 report["excludedStatements"] = json!(input.statement_exclusions);
813 }
814 report["summary"]["excludedStatements"] = json!(input.statement_exclusions.len());
815 let cached = json!({"key":cache_key,"digest":digest(&report),"report":report});
818 let _ = write_json(root, run, cache_file, &cached);
819 }
820 if let Some(id) = id {
821 let matches = report["assertions"]
822 .as_array()
823 .unwrap()
824 .iter()
825 .filter(|a| a["id"] == id)
826 .collect::<Vec<_>>();
827 let mut row = match matches.as_slice() {
828 [row] => (*row).clone(),
829 [] => {
830 return Err(format!(
831 "Unknown assertion ID: {id}; use runs {} assertions to list IDs",
832 run.id
833 ));
834 }
835 _ => {
836 return Err(format!(
837 "Ambiguous assertion ID: {id}; repair duplicate IDs in assertions.json"
838 ));
839 }
840 };
841 if let Some(authored) = map.assertions.iter().find(|a| a.id == id) {
842 for (flow, authored) in row["flows"]
843 .as_array_mut()
844 .unwrap()
845 .iter_mut()
846 .zip(&authored.flows)
847 {
848 let assessment = flow.as_object().unwrap().clone();
849 *flow = serde_json::to_value(authored).map_err(|e| e.to_string())?;
850 flow.as_object_mut().unwrap().extend(assessment);
851 }
852 }
853 report["assertion"] = row;
854 }
855 report["inheritance"] = json!(state.inheritance);
856 report["revision"] = json!(digest(&(&map, &state, &input.evidence_digest)));
857 Ok(report)
858}
859
860fn read_report_cache(path: &Path, key: &str) -> Option<Value> {
861 if fs::metadata(path).ok()?.len() > 256 * 1024 * 1024 {
862 return None;
863 }
864 let cached: Value = serde_json::from_slice(&fs::read(path).ok()?).ok()?;
865 let report = &cached["report"];
866 (cached["key"] == key
867 && cached["digest"] == digest(report)
868 && report["summary"].is_object()
869 && report["assertions"].is_array())
870 .then(|| report.clone())
871}
872
873pub fn assess(
874 map: &AssertionMap,
875 state: &State,
876 inputs: &Inputs,
877 coverage: &CoverageReport,
878 passed: bool,
879) -> Value {
880 assess_with(map, state, inputs, coverage, passed, true)
881}
882
883pub fn assess_summary(
890 map: &AssertionMap,
891 state: &State,
892 inputs: &Inputs,
893 coverage: &CoverageReport,
894 passed: bool,
895) -> Value {
896 assess_with(map, state, inputs, coverage, passed, false)
897}
898
899fn assess_with(
900 map: &AssertionMap,
901 state: &State,
902 inputs: &Inputs,
903 coverage: &CoverageReport,
904 passed: bool,
905 detail: bool,
906) -> Value {
907 let manifest = inputs.manifest();
908 let lines = crate::assertion_map::LineIndex::new(&inputs.files);
911 let ledger = model::Ledger::new(map, state, &manifest);
912 let validation = model::validation(map, state, inputs);
913 let errors = validation["errors"]
914 .as_array()
915 .unwrap()
916 .iter()
917 .map(|e| e.as_str().unwrap().to_owned())
918 .collect::<Vec<_>>();
919 let pending_changes = validation["changes"]
920 .as_array()
921 .unwrap()
922 .iter()
923 .filter(|c| c["current"] != true)
924 .count();
925 let view = &coverage.filters.passed;
926 let tests = view
927 .tests
928 .iter()
929 .filter(|t| t.role == "test")
930 .map(|t| (&t.id, t))
931 .collect::<BTreeMap<_, _>>();
932 let measured_statements = view
933 .points
934 .iter()
935 .filter(|p| p.measured && p.meta.kind == crate::coverage_analysis::PointKind::Statement)
936 .collect::<Vec<_>>();
937 let all_points = coverage
938 .view
939 .points
940 .iter()
941 .map(|p| (&p.meta.id, p))
942 .collect::<BTreeMap<_, _>>();
943 let all_tests = coverage
944 .view
945 .tests
946 .iter()
947 .map(|t| (&t.id, t))
948 .collect::<BTreeMap<_, _>>();
949 let mut by_file = BTreeMap::<&str, Vec<(usize, &crate::coverage_report::PointResult)>>::new();
950 let mut by_line = BTreeMap::<(String, usize), Vec<&crate::coverage_report::PointResult>>::new();
951 for point in &measured_statements {
952 by_line
953 .entry((point.meta.file.clone(), point.meta.line))
954 .or_default()
955 .push(point);
956 if let Some((text, starts)) = lines.get(&point.meta.file)
957 && let Some(column) = byte_column_in(
958 text,
959 starts,
960 point.meta.line,
961 point.meta.column,
962 &inputs.language,
963 )
964 {
965 let at = Anchor {
966 file: point.meta.file.clone(),
967 line: point.meta.line,
968 column,
969 text: point.meta.source.clone(),
970 };
971 if let Some(start) = at.offset_in(text, starts) {
972 by_file
973 .entry(&point.meta.file)
974 .or_default()
975 .push((start, point));
976 }
977 }
978 }
979 for points in by_file.values_mut() {
980 points.sort_by_key(|(pos, _)| *pos);
981 }
982 let mut rows = Vec::new();
983 let mut claimed_points = BTreeSet::new();
984 let mut credited_points = BTreeSet::new();
985 let mut point_flows = BTreeMap::<String, BTreeSet<String>>::new();
986 let mut draft_flows = 0;
987 let mut stale_flows = 0;
988 let mut invalid_flows = 0;
989 let mut current_flows = 0;
990 let mut credit_flows = 0;
991 let mut line_assertions = BTreeMap::<(String, usize), BTreeSet<String>>::new();
992 let identities_valid = state.inputs_digest == inputs.identity()
995 && errors.iter().all(|e| {
996 !e.contains("duplicate") && !e.contains("schema") && !e.contains("assertion ID")
997 });
998 let mut phases_by_location = BTreeMap::new();
1001 for p in &view.phases {
1002 if !tests.contains_key(&p.test) {
1003 continue;
1004 }
1005 if let Some(location) = phase_location(&p.phase, inputs) {
1006 phases_by_location
1007 .entry(location)
1008 .or_insert_with(Vec::new)
1009 .push(p);
1010 }
1011 }
1012 let mut inventory = BTreeMap::<&Anchor, BTreeSet<&str>>::new();
1013 for site in &inputs.assertions {
1014 inventory
1015 .entry(&site.at)
1016 .or_default()
1017 .insert(&site.operation);
1018 }
1019 let mapped_anchors = map
1020 .assertions
1021 .iter()
1022 .map(|a| &a.at)
1023 .collect::<BTreeSet<_>>();
1024 let mut used_ids = map
1025 .assertions
1026 .iter()
1027 .map(|a| a.id.clone())
1028 .chain(
1029 map.retired_assertions
1030 .iter()
1031 .map(|r| r.assertion.id.clone()),
1032 )
1033 .collect::<BTreeSet<_>>();
1034 let missing = seed(inputs, "")
1035 .0
1036 .assertions
1037 .into_iter()
1038 .filter(|a| !mapped_anchors.contains(&a.at))
1039 .map(|mut a| {
1040 while !used_ids.insert(a.id.clone()) {
1041 a.id.push('_');
1042 }
1043 a
1044 })
1045 .collect::<Vec<_>>();
1046 for (a, in_map) in map
1047 .assertions
1048 .iter()
1049 .map(|a| (a, true))
1050 .chain(missing.iter().map(|a| (a, false)))
1051 {
1052 let witnesses = phases_by_location
1053 .get(&(a.at.file.as_str(), a.at.line, a.at.column))
1054 .into_iter()
1055 .flatten()
1056 .filter(|p| {
1059 inputs.language != "javascript"
1060 || inventory
1061 .get(&a.at)
1062 .is_some_and(|operations| operations.contains(p.phase.operation.as_str()))
1063 })
1064 .map(|p| p.test.clone())
1065 .collect::<BTreeSet<_>>();
1066 let mut flows = Vec::new();
1067 for f in &a.flows {
1068 let dirty = model::reasons_with(a, f, state, inputs, &manifest, &ledger);
1069 let valid = model::validate_flow(f, &inputs.files).is_empty()
1070 && a.at.offset(&inputs.files).is_some();
1071 let freshness = if f.basis.is_none() {
1072 draft_flows += 1;
1073 "draft"
1074 } else if f.basis.as_deref()
1075 != Some(model::expected_basis_with(a, f, state, &manifest, &ledger).as_str())
1076 {
1077 stale_flows += 1;
1078 "stale"
1079 } else {
1080 "current"
1081 };
1082 if !valid {
1083 invalid_flows += 1;
1084 }
1085 if dirty.is_empty() {
1086 current_flows += 1;
1087 }
1088 let mut resolved = BTreeSet::new();
1091 let mut selectors = Vec::new();
1092 for selector in &f.applies_to {
1093 let matches = coverage
1094 .view
1095 .tests
1096 .iter()
1097 .filter(|t| {
1098 t.file.as_deref() == Some(selector.file.as_str()) && t.name == selector.name
1099 })
1100 .collect::<Vec<_>>();
1101 let status = match matches.as_slice() {
1102 [test] if witnesses.contains(&test.id) && tests.contains_key(&test.id) => {
1103 resolved.insert(test.id.clone());
1104 "observed"
1105 }
1106 [] => "missing",
1107 [_] => "unobserved",
1108 _ => "ambiguous",
1109 };
1110 selectors.push(json!({"file":selector.file,"name":selector.name,"status":status,
1111 "outcomes":matches.iter().map(|t| &t.outcome).collect::<Vec<_>>(),
1112 "reason":match matches.as_slice() {
1113 [] => "No test execution record matches this selector. The test may be disabled or outside this run.",
1114 [test] if test.outcome != "passed" => "The selected test has no passing outcome.",
1115 [_] if status == "unobserved" => "The test passed but this assertion occurrence was not recorded. Its branch may not have run, or attribution may be missing.",
1116 [_] => "A passing assertion occurrence matches this test.",
1117 _ => "Multiple test identities match this selector."
1118 }}));
1119 }
1120 let applicable = resolved;
1121 let eligible = passed && identities_valid && dirty.is_empty() && !applicable.is_empty();
1122 let mut blockers = Vec::new();
1123 if !passed {
1124 blockers.push("run did not pass");
1125 }
1126 if !identities_valid {
1127 blockers.push("invalid map identities");
1128 }
1129 if !dirty.is_empty() {
1130 blockers.push("flow requires review or reference repair");
1131 }
1132 if applicable.is_empty() {
1133 blockers.push("no matching passing assertion occurrence for appliesTo");
1134 }
1135 if eligible {
1136 credit_flows += 1;
1137 }
1138 let mut lines = BTreeSet::new();
1139 let mut node_credit = Vec::new();
1140 for node in &f.nodes {
1141 let claimed = f.counts_as_asserted.contains(&node.id);
1142 let start = node.at.offset(&inputs.files);
1143 let mut matched = Vec::new();
1144 if let Some(start) = start
1145 && let Some(points) = by_file.get(node.at.file.as_str())
1146 {
1147 let first = points.partition_point(|(pos, _)| *pos < start);
1148 for (_, point) in points[first..].iter().take_while(|(pos, _)| *pos == start) {
1149 if point.meta.source == node.at.text {
1152 matched.push(*point);
1153 }
1154 }
1155 }
1156 let matching_tests = matched
1157 .iter()
1158 .filter(|p| p.covered)
1159 .flat_map(|p| p.tests.iter())
1160 .filter(|test| applicable.contains(*test))
1161 .collect::<BTreeSet<_>>();
1162 let credited = claimed && eligible && !matching_tests.is_empty();
1163 let mut reasons = Vec::new();
1164 let mut reason = |code: &str, message: String| {
1165 reasons.push(json!({"code":code,"message":message}));
1166 };
1167 if !claimed {
1168 reason(
1169 "context_only",
1170 "The agent included this node as context, not in countsAsAsserted.".into(),
1171 );
1172 } else if credited {
1173 reason("same_test_execution", "Current agent claim, passing assertion, and statement execution in the same selected test.".into());
1174 } else {
1175 if start.is_none() {
1176 reason(
1177 "invalid_source_anchor",
1178 "The node's source anchor does not match the run's current source."
1179 .into(),
1180 );
1181 } else if matched.is_empty() {
1182 reason("no_measured_statement", "The node does not exactly identify a measured production statement in this run.".into());
1183 }
1184 if !passed {
1185 reason("run_failed", "The test run did not pass.".into());
1186 }
1187 if !identities_valid {
1188 reason("invalid_map_identity", "Map identities or managed input state are invalid; see validation errors.".into());
1189 }
1190 if !dirty.is_empty() {
1191 reason(
1192 "flow_needs_attention",
1193 format!(
1194 "Flow needs investigation: {}",
1195 dirty.iter().cloned().collect::<Vec<_>>().join("; ")
1196 ),
1197 );
1198 }
1199 if applicable.is_empty() {
1200 reason("no_passing_assertion", "No selected test has a matching passing occurrence of this assertion; see flow selectors.".into());
1201 }
1202 if !matched.is_empty() {
1203 if !matched.iter().any(|p| p.covered) {
1204 let executed = matched
1205 .iter()
1206 .filter_map(|p| all_points.get(&p.meta.id))
1207 .filter(|p| p.covered)
1208 .collect::<Vec<_>>();
1209 if !executed.is_empty() {
1210 let setup = executed
1211 .iter()
1212 .flat_map(|p| &p.tests)
1213 .any(|id| all_tests.get(id).is_some_and(|t| t.role == "setup"));
1214 reason(if setup {"shared_setup_execution"} else {"execution_outside_passing_tests"},
1215 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());
1216 }
1217 reason(
1218 "no_passing_execution",
1219 "No execution evidence from passing tests for this statement."
1220 .into(),
1221 );
1222 } else if !applicable.is_empty() && matching_tests.is_empty() {
1223 if matched
1224 .iter()
1225 .flat_map(|p| &p.tests)
1226 .any(|id| all_tests.get(id).is_some_and(|t| t.role == "setup"))
1227 {
1228 reason("shared_setup_execution", "Execution was recorded in a separate setup scope. Shared setup is not automatically credited to consuming tests.".into());
1229 }
1230 reason("no_same_test_execution", "No execution evidence attributed to a selected passing test for this assertion.".into());
1231 }
1232 }
1233 }
1234 node_credit.push(json!({
1235 "nodeId":node.id,
1236 "location":{"file":node.at.file,"line":node.at.line,"column":node.at.column},
1237 "status":if !claimed {"context"} else if credited {"credited"} else {"notCredited"},
1238 "statementIds":matched.iter().map(|p| &p.meta.id).collect::<Vec<_>>(),
1239 "matchingTests":matching_tests,
1240 "reasons":reasons,
1241 }));
1242 for point in matched.into_iter().filter(|_| claimed) {
1243 claimed_points.insert(point.meta.id.clone());
1244 if eligible
1245 && point.covered
1246 && point.tests.iter().any(|t| applicable.contains(t))
1247 {
1248 credited_points.insert(point.meta.id.clone());
1249 point_flows
1250 .entry(point.meta.id.clone())
1251 .or_default()
1252 .insert(flow_key(a, f));
1253 lines.insert((node.at.file.clone(), point.meta.line));
1254 line_assertions
1255 .entry((node.at.file.clone(), point.meta.line))
1256 .or_default()
1257 .insert(a.id.clone());
1258 }
1259 }
1260 }
1261 let notices = state
1262 .flows
1263 .get(&flow_key(a, f))
1264 .map(|s| s.notices.clone())
1265 .unwrap_or_default();
1266 let exposed_to = state
1267 .changes
1268 .iter()
1269 .filter(|c| c.exposed.contains(&flow_key(a, f)))
1270 .map(|c| c.id.clone())
1271 .collect::<Vec<_>>();
1272 flows.push(json!({"id":f.id,"freshness":freshness,"valid":valid,"current":dirty.is_empty(),"expectedBasis":model::expected_basis_with(a,f,state,&manifest,&ledger),"selectors":selectors,"questions":f.questions,"reasons":dirty,"notices":notices,"exposedTo":exposed_to,"eligible":eligible,"blockers":blockers,"matchingTests":applicable,"creditedStatementLines":lines,"nodeCredit":node_credit}));
1273 }
1274 let observation = if !witnesses.is_empty() {
1275 "Passing assertion occurrence recorded."
1276 } else if flows
1277 .iter()
1278 .flat_map(|f| f["selectors"].as_array().into_iter().flatten())
1279 .any(|s| s["status"] == "missing")
1280 {
1281 "No passing occurrence. Some selected tests have no execution record (for example disabled tests or tests outside this run)."
1282 } else if flows
1283 .iter()
1284 .flat_map(|f| f["selectors"].as_array().into_iter().flatten())
1285 .any(|s| {
1286 s["outcomes"]
1287 .as_array()
1288 .is_some_and(|outcomes| outcomes.iter().any(|o| o == "skipped"))
1289 })
1290 {
1291 "No passing occurrence. Selected tests include skipped/TODO executions."
1292 } else {
1293 "No passing occurrence recorded. The assertion may be in an untaken branch or its execution attribution may be missing; inspect the selected tests."
1294 };
1295 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}));
1296 }
1297 let denominator = coverage
1305 .view
1306 .lines
1307 .iter()
1308 .filter(|l| l.measured)
1309 .map(|l| (l.file.clone(), l.line))
1310 .filter(|location| by_line.contains_key(location))
1311 .collect::<BTreeSet<_>>();
1312 let mut credited = BTreeSet::new();
1313 let mut declared = BTreeSet::new();
1314 for location in &denominator {
1315 let statements = by_line.get(location).map(Vec::as_slice).unwrap_or(&[]);
1316 if !statements.is_empty()
1317 && statements
1318 .iter()
1319 .all(|p| claimed_points.contains(&p.meta.id))
1320 {
1321 declared.insert(location.clone());
1322 }
1323 if !statements.is_empty()
1324 && statements
1325 .iter()
1326 .all(|p| credited_points.contains(&p.meta.id))
1327 {
1328 credited.insert(location.clone());
1329 }
1330 }
1331 let missing_inventory = inputs
1332 .assertions
1333 .iter()
1334 .filter(|s| !map.assertions.iter().any(|a| a.at == s.at))
1335 .count();
1336 let (status, reason) = if !passed || !identities_valid {
1337 ("unavailable", "Run failed or map identities are invalid")
1338 } else if pending_changes > 0 {
1339 ("pending", "Source changes need impact assessment")
1340 } else if measured_statements.is_empty() {
1341 ("notApplicable", "No measured statements")
1342 } else if credit_flows > 0 {
1343 (
1344 "available",
1345 "Agent-assessed statements; mapping completeness is unknown",
1346 )
1347 } else if map.assertions.iter().all(|a| a.flows.is_empty()) {
1348 ("notAssessed", "No recorded flow explanations")
1349 } else {
1350 (
1351 "pending",
1352 "No current flow with matching passing assertion evidence",
1353 )
1354 };
1355 let assertions_without_current_explanation = rows
1356 .iter()
1357 .filter(|a| {
1358 inventory.keys().any(|at| json!(at) == a["at"])
1359 && a["observedPassingTests"]
1360 .as_array()
1361 .is_some_and(|v| !v.is_empty())
1362 && a["flows"]
1363 .as_array()
1364 .is_none_or(|v| !v.iter().any(|f| f["eligible"] == true))
1365 })
1366 .count();
1367 let total = denominator.len();
1368 let located = measured_statements
1372 .iter()
1373 .map(|p| {
1374 let (text, starts) = lines.get(&p.meta.file)?;
1375 let column =
1376 byte_column_in(text, starts, p.meta.line, p.meta.column, &inputs.language)?;
1377 crate::assertion_map::locate(
1378 &p.meta.file,
1379 p.meta.line,
1380 column,
1381 &p.meta.source,
1382 text,
1383 starts,
1384 )
1385 .map(|_| column)
1386 })
1387 .collect::<Vec<_>>();
1388 let unanchored = located.iter().filter(|column| column.is_none()).count();
1389 let statements = if !detail {
1390 Vec::new()
1391 } else {
1392 measured_statements.iter().zip(&located).map(|(p, column)| {
1393 let at = column.map(|column| Anchor { file: p.meta.file.clone(), line: p.meta.line, column, text: p.meta.source.clone() });
1394 let all = all_points.get(&p.meta.id);
1395 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(),
1396 "executionEvidence":{"anyExecution":all.is_some_and(|p| p.covered),"passingTests":p.tests.iter().filter(|id| tests.contains_key(id)).collect::<Vec<_>>(),
1397 "outsidePassingTests":all.into_iter().flat_map(|p| &p.tests).filter(|id| !tests.contains_key(id)).collect::<Vec<_>>()}})
1398 }).collect::<Vec<_>>()
1399 };
1400 json!({"basis":"agent-assessed; passing assertion identity and same-test execution required; not mutation resistance",
1401 "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,
1402 "assertionsWithFlows":rows.iter().filter(|a| a["flows"].as_array().is_some_and(|f| !f.is_empty())).count(),
1403 "assertionsWithoutFlows":rows.iter().filter(|a| a["flows"].as_array().is_none_or(Vec::is_empty)).count(),
1404 "observedAssertionsWithoutCurrentExplanation":assertions_without_current_explanation,
1405 "questions":map.assertions.iter().map(|a| a.questions.len()+a.flows.iter().map(|f| f.questions.len()).sum::<usize>()).sum::<usize>(),
1406 "currentFlows":current_flows,"draftFlows":draft_flows,"staleFlows":stale_flows,"invalidFlows":invalid_flows,"eligibleFlows":credit_flows,"retiredAssertions":map.retired_assertions.len(),
1407 "unobservedAssertions":rows.iter().filter(|a| a["observedPassingTests"].as_array().is_none_or(Vec::is_empty)).count(),
1408 "inventoryFailures":inputs.limitations.iter().filter(|s| s.starts_with("Inventory unavailable for ")).count(),
1409 "unanchoredStatements":unanchored,
1410 "runPassed":passed,
1411 "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)}}},
1412 "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,
1413 "hasExecutionEvidence":!t.hits.is_empty() || !t.decisions.is_empty() || !t.lines.is_empty()})).collect::<Vec<_>>(),
1414 "creditedLines":if !detail { Vec::new() } else { credited.iter().map(|loc| json!({"file":loc.0,"line":loc.1,"assertions":line_assertions.get(loc)})).collect::<Vec<_>>() },
1415 "unassertedLines":if !detail { Vec::new() } else { denominator.difference(&credited).map(|(f,l)| json!({"file":f,"line":l})).collect::<Vec<_>>() },
1416 "changes":validation["changes"],"validationErrors":errors,"advisories":model::advisories(map),"limitations":inputs.limitations})
1417}
1418
1419#[cfg(test)]
1420mod tests {
1421 use super::*;
1422 fn fingerprint() -> RunFingerprint {
1423 RunFingerprint {
1424 algorithm: "sha256".into(),
1425 source: "source".into(),
1426 tests: "tests".into(),
1427 dependencies: "dependencies".into(),
1428 configuration: "configuration".into(),
1429 instrumenter: "instrumenter".into(),
1430 execution: "execution".into(),
1431 combined: "combined".into(),
1432 source_files: 1,
1433 test_files: 1,
1434 }
1435 }
1436
1437 fn reference_byte_column(
1439 source: &str,
1440 line: usize,
1441 column: usize,
1442 language: &str,
1443 ) -> Option<usize> {
1444 if line == 0 {
1445 return None;
1446 }
1447 let line = source.lines().nth(line - 1)?;
1448 if language != "javascript" {
1449 return column.checked_add(1);
1450 }
1451 if column == 0 {
1452 return None;
1453 }
1454 let mut units = 0;
1455 for (byte, ch) in line.char_indices() {
1456 if units == column - 1 {
1457 return Some(byte + 1);
1458 }
1459 units += ch.len_utf16();
1460 }
1461 (units == column - 1).then_some(line.len() + 1)
1462 }
1463
1464 #[test]
1465 fn a_column_from_line_starts_agrees_with_walking_the_file() {
1466 for source in [
1469 "",
1470 "a",
1471 "a\n",
1472 "a\r\nb\r\n",
1473 "x\r",
1474 "héllo\nwörld\n",
1475 "😀x\ny😀z\n",
1476 "const a = 1;\n b();\n",
1477 ] {
1478 let starts = crate::assertion_map::line_starts(source);
1479 for language in ["javascript", "python"] {
1480 for line in 0..source.len() + 3 {
1481 for column in 0..source.len() + 4 {
1482 assert_eq!(
1483 byte_column_in(source, &starts, line, column, language),
1484 reference_byte_column(source, line, column, language),
1485 "{language} {line}:{column} in {source:?}"
1486 );
1487 }
1488 }
1489 }
1490 }
1491 }
1492
1493 #[test]
1494 fn upgrading_supercov_does_not_make_every_map_stale() {
1495 let before = fingerprint();
1500 let mut after = fingerprint();
1501 after.instrumenter = "a new supercov".into();
1502 let delta = context_delta(&before, &after, "context", "context");
1503 assert!(!delta.execution);
1504 assert!(!delta.dependencies);
1505 }
1506
1507 #[test]
1508 fn a_dependency_upgrade_is_reported_without_invalidating_anything() {
1509 let before = fingerprint();
1513 let mut after = fingerprint();
1514 after.dependencies = "an upgraded lockfile".into();
1515 let delta = context_delta(&before, &after, "context", "context");
1516 assert!(delta.dependencies);
1517 assert!(!delta.execution, "an upgrade must not invalidate flows");
1518 }
1519
1520 #[test]
1521 fn what_actually_executes_still_invalidates_every_flow() {
1522 let before = fingerprint();
1525 let mut after = fingerprint();
1526 after.configuration = "a different tsconfig".into();
1527 assert!(context_delta(&before, &after, "context", "context").execution);
1528 assert!(
1529 context_delta(&before, &fingerprint(), "context", "another context").execution,
1530 "declared context variables still count"
1531 );
1532 assert!(!context_delta(&before, &fingerprint(), "context", "context").execution);
1533 }
1534
1535 #[test]
1536 fn report_cache_requires_exact_revision_and_intact_payload() {
1537 let directory =
1538 std::env::temp_dir().join(format!("supercov-report-cache-{}", std::process::id()));
1539 fs::create_dir_all(&directory).unwrap();
1540 let path = directory.join(REPORT_CACHE_FILE);
1541 let report = json!({"summary":{"statements":{"asserted":4,"percentage":97.02842377260981}},"assertions":[]});
1542 let mut cache = json!({"key":"revision-one","digest":digest(&report),"report":report});
1543 fs::write(&path, serde_json::to_vec(&cache).unwrap()).unwrap();
1544 assert_eq!(read_report_cache(&path, "revision-one"), Some(report));
1545 assert!(read_report_cache(&path, "revision-two").is_none());
1546 cache["report"]["summary"]["statements"]["asserted"] = json!(100);
1547 fs::write(&path, serde_json::to_vec(&cache).unwrap()).unwrap();
1548 assert!(read_report_cache(&path, "revision-one").is_none());
1549 fs::write(&path, "interrupted write").unwrap();
1550 assert!(read_report_cache(&path, "revision-one").is_none());
1551 fs::remove_dir_all(directory).unwrap();
1552 }
1553 use crate::evidence_archive::{EvidenceArchiveEntry, write_archive};
1554
1555 #[test]
1558 fn publication_records_what_each_test_ran_and_affected_tests_reads_it() {
1559 let root = std::env::temp_dir().join(format!("supercov-affected-{}", std::process::id()));
1560 let _ = fs::remove_dir_all(&root);
1561 fs::create_dir_all(root.join("src")).unwrap();
1562 fs::create_dir_all(root.join("tests")).unwrap();
1563 let app = "function work() {\n return 1;\n}\nfunction idle() {\n return 2;\n}\n";
1567 let test = "import assert from 'node:assert/strict';\nassert.equal(work(), 1);\n";
1568 fs::write(root.join("src/app.js"), app).unwrap();
1569 fs::write(root.join("tests/app.test.js"), test).unwrap();
1570 let inputs = crate::assertion_inputs::capture(
1574 &root,
1575 "python",
1576 ["src/app.js".into(), "tests/app.test.js".into()],
1577 )
1578 .unwrap();
1579 let directory = crate::run_store::create_analyzable_test_run(&root, "first");
1580 let path = directory.join("evidence.raw.gz");
1581 let entries =
1582 crate::assertion_inputs::append(read_archive(&path).unwrap(), &inputs).unwrap();
1583 let archive = write_archive(entries, &path).unwrap();
1584 let metadata_path = directory.join("run.json");
1585 let mut metadata: RunMetadata =
1586 serde_json::from_slice(&fs::read(&metadata_path).unwrap()).unwrap();
1587 metadata.raw_evidence.files = archive.files;
1588 metadata.raw_evidence.compressed_bytes = archive.compressed_bytes;
1589 metadata.raw_evidence.uncompressed_bytes = archive.uncompressed_bytes;
1590 fs::write(&metadata_path, serde_json::to_vec(&metadata).unwrap()).unwrap();
1591 prepare_publication(&root, &directory, &metadata).unwrap();
1592 let run = discover_runs(&root).unwrap().runs.remove(0);
1593 let stored = load_manifest(&run).unwrap();
1594 let (_, state) = load(&run, &stored).unwrap();
1595 let executions = state.executions.as_ref().expect("a record");
1596 let code = stored.manifest.files["src/app.js"].code.as_ref().unwrap();
1597 let work = code.units.iter().position(|u| u.path == "work").unwrap();
1598 let idle = code.units.iter().position(|u| u.path == "idle").unwrap();
1599 assert_eq!(executions.tests.len(), 1);
1600 let record = &executions.tests[0];
1601 assert_eq!(record.test.file, "tests/app.test.js");
1602 assert_eq!(record.test.name, "test");
1603 assert!(record.passed);
1604 assert_eq!(record.files["src/app.js"], vec![work]);
1605 assert_eq!(
1606 executions.probed["src/app.js"],
1607 vec![work],
1608 "idle holds no probe in this run"
1609 );
1610 let _ = idle;
1611
1612 let names = |value: &Value, key: &str| {
1613 value[key]
1614 .as_array()
1615 .unwrap()
1616 .iter()
1617 .map(|t| t["name"].as_str().unwrap().to_owned())
1618 .collect::<Vec<_>>()
1619 };
1620 let report = affected_tests(&root, &run).unwrap();
1622 assert!(names(&report, "affected").is_empty(), "{report}");
1623 assert_eq!(names(&report, "unaffected"), ["test"]);
1624 assert_eq!(report["summary"]["changedFiles"], 0);
1625 fs::write(root.join("src/app.js"), format!("// about\n{app}")).unwrap();
1627 let report = affected_tests(&root, &run).unwrap();
1628 assert!(names(&report, "affected").is_empty(), "{report}");
1629 assert_eq!(report["changedFiles"][0]["change"], "formatting");
1630 fs::write(
1632 root.join("src/app.js"),
1633 app.replace("return 1;", "return 1 + 0;"),
1634 )
1635 .unwrap();
1636 let report = affected_tests(&root, &run).unwrap();
1637 assert_eq!(names(&report, "affected"), ["test"]);
1638 assert_eq!(
1639 report["affected"][0]["reasons"][0],
1640 "src/app.js: work (line 1) changed (this test ran it)"
1641 );
1642 fs::write(
1646 root.join("src/app.js"),
1647 app.replace("return 2;", "return 2 + 0;"),
1648 )
1649 .unwrap();
1650 let report = affected_tests(&root, &run).unwrap();
1651 assert_eq!(names(&report, "affected"), ["test"], "{report}");
1652 assert!(
1653 report["affected"][0]["reasons"][0]
1654 .as_str()
1655 .unwrap()
1656 .contains("this test ran code in this file"),
1657 "{report}"
1658 );
1659 fs::write(
1661 root.join("src/app.js"),
1662 format!("{app}function more() {{}}\n"),
1663 )
1664 .unwrap();
1665 let report = affected_tests(&root, &run).unwrap();
1666 assert_eq!(names(&report, "affected"), ["test"]);
1667 assert_eq!(report["changedFiles"][0]["change"], "declarations");
1668 fs::write(root.join("src/app.js"), app).unwrap();
1670 fs::write(root.join("tests/app.test.js"), test.replace("1)", "2)")).unwrap();
1671 let report = affected_tests(&root, &run).unwrap();
1672 assert_eq!(names(&report, "affected"), ["test"]);
1673 assert!(
1674 report["affected"][0]["reasons"][0]
1675 .as_str()
1676 .unwrap()
1677 .starts_with("test file changed"),
1678 "{report}"
1679 );
1680 fs::write(root.join("tests/app.test.js"), test).unwrap();
1682 fs::remove_file(root.join("src/app.js")).unwrap();
1683 let report = affected_tests(&root, &run).unwrap();
1684 assert_eq!(
1685 report["affected"][0]["reasons"][0],
1686 "src/app.js removed (this test ran code in it)"
1687 );
1688 fs::remove_dir_all(root).unwrap();
1689 }
1690
1691 #[test]
1692 fn a_test_that_ran_beside_others_is_undetermined_rather_than_unaffected() {
1693 let root = std::env::temp_dir().join(format!(
1700 "supercov-undetermined-{}-{}",
1701 std::process::id(),
1702 std::time::SystemTime::now()
1703 .duration_since(std::time::UNIX_EPOCH)
1704 .unwrap()
1705 .as_nanos()
1706 ));
1707 let _ = fs::remove_dir_all(&root);
1708 fs::create_dir_all(root.join("src")).unwrap();
1709 fs::create_dir_all(root.join("tests")).unwrap();
1710 let app = "function work() {\n return 1;\n}\nfunction idle() {\n return 2;\n}\n";
1711 let test = "import assert from 'node:assert/strict';\nassert.equal(work(), 1);\n";
1712 fs::write(root.join("src/app.js"), app).unwrap();
1713 fs::write(root.join("tests/app.test.js"), test).unwrap();
1714 let inputs = crate::assertion_inputs::capture(
1715 &root,
1716 "python",
1717 ["src/app.js".into(), "tests/app.test.js".into()],
1718 )
1719 .unwrap();
1720 let directory = crate::run_store::create_run_wide_test_run(&root, "parallel");
1721 let path = directory.join("evidence.raw.gz");
1722 let entries =
1723 crate::assertion_inputs::append(read_archive(&path).unwrap(), &inputs).unwrap();
1724 let archive = write_archive(entries, &path).unwrap();
1725 let metadata_path = directory.join("run.json");
1726 let mut metadata: RunMetadata =
1727 serde_json::from_slice(&fs::read(&metadata_path).unwrap()).unwrap();
1728 metadata.raw_evidence.files = archive.files;
1729 metadata.raw_evidence.compressed_bytes = archive.compressed_bytes;
1730 metadata.raw_evidence.uncompressed_bytes = archive.uncompressed_bytes;
1731 fs::write(&metadata_path, serde_json::to_vec(&metadata).unwrap()).unwrap();
1732 prepare_publication(&root, &directory, &metadata).unwrap();
1733 let run = discover_runs(&root).unwrap().runs.remove(0);
1734
1735 let (_, state) = load(&run, &load_manifest(&run).unwrap()).unwrap();
1736 let executions = state.executions.as_ref().expect("a record");
1737 assert_eq!(executions.tests.len(), 1, "the named test, not the record");
1738 let record = &executions.tests[0];
1739 assert_eq!(
1740 record.attribution,
1741 crate::coverage_report::ATTRIBUTION_RUN_WIDE
1742 );
1743 assert!(
1744 record.files.is_empty(),
1745 "it is credited with nothing: {:?}",
1746 record.files
1747 );
1748 assert!(
1749 !executions.covered["src/app.js"].is_empty(),
1750 "and the run is credited with what it reached"
1751 );
1752
1753 let bucket = |value: &Value, key: &str| {
1754 value[key]
1755 .as_array()
1756 .unwrap()
1757 .iter()
1758 .map(|t| t["name"].as_str().unwrap().to_owned())
1759 .collect::<Vec<_>>()
1760 };
1761
1762 let report = affected_tests(&root, &run).unwrap();
1765 assert_eq!(bucket(&report, "unaffected"), ["test"], "{report}");
1766 assert!(bucket(&report, "undetermined").is_empty(), "{report}");
1767
1768 fs::write(
1771 root.join("src/app.js"),
1772 app.replace("return 1;", "return 3;"),
1773 )
1774 .unwrap();
1775 let report = affected_tests(&root, &run).unwrap();
1776 assert!(
1777 bucket(&report, "affected").is_empty(),
1778 "nothing proves it ran the change: {report}"
1779 );
1780 assert_eq!(
1781 bucket(&report, "undetermined"),
1782 ["test"],
1783 "the run reached it, so this test might have: {report}"
1784 );
1785 assert_eq!(
1786 report["undetermined"][0]["attribution"],
1787 crate::coverage_report::ATTRIBUTION_RUN_WIDE,
1788 "{report}"
1789 );
1790 fs::remove_dir_all(root).unwrap();
1791 }
1792
1793 #[test]
1794 fn a_lower_bound_still_proves_what_it_recorded() {
1795 let root = std::env::temp_dir().join(format!(
1802 "supercov-partial-{}-{}",
1803 std::process::id(),
1804 std::time::SystemTime::now()
1805 .duration_since(std::time::UNIX_EPOCH)
1806 .unwrap()
1807 .as_nanos()
1808 ));
1809 let _ = fs::remove_dir_all(&root);
1810 fs::create_dir_all(root.join("src")).unwrap();
1811 fs::create_dir_all(root.join("tests")).unwrap();
1812 let app = "function work() {\n return 1;\n}\nfunction idle() {\n return 2;\n}\n";
1813 let test = "import assert from 'node:assert/strict';\nassert.equal(work(), 1);\n";
1814 fs::write(root.join("src/app.js"), app).unwrap();
1815 fs::write(root.join("tests/app.test.js"), test).unwrap();
1816 let inputs = crate::assertion_inputs::capture(
1817 &root,
1818 "python",
1819 ["src/app.js".into(), "tests/app.test.js".into()],
1820 )
1821 .unwrap();
1822 let directory = crate::run_store::create_partial_test_run(&root, "partial");
1823 let path = directory.join("evidence.raw.gz");
1824 let entries =
1825 crate::assertion_inputs::append(read_archive(&path).unwrap(), &inputs).unwrap();
1826 let archive = write_archive(entries, &path).unwrap();
1827 let metadata_path = directory.join("run.json");
1828 let mut metadata: RunMetadata =
1829 serde_json::from_slice(&fs::read(&metadata_path).unwrap()).unwrap();
1830 metadata.raw_evidence.files = archive.files;
1831 metadata.raw_evidence.compressed_bytes = archive.compressed_bytes;
1832 metadata.raw_evidence.uncompressed_bytes = archive.uncompressed_bytes;
1833 fs::write(&metadata_path, serde_json::to_vec(&metadata).unwrap()).unwrap();
1834 prepare_publication(&root, &directory, &metadata).unwrap();
1835 let run = discover_runs(&root).unwrap().runs.remove(0);
1836
1837 let (_, state) = load(&run, &load_manifest(&run).unwrap()).unwrap();
1838 let record = &state.executions.as_ref().expect("a record").tests[0];
1839 assert_eq!(
1840 record.attribution,
1841 crate::coverage_report::ATTRIBUTION_PARTIAL
1842 );
1843 assert!(
1844 !record.files.is_empty(),
1845 "a lower bound is coverage it was credited with"
1846 );
1847
1848 let names = |value: &Value, key: &str| {
1849 value[key]
1850 .as_array()
1851 .unwrap()
1852 .iter()
1853 .map(|t| t["name"].as_str().unwrap().to_owned())
1854 .collect::<Vec<_>>()
1855 };
1856 fs::write(
1857 root.join("src/app.js"),
1858 app.replace("return 1;", "return 3;"),
1859 )
1860 .unwrap();
1861 let report = affected_tests(&root, &run).unwrap();
1862 assert_eq!(
1863 names(&report, "affected"),
1864 ["test"],
1865 "what its own record proves, it proves: {report}"
1866 );
1867 assert!(names(&report, "undetermined").is_empty(), "{report}");
1868 let reasons = report["affected"][0]["reasons"].as_array().unwrap();
1870 assert_eq!(reasons.len(), 1, "{report}");
1871 let reason = reasons[0].as_str().unwrap();
1872 assert!(
1873 reason.contains("this test ran it"),
1874 "the reason is its own: {report}"
1875 );
1876 assert!(
1877 !reason.contains("the run covered"),
1878 "and not the run's stand-in: {report}"
1879 );
1880 fs::remove_dir_all(root).unwrap();
1881 }
1882
1883 #[test]
1884 fn legacy_maps_import_without_the_old_checkout_and_require_review() {
1885 let root = std::env::temp_dir().join(format!("supercov-legacy-map-{}", std::process::id()));
1886 fs::create_dir_all(&root).unwrap();
1887 let source = "import assert from 'node:assert/strict'; assert.equal(1, 1);\n";
1888 fs::write(root.join("test.js"), source).unwrap();
1889 let old =
1890 crate::assertion_inputs::capture(&root, "javascript", ["test.js".into()]).unwrap();
1891 let directory = crate::run_store::create_analyzable_test_run(&root, "legacy");
1892 let path = directory.join("evidence.raw.gz");
1893 let mut entries = read_archive(&path).unwrap();
1894 entries.push(EvidenceArchiveEntry {
1895 path: ARCHIVE_PATH.into(),
1896 contents: serde_json::to_vec(&old).unwrap(),
1897 });
1898 let archive = write_archive(entries, &path).unwrap();
1899 let metadata_path = directory.join("run.json");
1900 let mut metadata: RunMetadata =
1901 serde_json::from_slice(&fs::read(&metadata_path).unwrap()).unwrap();
1902 metadata.raw_evidence.files = archive.files;
1903 metadata.raw_evidence.compressed_bytes = archive.compressed_bytes;
1904 metadata.raw_evidence.uncompressed_bytes = archive.uncompressed_bytes;
1905 fs::write(&metadata_path, serde_json::to_vec(&metadata).unwrap()).unwrap();
1906 let run = discover_runs(&root).unwrap().runs.remove(0);
1907 let stored = load_manifest(&run).unwrap();
1908 let (map, _) = seed(&old, &stored.evidence_digest);
1909 let legacy_map = json!({"schemaVersion":1,"assertions":[{"id":map.assertions[0].id,"at":map.assertions[0].at,"analysis":"mapped","observes":[],"flows":[{
1910 "id":"constant","explanation":"The assertion checks the constant one.","appliesTo":[],"nodes":[],"edges":[],"countsAsAsserted":[],"watch":[{"kind":"span","at":map.assertions[0].at}]
1911 }]}]});
1912 let legacy_state = json!({"schemaVersion":1,"inputsDigest":digest(&old),"evidenceDigest":stored.evidence_digest,"reviews":{},"scopeReview":[]});
1913 write_json(&root, &run, MAP_FILE, &legacy_map).unwrap();
1914 write_json(&root, &run, STATE_FILE, &legacy_state).unwrap();
1915 let map = model::parse_stored(&serde_json::to_vec(&legacy_map).unwrap()).unwrap();
1916 let map_bytes = fs::read(directory.join(MAP_FILE)).unwrap();
1917 let state_bytes = fs::read(directory.join(STATE_FILE)).unwrap();
1918
1919 fs::write(root.join("test.js"), format!("\n{source}")).unwrap();
1920 assert!(load_inputs(&root, &run).is_err());
1921 let (imported, state) = load(&run, &stored).unwrap();
1922 let new =
1923 crate::assertion_inputs::capture(&root, "javascript", ["test.js".into()]).unwrap();
1924 let (next, state) =
1925 carry(&imported, &state, &stored.manifest, &new, "new-run", false).unwrap();
1926 assert_eq!(next.assertions[0].id, map.assertions[0].id);
1927 assert_eq!(
1928 next.assertions[0].flows[0].explanation,
1929 map.assertions[0].flows[0].explanation
1930 );
1931 assert!(next.assertions[0].flows[0].basis.is_none());
1932 assert!(!next.assertions[0].flows[0].questions.is_empty());
1933 assert_eq!(fs::read(directory.join(MAP_FILE)).unwrap(), map_bytes);
1934 assert_eq!(fs::read(directory.join(STATE_FILE)).unwrap(), state_bytes);
1935
1936 let next_directory = crate::run_store::create_analyzable_test_run(&root, "next");
1939 let mut entries = read_archive(&path).unwrap();
1940 entries
1941 .iter_mut()
1942 .find(|e| e.path == ARCHIVE_PATH)
1943 .unwrap()
1944 .contents = serde_json::to_vec(&old.manifest()).unwrap();
1945 let raw = write_archive(entries, &next_directory.join("evidence.raw.gz")).unwrap();
1946 let mut next_metadata = metadata.clone();
1947 next_metadata.id = "next".into();
1948 next_metadata.started_at = "next".into();
1949 next_metadata.raw_evidence.compressed_bytes = raw.compressed_bytes;
1950 next_metadata.raw_evidence.uncompressed_bytes = raw.uncompressed_bytes;
1951 fs::write(
1952 next_directory.join("run.json"),
1953 serde_json::to_vec(&next_metadata).unwrap(),
1954 )
1955 .unwrap();
1956 fs::remove_file(root.join("test.js")).unwrap();
1957 prepare_publication(&root, &next_directory, &next_metadata).unwrap();
1958 let next_run = discover_runs(&root)
1959 .unwrap()
1960 .runs
1961 .into_iter()
1962 .find(|r| r.id == "next")
1963 .unwrap();
1964 let next_manifest = load_manifest(&next_run).unwrap();
1965 let (pending, pending_state) = load(&next_run, &next_manifest).unwrap();
1966 assert_eq!(pending.assertions[0], map.assertions[0]);
1967 assert!(!pending_state.changes.is_empty());
1968 assert!(
1969 !pending_state.flows
1970 [&flow_key(&pending.assertions[0], &pending.assertions[0].flows[0])]
1971 .reasons
1972 .is_empty()
1973 );
1974 assert!(load_inputs(&root, &next_run).is_err());
1975 assert_eq!(fs::read(directory.join(MAP_FILE)).unwrap(), map_bytes);
1976 assert_eq!(fs::read(directory.join(STATE_FILE)).unwrap(), state_bytes);
1977
1978 let mut corrupt = state.clone();
1979 corrupt.evidence_digest = "wrong".into();
1980 write_json(&root, &run, STATE_FILE, &corrupt).unwrap();
1981 assert!(load(&run, &stored).is_err());
1982 fs::remove_dir_all(root).unwrap();
1983 }
1984}