1use std::{
10 collections::{BTreeMap, BTreeSet},
11 fs,
12 path::{Path, PathBuf},
13};
14
15use serde::{Deserialize, Serialize};
16
17use crate::{
18 coverage_analysis::McdcVector,
19 coverage_report::{DecisionSnapshot, RuntimeEvent, RuntimeSnapshot},
20 rust_compiler_manifest::NormalizedRustCompilerManifest,
21};
22
23const BUNDLE_SCHEMA: &str = "supercov-rust-ctfe-unit-v1";
24
25#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27struct CtfeBundleFile {
28 schema: String,
29 #[serde(rename = "crate")]
30 crate_name: String,
31 mappings: Vec<CtfeMapping>,
32 events: Vec<CtfeEvent>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
36#[serde(rename_all = "camelCase", deny_unknown_fields)]
37struct CtfeMapping {
38 marker: String,
39 definition: String,
40 observation_kind: String,
41 ordinal: u32,
42 hit_ordinals: Vec<String>,
43 decision: Option<CtfeDecisionMapping>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
47#[serde(rename_all = "camelCase", deny_unknown_fields)]
48struct CtfeDecisionMapping {
49 id: String,
50 event: String,
51 condition_index: Option<u64>,
52 value: Option<bool>,
53 outcome: Option<bool>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
57#[serde(rename_all = "camelCase", deny_unknown_fields)]
58struct CtfeEvent {
59 #[serde(rename = "crate")]
60 crate_name: String,
61 kind: String,
62 marker: String,
63 definition: String,
64 observation_kind: String,
65 ordinal: u32,
66 thread: String,
67}
68
69#[derive(Debug)]
70struct ActiveDecision {
71 id: String,
72 values: Vec<Option<bool>>,
73}
74
75#[derive(Debug)]
76struct ActiveInvocation {
77 definition: String,
78 decisions: Vec<ActiveDecision>,
79 committed_loops: BTreeSet<String>,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
83#[serde(rename_all = "camelCase")]
84pub struct RustCompilerCtfeUnit {
85 pub identity: String,
86 pub crate_name: String,
87 pub snapshot: RuntimeSnapshot,
88 pub observations: usize,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum RustCompilerCtfeError {
93 Io { path: PathBuf, reason: String },
94 Invalid(String),
95}
96
97impl std::fmt::Display for RustCompilerCtfeError {
98 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 match self {
100 Self::Io { path, reason } => write!(formatter, "{}: {reason}", path.display()),
101 Self::Invalid(reason) => write!(formatter, "invalid Rust CTFE evidence: {reason}"),
102 }
103 }
104}
105
106impl std::error::Error for RustCompilerCtfeError {}
107
108fn io_error(path: &Path, error: impl std::fmt::Display) -> RustCompilerCtfeError {
109 RustCompilerCtfeError::Io {
110 path: path.to_path_buf(),
111 reason: error.to_string(),
112 }
113}
114
115fn parse_u64(value: &str, context: &str) -> Result<u64, RustCompilerCtfeError> {
116 if value.is_empty() || (value.len() > 1 && value.starts_with('0')) {
117 return Err(RustCompilerCtfeError::Invalid(format!(
118 "{context} is not canonical unsigned decimal"
119 )));
120 }
121 value.parse::<u64>().map_err(|_| {
122 RustCompilerCtfeError::Invalid(format!("{context} is not canonical unsigned decimal"))
123 })
124}
125
126fn parse_json<T: for<'de> Deserialize<'de>>(
127 path: &Path,
128 bytes: &[u8],
129) -> Result<T, RustCompilerCtfeError> {
130 serde_json::from_slice(bytes)
131 .map_err(|error| RustCompilerCtfeError::Invalid(format!("{}: {error}", path.display())))
132}
133
134fn ctfe_files(directory: &Path) -> Result<BTreeMap<String, PathBuf>, RustCompilerCtfeError> {
135 let mut units = BTreeMap::new();
136 for entry in fs::read_dir(directory)
137 .map_err(|error| io_error(directory, error))?
138 .collect::<Result<Vec<_>, _>>()
139 .map_err(|error| io_error(directory, error))?
140 {
141 let path = entry.path();
142 let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
143 return Err(RustCompilerCtfeError::Invalid(
144 "compiler output contains a non-UTF-8 name".into(),
145 ));
146 };
147 let file_type = entry.file_type().map_err(|error| io_error(&path, error))?;
148 if !file_type.is_file() {
149 if name.starts_with("ctfe-") || name.starts_with(".ctfe-") {
150 return Err(RustCompilerCtfeError::Invalid(format!(
151 "CTFE compiler artifact is not a regular file: {name}"
152 )));
153 }
154 continue;
155 }
156 let identity = name
157 .strip_prefix("ctfe-unit-")
158 .and_then(|name| name.strip_suffix(".json"))
159 .filter(|identity| !identity.is_empty());
160 if let Some(identity) = identity {
161 if units.insert(identity.to_owned(), path).is_some() {
162 return Err(RustCompilerCtfeError::Invalid(format!(
163 "duplicate CTFE compiler unit {identity}"
164 )));
165 }
166 } else if name.starts_with("ctfe-") || name.starts_with(".ctfe-") {
167 return Err(RustCompilerCtfeError::Invalid(format!(
168 "unrecognized or incomplete CTFE compiler artifact {name}"
169 )));
170 }
171 }
172 Ok(units)
173}
174
175fn reconstruct_unit(
176 identity: String,
177 bundle_path: &Path,
178 normalized: &NormalizedRustCompilerManifest,
179 timestamp_ms: i64,
180) -> Result<RustCompilerCtfeUnit, RustCompilerCtfeError> {
181 let bundle: CtfeBundleFile = parse_json(
182 bundle_path,
183 &fs::read(bundle_path).map_err(|error| io_error(bundle_path, error))?,
184 )?;
185 if bundle.schema != BUNDLE_SCHEMA || bundle.crate_name.trim().is_empty() {
186 return Err(RustCompilerCtfeError::Invalid(format!(
187 "{} has an unsupported schema or empty crate",
188 bundle_path.display()
189 )));
190 }
191 let decisions = normalized
192 .manifest
193 .decisions
194 .iter()
195 .map(|decision| (decision.id.as_str(), decision))
196 .collect::<BTreeMap<_, _>>();
197 let mut mappings = BTreeMap::<u64, CtfeMapping>::new();
198 for mapping in bundle.mappings {
199 let marker = parse_u64(&mapping.marker, "CTFE marker")?;
200 if marker == 0
201 || mapping.definition.trim().is_empty()
202 || !matches!(
203 mapping.observation_kind.as_str(),
204 "entry"
205 | "block"
206 | "edge"
207 | "selection"
208 | "exit"
209 | "decision-start"
210 | "decision-condition"
211 | "decision-finish"
212 )
213 {
214 return Err(RustCompilerCtfeError::Invalid(format!(
215 "malformed mapping for marker {marker}"
216 )));
217 }
218 let mut previous = None;
219 for hit in &mapping.hit_ordinals {
220 let hit = parse_u64(hit, "CTFE hit ordinal")?;
221 if hit == 0 || previous.is_some_and(|previous| previous >= hit) {
222 return Err(RustCompilerCtfeError::Invalid(format!(
223 "marker {marker} has non-canonical hit ordinals"
224 )));
225 }
226 if normalized.internal_ordinals.contains(&hit)
227 || !normalized.hit_obligations_by_ordinal.contains_key(&hit)
228 {
229 return Err(RustCompilerCtfeError::Invalid(format!(
230 "marker {marker} references unknown/non-evidence ordinal {hit}"
231 )));
232 }
233 previous = Some(hit);
234 }
235 match &mapping.decision {
236 None if mapping.observation_kind.starts_with("decision-") => {
237 return Err(RustCompilerCtfeError::Invalid(format!(
238 "semantic marker {marker} has no decision mapping"
239 )));
240 }
241 Some(_) if !mapping.observation_kind.starts_with("decision-") => {
242 return Err(RustCompilerCtfeError::Invalid(format!(
243 "non-decision marker {marker} carries a decision mapping"
244 )));
245 }
246 Some(decision) => {
247 let meta = decisions.get(decision.id.as_str()).ok_or_else(|| {
248 RustCompilerCtfeError::Invalid(format!(
249 "marker {marker} references unknown decision {}",
250 decision.id
251 ))
252 })?;
253 let valid_shape = match decision.event.as_str() {
254 "start" => {
255 mapping.observation_kind == "decision-start"
256 && decision.condition_index.is_none()
257 && decision.value.is_none()
258 && decision.outcome.is_none()
259 }
260 "condition" => {
261 mapping.observation_kind == "decision-condition"
262 && decision
263 .condition_index
264 .is_some_and(|index| index < meta.conditions.len() as u64)
265 && decision.value.is_some()
266 && decision.outcome.is_none()
267 }
268 "finish" => {
269 mapping.observation_kind == "decision-finish"
270 && decision.condition_index.is_none()
271 && decision.value.is_none()
272 && decision.outcome.is_some()
273 }
274 _ => false,
275 };
276 if !valid_shape {
277 return Err(RustCompilerCtfeError::Invalid(format!(
278 "marker {marker} has a malformed decision event"
279 )));
280 }
281 let expected_hits = match decision.event.as_str() {
282 "start" | "condition" => BTreeSet::new(),
283 "finish" => {
284 let outcome = decision.outcome.expect("validated decision outcome");
285 let alternatives = normalized
286 .decision_outcome_obligations
287 .get(&decision.id)
288 .expect("validated decision outcome mapping");
289 let mut expected = BTreeSet::from([if outcome {
290 alternatives.1.as_str()
291 } else {
292 alternatives.0.as_str()
293 }]);
294 if let Some(loop_alternatives) =
295 normalized.decision_loop_obligations.get(&decision.id)
296 {
297 expected.insert(if outcome {
298 loop_alternatives.1.as_str()
299 } else {
300 loop_alternatives.0.as_str()
301 });
302 }
303 expected
304 }
305 _ => unreachable!("validated decision event"),
306 };
307 let mapped_hits = mapping
308 .hit_ordinals
309 .iter()
310 .map(|ordinal| parse_u64(ordinal, "CTFE hit ordinal"))
311 .collect::<Result<Vec<_>, _>>()?
312 .into_iter()
313 .flat_map(|ordinal| normalized.hit_obligations_by_ordinal[&ordinal].iter())
314 .map(String::as_str)
315 .collect::<BTreeSet<_>>();
316 if mapped_hits != expected_hits {
317 return Err(RustCompilerCtfeError::Invalid(format!(
318 "decision {} {} marker maps to the wrong coverage obligations",
319 decision.id, decision.event
320 )));
321 }
322 }
323 None => {}
324 }
325 if mappings.insert(marker, mapping).is_some() {
326 return Err(RustCompilerCtfeError::Invalid(format!(
327 "duplicate CTFE marker {marker}"
328 )));
329 }
330 }
331 if mappings.is_empty() {
332 return Err(RustCompilerCtfeError::Invalid(format!(
333 "{} contains no mappings",
334 bundle_path.display()
335 )));
336 }
337
338 let events = bundle.events;
339 let mut stacks = BTreeMap::<String, Vec<ActiveInvocation>>::new();
340 let mut hits = BTreeSet::new();
341 let mut decision_vectors = BTreeMap::<String, BTreeSet<(Vec<Option<bool>>, bool)>>::new();
342 let mut runtime_events = Vec::new();
343 for event in &events {
344 let mut ignored_hits = BTreeSet::new();
345 if event.kind != "ctfe-marker"
346 || event.crate_name != bundle.crate_name
347 || event.thread.trim().is_empty()
348 {
349 return Err(RustCompilerCtfeError::Invalid(format!(
350 "{} contains malformed event identity",
351 bundle_path.display()
352 )));
353 }
354 let marker = parse_u64(&event.marker, "observed CTFE marker")?;
355 let mapping = mappings.get(&marker).ok_or_else(|| {
356 RustCompilerCtfeError::Invalid(format!("observed CTFE marker {marker} is unmapped"))
357 })?;
358 if mapping.definition != event.definition
359 || mapping.observation_kind != event.observation_kind
360 || mapping.ordinal != event.ordinal
361 {
362 return Err(RustCompilerCtfeError::Invalid(format!(
363 "observed CTFE marker {marker} changed identity"
364 )));
365 }
366 let stack = stacks.entry(event.thread.clone()).or_default();
367 match event.observation_kind.as_str() {
368 "entry" => stack.push(ActiveInvocation {
369 definition: event.definition.clone(),
370 decisions: Vec::new(),
371 committed_loops: BTreeSet::new(),
372 }),
373 "block" | "edge" | "selection" | "decision-start" | "decision-condition"
374 | "decision-finish" => {
375 let Some(invocation) = stack.last_mut() else {
376 return Err(RustCompilerCtfeError::Invalid(format!(
377 "CTFE marker {marker} was observed outside an invocation on {}",
378 event.thread
379 )));
380 };
381 if invocation.definition != event.definition {
382 return Err(RustCompilerCtfeError::Invalid(format!(
383 "CTFE marker {marker} crossed invocation identity on {}",
384 event.thread
385 )));
386 }
387 if let Some(decision) = &mapping.decision {
388 match decision.event.as_str() {
389 "start" => {
390 let meta = decisions[decision.id.as_str()];
391 invocation.decisions.push(ActiveDecision {
392 id: decision.id.clone(),
393 values: vec![None; meta.conditions.len()],
394 });
395 }
396 "condition" => {
397 let active = invocation.decisions.last_mut().ok_or_else(|| {
398 RustCompilerCtfeError::Invalid(format!(
399 "decision condition {} has no active frame",
400 decision.id
401 ))
402 })?;
403 if active.id != decision.id {
404 return Err(RustCompilerCtfeError::Invalid(format!(
405 "decision condition {} crossed active decision {}",
406 decision.id, active.id
407 )));
408 }
409 let index = usize::try_from(
410 decision.condition_index.expect("validated condition index"),
411 )
412 .map_err(|_| {
413 RustCompilerCtfeError::Invalid(format!(
414 "decision {} condition index exceeds usize",
415 decision.id
416 ))
417 })?;
418 if active.values[index]
419 .replace(decision.value.expect("validated condition value"))
420 .is_some()
421 {
422 return Err(RustCompilerCtfeError::Invalid(format!(
423 "decision {} condition {index} was observed twice",
424 decision.id
425 )));
426 }
427 }
428 "finish" => {
429 let active = invocation.decisions.pop().ok_or_else(|| {
430 RustCompilerCtfeError::Invalid(format!(
431 "decision finish {} has no active frame",
432 decision.id
433 ))
434 })?;
435 if active.id != decision.id {
436 return Err(RustCompilerCtfeError::Invalid(format!(
437 "decision finish {} closed active decision {}",
438 decision.id, active.id
439 )));
440 }
441 let outcome = decision.outcome.expect("validated decision outcome");
442 if let Some(loop_alternatives) =
443 normalized.decision_loop_obligations.get(&decision.id)
444 && !invocation.committed_loops.insert(decision.id.clone())
445 {
446 ignored_hits.insert(if outcome {
447 loop_alternatives.1.clone()
448 } else {
449 loop_alternatives.0.clone()
450 });
451 }
452 decision_vectors
453 .entry(decision.id.clone())
454 .or_default()
455 .insert((active.values.clone(), outcome));
456 if let Some(selections) = normalized
457 .decision_logical_selection_obligations
458 .get(&decision.id)
459 {
460 for selection in selections {
461 let alternative_id =
462 if active.values[selection.right_condition_index].is_some()
463 {
464 &selection.right_evaluated_id
465 } else {
466 &selection.short_circuited_id
467 };
468 hits.insert(alternative_id.clone());
469 runtime_events.push(RuntimeEvent {
470 event_type: "hit".into(),
471 id: alternative_id.clone(),
472 vector: None,
473 timestamp_ms,
474 phase_id: None,
475 environment: "rust-ctfe".into(),
476 });
477 }
478 }
479 runtime_events.push(RuntimeEvent {
480 event_type: "decision".into(),
481 id: decision.id.clone(),
482 vector: Some(McdcVector {
483 values: active.values,
484 outcome,
485 }),
486 timestamp_ms,
487 phase_id: None,
488 environment: "rust-ctfe".into(),
489 });
490 }
491 _ => unreachable!("validated decision event"),
492 }
493 }
494 }
495 "exit" => {
496 let Some(invocation) = stack.pop() else {
497 return Err(RustCompilerCtfeError::Invalid(format!(
498 "CTFE marker {marker} closed an absent invocation on {}",
499 event.thread
500 )));
501 };
502 if invocation.definition != event.definition || !invocation.decisions.is_empty() {
503 return Err(RustCompilerCtfeError::Invalid(format!(
504 "CTFE marker {marker} closed the wrong or incomplete invocation on {}",
505 event.thread
506 )));
507 }
508 }
509 _ => unreachable!("validated observation kind"),
510 }
511 for ordinal in &mapping.hit_ordinals {
512 let ordinal = parse_u64(ordinal, "CTFE hit ordinal")?;
513 for id in &normalized.hit_obligations_by_ordinal[&ordinal] {
514 if ignored_hits.contains(id) {
515 continue;
516 }
517 hits.insert(id.clone());
518 runtime_events.push(RuntimeEvent {
519 event_type: "hit".into(),
520 id: id.clone(),
521 vector: None,
522 timestamp_ms,
523 phase_id: None,
524 environment: "rust-ctfe".into(),
525 });
526 }
527 }
528 }
529 if let Some((thread, stack)) = stacks.iter().find(|(_, stack)| !stack.is_empty()) {
530 return Err(RustCompilerCtfeError::Invalid(format!(
531 "successful compiler unit {identity} left {} CTFE frame(s) open on {thread}",
532 stack.len()
533 )));
534 }
535 Ok(RustCompilerCtfeUnit {
536 identity,
537 crate_name: bundle.crate_name,
538 snapshot: RuntimeSnapshot {
539 decisions: decision_vectors
540 .into_iter()
541 .map(|(id, vectors)| DecisionSnapshot {
542 meta: decisions[id.as_str()].clone(),
543 vectors: vectors
544 .into_iter()
545 .map(|(values, outcome)| McdcVector { values, outcome })
546 .collect(),
547 })
548 .collect(),
549 hits: hits.into_iter().collect(),
550 events: runtime_events,
551 },
552 observations: events.len(),
553 })
554}
555
556pub fn read_rust_compiler_ctfe(
557 directory: &Path,
558 normalized: &NormalizedRustCompilerManifest,
559 timestamp_ms: i64,
560) -> Result<Vec<RustCompilerCtfeUnit>, RustCompilerCtfeError> {
561 ctfe_files(directory)?
562 .into_iter()
563 .map(|(identity, bundle)| reconstruct_unit(identity, &bundle, normalized, timestamp_ms))
564 .collect()
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570 use std::{
571 sync::atomic::{AtomicU64, Ordering},
572 time::{SystemTime, UNIX_EPOCH},
573 };
574
575 use serde_json::{Value, json};
576
577 use crate::{
578 coverage_analysis::PointKind,
579 coverage_report::{
580 BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
581 },
582 };
583
584 static SCRATCH_NONCE: AtomicU64 = AtomicU64::new(0);
585
586 struct Scratch(PathBuf);
587
588 impl Scratch {
589 fn new() -> Self {
590 let epoch = SystemTime::now()
591 .duration_since(UNIX_EPOCH)
592 .expect("clock before epoch")
593 .as_nanos();
594 let path = std::env::temp_dir().join(format!(
595 "supercov-rust-ctfe-{}-{epoch}-{}",
596 std::process::id(),
597 SCRATCH_NONCE.fetch_add(1, Ordering::Relaxed)
598 ));
599 fs::create_dir_all(&path).expect("create CTFE scratch directory");
600 Self(path)
601 }
602
603 fn write(&self, map: Value, events: &[Value]) {
604 let mut bundle = map;
605 bundle["events"] = Value::Array(events.to_vec());
606 fs::write(
607 self.0.join("ctfe-unit-unit.json"),
608 serde_json::to_vec(&bundle).expect("serialize CTFE bundle"),
609 )
610 .expect("write CTFE bundle");
611 }
612
613 fn write_raw(&self, name: &str, bytes: &[u8]) {
614 fs::write(self.0.join(name), bytes).expect("write raw CTFE artifact");
615 }
616 }
617
618 impl Drop for Scratch {
619 fn drop(&mut self) {
620 let _ = fs::remove_dir_all(&self.0);
621 }
622 }
623
624 fn normalized_manifest() -> NormalizedRustCompilerManifest {
625 let decision = DecisionMeta {
626 id: "decision".into(),
627 file: "src/lib.rs".into(),
628 line: 1,
629 column: 1,
630 source: "value".into(),
631 conditions: vec!["value".into()],
632 kind: "control".into(),
633 };
634 let branch = BranchMeta {
635 id: "outcome".into(),
636 kind: "decision-outcome".into(),
637 file: "src/lib.rs".into(),
638 line: 1,
639 column: 1,
640 source: "value".into(),
641 alternatives: vec![
642 BranchAlternativeMeta {
643 id: "false-alternative".into(),
644 label: "condition false".into(),
645 },
646 BranchAlternativeMeta {
647 id: "true-alternative".into(),
648 label: "condition true".into(),
649 },
650 ],
651 };
652 NormalizedRustCompilerManifest {
653 manifest: CoverageManifest {
654 unmeasured: Vec::new(),
655 decisions: vec![decision],
656 points: vec![PointMeta {
657 id: "function".into(),
658 kind: PointKind::Function,
659 file: "src/lib.rs".into(),
660 line: 1,
661 column: 1,
662 source: "const fn evaluated(value: bool) -> bool".into(),
663 label: None,
664 }],
665 branches: vec![branch],
666 limitations: Vec::new(),
667 scope: None,
668 },
669 hit_obligations_by_ordinal: BTreeMap::from([
670 (101, vec!["function".into()]),
671 (201, vec!["false-alternative".into()]),
672 (202, vec!["true-alternative".into()]),
673 ]),
674 internal_ordinals: BTreeSet::new(),
675 decision_outcome_obligations: BTreeMap::from([(
676 "decision".into(),
677 ("false-alternative".into(), "true-alternative".into()),
678 )]),
679 decision_loop_obligations: BTreeMap::new(),
680 decision_logical_selection_obligations: BTreeMap::new(),
681 }
682 }
683
684 fn mapping(
685 marker: &str,
686 observation_kind: &str,
687 hit_ordinals: &[&str],
688 decision: Option<Value>,
689 ) -> Value {
690 json!({
691 "marker": marker,
692 "definition": "fixture::evaluated",
693 "observationKind": observation_kind,
694 "ordinal": 0,
695 "hitOrdinals": hit_ordinals,
696 "decision": decision,
697 })
698 }
699
700 fn event(marker: &str, observation_kind: &str) -> Value {
701 json!({
702 "crate": "fixture",
703 "kind": "ctfe-marker",
704 "marker": marker,
705 "definition": "fixture::evaluated",
706 "observationKind": observation_kind,
707 "ordinal": 0,
708 "thread": "compiler-thread-1",
709 })
710 }
711
712 fn decision_event(
713 id: &str,
714 event: &str,
715 condition_index: Option<u64>,
716 value: Option<bool>,
717 outcome: Option<bool>,
718 ) -> Value {
719 json!({
720 "id": id,
721 "event": event,
722 "conditionIndex": condition_index,
723 "value": value,
724 "outcome": outcome,
725 })
726 }
727
728 fn valid_map() -> Value {
729 json!({
730 "schema": BUNDLE_SCHEMA,
731 "crate": "fixture",
732 "mappings": [
733 mapping("1", "entry", &["101"], None),
734 mapping("2", "decision-start", &[], Some(decision_event(
735 "decision", "start", None, None, None,
736 ))),
737 mapping("3", "decision-condition", &[], Some(decision_event(
738 "decision", "condition", Some(0), Some(false), None,
739 ))),
740 mapping("4", "decision-finish", &["201"], Some(decision_event(
741 "decision", "finish", None, None, Some(false),
742 ))),
743 mapping("5", "exit", &[], None),
744 mapping("6", "decision-condition", &[], Some(decision_event(
745 "decision", "condition", Some(0), Some(true), None,
746 ))),
747 mapping("7", "decision-finish", &["202"], Some(decision_event(
748 "decision", "finish", None, None, Some(true),
749 ))),
750 ],
751 })
752 }
753
754 fn valid_events() -> Vec<Value> {
755 [
756 ("1", "entry"),
757 ("2", "decision-start"),
758 ("3", "decision-condition"),
759 ("4", "decision-finish"),
760 ("5", "exit"),
761 ("1", "entry"),
762 ("2", "decision-start"),
763 ("6", "decision-condition"),
764 ("7", "decision-finish"),
765 ("5", "exit"),
766 ]
767 .into_iter()
768 .map(|(marker, kind)| event(marker, kind))
769 .collect()
770 }
771
772 fn loop_manifest() -> NormalizedRustCompilerManifest {
773 let mut normalized = normalized_manifest();
774 normalized.manifest.decisions[0].kind = "while".into();
775 normalized.manifest.branches.push(BranchMeta {
776 id: "loop-entry".into(),
777 kind: "loop-entry".into(),
778 file: "src/lib.rs".into(),
779 line: 1,
780 column: 1,
781 source: "while value".into(),
782 alternatives: vec![
783 BranchAlternativeMeta {
784 id: "zero-iterations".into(),
785 label: "zero iterations".into(),
786 },
787 BranchAlternativeMeta {
788 id: "entered".into(),
789 label: "entered".into(),
790 },
791 ],
792 });
793 normalized
794 .hit_obligations_by_ordinal
795 .insert(301, vec!["zero-iterations".into()]);
796 normalized
797 .hit_obligations_by_ordinal
798 .insert(302, vec!["entered".into()]);
799 normalized.decision_loop_obligations.insert(
800 "decision".into(),
801 ("zero-iterations".into(), "entered".into()),
802 );
803 normalized
804 }
805
806 fn loop_map() -> Value {
807 let mut map = valid_map();
808 map["mappings"][3]["hitOrdinals"] = json!(["201", "301"]);
809 map["mappings"][6]["hitOrdinals"] = json!(["202", "302"]);
810 map
811 }
812
813 #[test]
814 fn canonical_unsigned_decimal_rejects_aliases() {
815 assert_eq!(parse_u64("12", "marker").unwrap(), 12);
816 assert!(parse_u64("012", "marker").is_err());
817 assert!(parse_u64("-1", "marker").is_err());
818 assert!(parse_u64("", "marker").is_err());
819 }
820
821 #[test]
822 fn reconstructs_exact_independent_ctfe_vectors_and_outcome_hits() {
823 let scratch = Scratch::new();
824 scratch.write(valid_map(), &valid_events());
825
826 let units = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42).unwrap();
827 assert_eq!(units.len(), 1);
828 assert_eq!(units[0].observations, 10);
829 assert_eq!(
830 units[0].snapshot.hits,
831 ["false-alternative", "function", "true-alternative"]
832 );
833 assert_eq!(units[0].snapshot.decisions.len(), 1);
834 assert_eq!(
835 units[0].snapshot.decisions[0].vectors,
836 [
837 McdcVector {
838 values: vec![Some(false)],
839 outcome: false,
840 },
841 McdcVector {
842 values: vec![Some(true)],
843 outcome: true,
844 },
845 ]
846 );
847 }
848
849 #[test]
850 fn reconstructs_logical_selection_hits_from_ctfe_ternary_vectors() {
851 let scratch = Scratch::new();
852 let mut map = valid_map();
853 map["mappings"].as_array_mut().unwrap().push(mapping(
854 "8",
855 "decision-condition",
856 &[],
857 Some(decision_event(
858 "decision",
859 "condition",
860 Some(1),
861 Some(true),
862 None,
863 )),
864 ));
865 let events = [
866 ("1", "entry"),
867 ("2", "decision-start"),
868 ("3", "decision-condition"),
869 ("4", "decision-finish"),
870 ("2", "decision-start"),
871 ("6", "decision-condition"),
872 ("8", "decision-condition"),
873 ("7", "decision-finish"),
874 ("5", "exit"),
875 ]
876 .into_iter()
877 .map(|(marker, kind)| event(marker, kind))
878 .collect::<Vec<_>>();
879 scratch.write(map, &events);
880
881 let mut normalized = normalized_manifest();
882 normalized.manifest.decisions[0].conditions = vec!["left".into(), "right".into()];
883 normalized.manifest.branches.push(BranchMeta {
884 id: "logical".into(),
885 kind: "logical-selection".into(),
886 file: "src/lib.rs".into(),
887 line: 1,
888 column: 1,
889 source: "left && right".into(),
890 alternatives: vec![
891 BranchAlternativeMeta {
892 id: "short".into(),
893 label: "short-circuited".into(),
894 },
895 BranchAlternativeMeta {
896 id: "evaluated".into(),
897 label: "right operand evaluated".into(),
898 },
899 ],
900 });
901 normalized.decision_logical_selection_obligations.insert(
902 "decision".into(),
903 vec![
904 crate::rust_compiler_manifest::NormalizedRustLogicalSelection {
905 short_circuited_id: "short".into(),
906 right_evaluated_id: "evaluated".into(),
907 right_condition_index: 1,
908 },
909 ],
910 );
911
912 let units = read_rust_compiler_ctfe(&scratch.0, &normalized, 42).unwrap();
913 assert_eq!(
914 units[0].snapshot.hits,
915 [
916 "evaluated",
917 "false-alternative",
918 "function",
919 "short",
920 "true-alternative"
921 ]
922 );
923 assert_eq!(
924 units[0].snapshot.decisions[0].vectors,
925 [
926 McdcVector {
927 values: vec![Some(false), None],
928 outcome: false,
929 },
930 McdcVector {
931 values: vec![Some(true), Some(true)],
932 outcome: true,
933 },
934 ]
935 );
936 }
937
938 #[test]
939 fn commits_only_the_first_loop_entry_outcome_per_ctfe_invocation() {
940 let scratch = Scratch::new();
941 let events = [
942 ("1", "entry"),
943 ("2", "decision-start"),
944 ("6", "decision-condition"),
945 ("7", "decision-finish"),
946 ("2", "decision-start"),
947 ("3", "decision-condition"),
948 ("4", "decision-finish"),
949 ("5", "exit"),
950 ]
951 .into_iter()
952 .map(|(marker, kind)| event(marker, kind))
953 .collect::<Vec<_>>();
954 scratch.write(loop_map(), &events);
955
956 let units = read_rust_compiler_ctfe(&scratch.0, &loop_manifest(), 42).unwrap();
957 assert_eq!(
958 units[0].snapshot.hits,
959 [
960 "entered",
961 "false-alternative",
962 "function",
963 "true-alternative"
964 ]
965 );
966 assert_eq!(
967 units[0].snapshot.decisions[0].vectors,
968 [
969 McdcVector {
970 values: vec![Some(false)],
971 outcome: false,
972 },
973 McdcVector {
974 values: vec![Some(true)],
975 outcome: true,
976 },
977 ]
978 );
979 }
980
981 #[test]
982 fn preserves_a_zero_iteration_loop_outcome() {
983 let scratch = Scratch::new();
984 let events = [
985 ("1", "entry"),
986 ("2", "decision-start"),
987 ("3", "decision-condition"),
988 ("4", "decision-finish"),
989 ("5", "exit"),
990 ]
991 .into_iter()
992 .map(|(marker, kind)| event(marker, kind))
993 .collect::<Vec<_>>();
994 scratch.write(loop_map(), &events);
995
996 let units = read_rust_compiler_ctfe(&scratch.0, &loop_manifest(), 42).unwrap();
997 assert_eq!(
998 units[0].snapshot.hits,
999 ["false-alternative", "function", "zero-iterations"]
1000 );
1001 }
1002
1003 #[test]
1004 fn rejects_semantic_marker_with_unrelated_hit() {
1005 let scratch = Scratch::new();
1006 let mut map = valid_map();
1007 map["mappings"][1]["hitOrdinals"] = json!(["201"]);
1008 scratch.write(map, &valid_events());
1009
1010 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1011 .expect_err("semantic start marker must not carry a coverage hit");
1012 assert!(error.to_string().contains("wrong coverage obligations"));
1013 }
1014
1015 #[test]
1016 fn rejects_finish_mapped_to_the_wrong_outcome_alternative() {
1017 let scratch = Scratch::new();
1018 let mut map = valid_map();
1019 map["mappings"][3]["hitOrdinals"] = json!(["202"]);
1020 scratch.write(map, &valid_events());
1021
1022 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1023 .expect_err("false finish must not map to the true alternative");
1024 assert!(error.to_string().contains("wrong coverage obligations"));
1025 }
1026
1027 #[test]
1028 fn rejects_finish_mapped_to_the_wrong_loop_alternative() {
1029 let scratch = Scratch::new();
1030 let mut map = loop_map();
1031 map["mappings"][3]["hitOrdinals"] = json!(["201", "302"]);
1032 scratch.write(map, &valid_events());
1033
1034 let error = read_rust_compiler_ctfe(&scratch.0, &loop_manifest(), 42)
1035 .expect_err("zero-iteration finish must not map to entered");
1036 assert!(error.to_string().contains("wrong coverage obligations"));
1037 }
1038
1039 #[test]
1040 fn rejects_condition_without_an_active_decision() {
1041 let scratch = Scratch::new();
1042 let mut events = valid_events();
1043 events.remove(1);
1044 scratch.write(valid_map(), &events);
1045
1046 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1047 .expect_err("condition without start must fail closed");
1048 assert!(error.to_string().contains("has no active frame"));
1049 }
1050
1051 #[test]
1052 fn rejects_exit_with_an_incomplete_decision() {
1053 let scratch = Scratch::new();
1054 let events = valid_events()
1055 .into_iter()
1056 .enumerate()
1057 .filter_map(|(index, event)| (index != 3).then_some(event))
1058 .collect::<Vec<_>>();
1059 scratch.write(valid_map(), &events);
1060
1061 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1062 .expect_err("invocation exit with an open decision must fail closed");
1063 assert!(error.to_string().contains("wrong or incomplete invocation"));
1064 }
1065
1066 #[test]
1067 fn rejects_legacy_partial_nonregular_and_truncated_units() {
1068 for (name, bytes, expected) in [
1069 ("ctfe-map-unit.json", b"{}".as_slice(), "unrecognized"),
1070 (".ctfe-unit-unit.partial", b"{}".as_slice(), "unrecognized"),
1071 ("ctfe-unit-unit.json", b"{", "EOF"),
1072 ] {
1073 let scratch = Scratch::new();
1074 scratch.write_raw(name, bytes);
1075 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1076 .expect_err("recognized invalid CTFE artifact must fail closed");
1077 assert!(
1078 error.to_string().contains(expected),
1079 "unexpected {name} error: {error}"
1080 );
1081 }
1082
1083 let scratch = Scratch::new();
1084 fs::create_dir(scratch.0.join("ctfe-unit-unit.json"))
1085 .expect("create nonregular CTFE artifact");
1086 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1087 .expect_err("nonregular CTFE artifact must fail closed");
1088 assert!(error.to_string().contains("not a regular file"));
1089 }
1090
1091 #[test]
1092 fn rejects_unknown_bundle_and_event_fields() {
1093 let scratch = Scratch::new();
1094 let mut map = valid_map();
1095 map["unknown"] = json!(true);
1096 scratch.write(map, &valid_events());
1097 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1098 .expect_err("unknown bundle field must fail closed");
1099 assert!(error.to_string().contains("unknown field"));
1100
1101 let scratch = Scratch::new();
1102 let mut events = valid_events();
1103 events[0]["unknown"] = json!(true);
1104 scratch.write(valid_map(), &events);
1105 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1106 .expect_err("unknown event field must fail closed");
1107 assert!(error.to_string().contains("unknown field"));
1108 }
1109}