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 statement_id: None,
476 environment: "rust-ctfe".into(),
477 });
478 }
479 }
480 runtime_events.push(RuntimeEvent {
481 event_type: "decision".into(),
482 id: decision.id.clone(),
483 vector: Some(McdcVector {
484 values: active.values,
485 outcome,
486 }),
487 timestamp_ms,
488 phase_id: None,
489 statement_id: None,
490 environment: "rust-ctfe".into(),
491 });
492 }
493 _ => unreachable!("validated decision event"),
494 }
495 }
496 }
497 "exit" => {
498 let Some(invocation) = stack.pop() else {
499 return Err(RustCompilerCtfeError::Invalid(format!(
500 "CTFE marker {marker} closed an absent invocation on {}",
501 event.thread
502 )));
503 };
504 if invocation.definition != event.definition || !invocation.decisions.is_empty() {
505 return Err(RustCompilerCtfeError::Invalid(format!(
506 "CTFE marker {marker} closed the wrong or incomplete invocation on {}",
507 event.thread
508 )));
509 }
510 }
511 _ => unreachable!("validated observation kind"),
512 }
513 for ordinal in &mapping.hit_ordinals {
514 let ordinal = parse_u64(ordinal, "CTFE hit ordinal")?;
515 for id in &normalized.hit_obligations_by_ordinal[&ordinal] {
516 if ignored_hits.contains(id) {
517 continue;
518 }
519 hits.insert(id.clone());
520 runtime_events.push(RuntimeEvent {
521 event_type: "hit".into(),
522 id: id.clone(),
523 vector: None,
524 timestamp_ms,
525 phase_id: None,
526 statement_id: None,
527 environment: "rust-ctfe".into(),
528 });
529 }
530 }
531 }
532 if let Some((thread, stack)) = stacks.iter().find(|(_, stack)| !stack.is_empty()) {
533 return Err(RustCompilerCtfeError::Invalid(format!(
534 "successful compiler unit {identity} left {} CTFE frame(s) open on {thread}",
535 stack.len()
536 )));
537 }
538 Ok(RustCompilerCtfeUnit {
539 identity,
540 crate_name: bundle.crate_name,
541 snapshot: RuntimeSnapshot {
542 decisions: decision_vectors
543 .into_iter()
544 .map(|(id, vectors)| DecisionSnapshot {
545 meta: decisions[id.as_str()].clone(),
546 vectors: vectors
547 .into_iter()
548 .map(|(values, outcome)| McdcVector { values, outcome })
549 .collect(),
550 })
551 .collect(),
552 hits: hits.into_iter().collect(),
553 events: runtime_events,
554 logicals: Vec::new(),
555 phase_id: None,
556 },
557 observations: events.len(),
558 })
559}
560
561pub fn read_rust_compiler_ctfe(
562 directory: &Path,
563 normalized: &NormalizedRustCompilerManifest,
564 timestamp_ms: i64,
565) -> Result<Vec<RustCompilerCtfeUnit>, RustCompilerCtfeError> {
566 ctfe_files(directory)?
567 .into_iter()
568 .map(|(identity, bundle)| reconstruct_unit(identity, &bundle, normalized, timestamp_ms))
569 .collect()
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575 use std::{
576 sync::atomic::{AtomicU64, Ordering},
577 time::{SystemTime, UNIX_EPOCH},
578 };
579
580 use serde_json::{Value, json};
581
582 use crate::{
583 coverage_analysis::PointKind,
584 coverage_report::{
585 BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
586 },
587 };
588
589 static SCRATCH_NONCE: AtomicU64 = AtomicU64::new(0);
590
591 struct Scratch(PathBuf);
592
593 impl Scratch {
594 fn new() -> Self {
595 let epoch = SystemTime::now()
596 .duration_since(UNIX_EPOCH)
597 .expect("clock before epoch")
598 .as_nanos();
599 let path = std::env::temp_dir().join(format!(
600 "supercov-rust-ctfe-{}-{epoch}-{}",
601 std::process::id(),
602 SCRATCH_NONCE.fetch_add(1, Ordering::Relaxed)
603 ));
604 fs::create_dir_all(&path).expect("create CTFE scratch directory");
605 Self(path)
606 }
607
608 fn write(&self, map: Value, events: &[Value]) {
609 let mut bundle = map;
610 bundle["events"] = Value::Array(events.to_vec());
611 fs::write(
612 self.0.join("ctfe-unit-unit.json"),
613 serde_json::to_vec(&bundle).expect("serialize CTFE bundle"),
614 )
615 .expect("write CTFE bundle");
616 }
617
618 fn write_raw(&self, name: &str, bytes: &[u8]) {
619 fs::write(self.0.join(name), bytes).expect("write raw CTFE artifact");
620 }
621 }
622
623 impl Drop for Scratch {
624 fn drop(&mut self) {
625 let _ = fs::remove_dir_all(&self.0);
626 }
627 }
628
629 fn normalized_manifest() -> NormalizedRustCompilerManifest {
630 let decision = DecisionMeta {
631 id: "decision".into(),
632 file: "src/lib.rs".into(),
633 line: 1,
634 column: 1,
635 source: "value".into(),
636 conditions: vec!["value".into()],
637 kind: "control".into(),
638 };
639 let branch = BranchMeta {
640 id: "outcome".into(),
641 kind: "decision-outcome".into(),
642 file: "src/lib.rs".into(),
643 line: 1,
644 column: 1,
645 source: "value".into(),
646 alternatives: vec![
647 BranchAlternativeMeta {
648 id: "false-alternative".into(),
649 label: "condition false".into(),
650 },
651 BranchAlternativeMeta {
652 id: "true-alternative".into(),
653 label: "condition true".into(),
654 },
655 ],
656 };
657 NormalizedRustCompilerManifest {
658 manifest: CoverageManifest {
659 unmeasured: Vec::new(),
660 decisions: vec![decision],
661 points: vec![PointMeta {
662 id: "function".into(),
663 kind: PointKind::Function,
664 file: "src/lib.rs".into(),
665 line: 1,
666 column: 1,
667 source: "const fn evaluated(value: bool) -> bool".into(),
668 label: None,
669 }],
670 branches: vec![branch],
671 limitations: Vec::new(),
672 scope: None,
673 },
674 hit_obligations_by_ordinal: BTreeMap::from([
675 (101, vec!["function".into()]),
676 (201, vec!["false-alternative".into()]),
677 (202, vec!["true-alternative".into()]),
678 ]),
679 internal_ordinals: BTreeSet::new(),
680 decision_outcome_obligations: BTreeMap::from([(
681 "decision".into(),
682 ("false-alternative".into(), "true-alternative".into()),
683 )]),
684 decision_loop_obligations: BTreeMap::new(),
685 decision_logical_selection_obligations: BTreeMap::new(),
686 }
687 }
688
689 fn mapping(
690 marker: &str,
691 observation_kind: &str,
692 hit_ordinals: &[&str],
693 decision: Option<Value>,
694 ) -> Value {
695 json!({
696 "marker": marker,
697 "definition": "fixture::evaluated",
698 "observationKind": observation_kind,
699 "ordinal": 0,
700 "hitOrdinals": hit_ordinals,
701 "decision": decision,
702 })
703 }
704
705 fn event(marker: &str, observation_kind: &str) -> Value {
706 json!({
707 "crate": "fixture",
708 "kind": "ctfe-marker",
709 "marker": marker,
710 "definition": "fixture::evaluated",
711 "observationKind": observation_kind,
712 "ordinal": 0,
713 "thread": "compiler-thread-1",
714 })
715 }
716
717 fn decision_event(
718 id: &str,
719 event: &str,
720 condition_index: Option<u64>,
721 value: Option<bool>,
722 outcome: Option<bool>,
723 ) -> Value {
724 json!({
725 "id": id,
726 "event": event,
727 "conditionIndex": condition_index,
728 "value": value,
729 "outcome": outcome,
730 })
731 }
732
733 fn valid_map() -> Value {
734 json!({
735 "schema": BUNDLE_SCHEMA,
736 "crate": "fixture",
737 "mappings": [
738 mapping("1", "entry", &["101"], None),
739 mapping("2", "decision-start", &[], Some(decision_event(
740 "decision", "start", None, None, None,
741 ))),
742 mapping("3", "decision-condition", &[], Some(decision_event(
743 "decision", "condition", Some(0), Some(false), None,
744 ))),
745 mapping("4", "decision-finish", &["201"], Some(decision_event(
746 "decision", "finish", None, None, Some(false),
747 ))),
748 mapping("5", "exit", &[], None),
749 mapping("6", "decision-condition", &[], Some(decision_event(
750 "decision", "condition", Some(0), Some(true), None,
751 ))),
752 mapping("7", "decision-finish", &["202"], Some(decision_event(
753 "decision", "finish", None, None, Some(true),
754 ))),
755 ],
756 })
757 }
758
759 fn valid_events() -> Vec<Value> {
760 [
761 ("1", "entry"),
762 ("2", "decision-start"),
763 ("3", "decision-condition"),
764 ("4", "decision-finish"),
765 ("5", "exit"),
766 ("1", "entry"),
767 ("2", "decision-start"),
768 ("6", "decision-condition"),
769 ("7", "decision-finish"),
770 ("5", "exit"),
771 ]
772 .into_iter()
773 .map(|(marker, kind)| event(marker, kind))
774 .collect()
775 }
776
777 fn loop_manifest() -> NormalizedRustCompilerManifest {
778 let mut normalized = normalized_manifest();
779 normalized.manifest.decisions[0].kind = "while".into();
780 normalized.manifest.branches.push(BranchMeta {
781 id: "loop-entry".into(),
782 kind: "loop-entry".into(),
783 file: "src/lib.rs".into(),
784 line: 1,
785 column: 1,
786 source: "while value".into(),
787 alternatives: vec![
788 BranchAlternativeMeta {
789 id: "zero-iterations".into(),
790 label: "zero iterations".into(),
791 },
792 BranchAlternativeMeta {
793 id: "entered".into(),
794 label: "entered".into(),
795 },
796 ],
797 });
798 normalized
799 .hit_obligations_by_ordinal
800 .insert(301, vec!["zero-iterations".into()]);
801 normalized
802 .hit_obligations_by_ordinal
803 .insert(302, vec!["entered".into()]);
804 normalized.decision_loop_obligations.insert(
805 "decision".into(),
806 ("zero-iterations".into(), "entered".into()),
807 );
808 normalized
809 }
810
811 fn loop_map() -> Value {
812 let mut map = valid_map();
813 map["mappings"][3]["hitOrdinals"] = json!(["201", "301"]);
814 map["mappings"][6]["hitOrdinals"] = json!(["202", "302"]);
815 map
816 }
817
818 #[test]
819 fn canonical_unsigned_decimal_rejects_aliases() {
820 assert_eq!(parse_u64("12", "marker").unwrap(), 12);
821 assert!(parse_u64("012", "marker").is_err());
822 assert!(parse_u64("-1", "marker").is_err());
823 assert!(parse_u64("", "marker").is_err());
824 }
825
826 #[test]
827 fn reconstructs_exact_independent_ctfe_vectors_and_outcome_hits() {
828 let scratch = Scratch::new();
829 scratch.write(valid_map(), &valid_events());
830
831 let units = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42).unwrap();
832 assert_eq!(units.len(), 1);
833 assert_eq!(units[0].observations, 10);
834 assert_eq!(
835 units[0].snapshot.hits,
836 ["false-alternative", "function", "true-alternative"]
837 );
838 assert_eq!(units[0].snapshot.decisions.len(), 1);
839 assert_eq!(
840 units[0].snapshot.decisions[0].vectors,
841 [
842 McdcVector {
843 values: vec![Some(false)],
844 outcome: false,
845 },
846 McdcVector {
847 values: vec![Some(true)],
848 outcome: true,
849 },
850 ]
851 );
852 }
853
854 #[test]
855 fn reconstructs_logical_selection_hits_from_ctfe_ternary_vectors() {
856 let scratch = Scratch::new();
857 let mut map = valid_map();
858 map["mappings"].as_array_mut().unwrap().push(mapping(
859 "8",
860 "decision-condition",
861 &[],
862 Some(decision_event(
863 "decision",
864 "condition",
865 Some(1),
866 Some(true),
867 None,
868 )),
869 ));
870 let events = [
871 ("1", "entry"),
872 ("2", "decision-start"),
873 ("3", "decision-condition"),
874 ("4", "decision-finish"),
875 ("2", "decision-start"),
876 ("6", "decision-condition"),
877 ("8", "decision-condition"),
878 ("7", "decision-finish"),
879 ("5", "exit"),
880 ]
881 .into_iter()
882 .map(|(marker, kind)| event(marker, kind))
883 .collect::<Vec<_>>();
884 scratch.write(map, &events);
885
886 let mut normalized = normalized_manifest();
887 normalized.manifest.decisions[0].conditions = vec!["left".into(), "right".into()];
888 normalized.manifest.branches.push(BranchMeta {
889 id: "logical".into(),
890 kind: "logical-selection".into(),
891 file: "src/lib.rs".into(),
892 line: 1,
893 column: 1,
894 source: "left && right".into(),
895 alternatives: vec![
896 BranchAlternativeMeta {
897 id: "short".into(),
898 label: "short-circuited".into(),
899 },
900 BranchAlternativeMeta {
901 id: "evaluated".into(),
902 label: "right operand evaluated".into(),
903 },
904 ],
905 });
906 normalized.decision_logical_selection_obligations.insert(
907 "decision".into(),
908 vec![
909 crate::rust_compiler_manifest::NormalizedRustLogicalSelection {
910 short_circuited_id: "short".into(),
911 right_evaluated_id: "evaluated".into(),
912 right_condition_index: 1,
913 },
914 ],
915 );
916
917 let units = read_rust_compiler_ctfe(&scratch.0, &normalized, 42).unwrap();
918 assert_eq!(
919 units[0].snapshot.hits,
920 [
921 "evaluated",
922 "false-alternative",
923 "function",
924 "short",
925 "true-alternative"
926 ]
927 );
928 assert_eq!(
929 units[0].snapshot.decisions[0].vectors,
930 [
931 McdcVector {
932 values: vec![Some(false), None],
933 outcome: false,
934 },
935 McdcVector {
936 values: vec![Some(true), Some(true)],
937 outcome: true,
938 },
939 ]
940 );
941 }
942
943 #[test]
944 fn commits_only_the_first_loop_entry_outcome_per_ctfe_invocation() {
945 let scratch = Scratch::new();
946 let events = [
947 ("1", "entry"),
948 ("2", "decision-start"),
949 ("6", "decision-condition"),
950 ("7", "decision-finish"),
951 ("2", "decision-start"),
952 ("3", "decision-condition"),
953 ("4", "decision-finish"),
954 ("5", "exit"),
955 ]
956 .into_iter()
957 .map(|(marker, kind)| event(marker, kind))
958 .collect::<Vec<_>>();
959 scratch.write(loop_map(), &events);
960
961 let units = read_rust_compiler_ctfe(&scratch.0, &loop_manifest(), 42).unwrap();
962 assert_eq!(
963 units[0].snapshot.hits,
964 [
965 "entered",
966 "false-alternative",
967 "function",
968 "true-alternative"
969 ]
970 );
971 assert_eq!(
972 units[0].snapshot.decisions[0].vectors,
973 [
974 McdcVector {
975 values: vec![Some(false)],
976 outcome: false,
977 },
978 McdcVector {
979 values: vec![Some(true)],
980 outcome: true,
981 },
982 ]
983 );
984 }
985
986 #[test]
987 fn preserves_a_zero_iteration_loop_outcome() {
988 let scratch = Scratch::new();
989 let events = [
990 ("1", "entry"),
991 ("2", "decision-start"),
992 ("3", "decision-condition"),
993 ("4", "decision-finish"),
994 ("5", "exit"),
995 ]
996 .into_iter()
997 .map(|(marker, kind)| event(marker, kind))
998 .collect::<Vec<_>>();
999 scratch.write(loop_map(), &events);
1000
1001 let units = read_rust_compiler_ctfe(&scratch.0, &loop_manifest(), 42).unwrap();
1002 assert_eq!(
1003 units[0].snapshot.hits,
1004 ["false-alternative", "function", "zero-iterations"]
1005 );
1006 }
1007
1008 #[test]
1009 fn rejects_semantic_marker_with_unrelated_hit() {
1010 let scratch = Scratch::new();
1011 let mut map = valid_map();
1012 map["mappings"][1]["hitOrdinals"] = json!(["201"]);
1013 scratch.write(map, &valid_events());
1014
1015 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1016 .expect_err("semantic start marker must not carry a coverage hit");
1017 assert!(error.to_string().contains("wrong coverage obligations"));
1018 }
1019
1020 #[test]
1021 fn rejects_finish_mapped_to_the_wrong_outcome_alternative() {
1022 let scratch = Scratch::new();
1023 let mut map = valid_map();
1024 map["mappings"][3]["hitOrdinals"] = json!(["202"]);
1025 scratch.write(map, &valid_events());
1026
1027 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1028 .expect_err("false finish must not map to the true alternative");
1029 assert!(error.to_string().contains("wrong coverage obligations"));
1030 }
1031
1032 #[test]
1033 fn rejects_finish_mapped_to_the_wrong_loop_alternative() {
1034 let scratch = Scratch::new();
1035 let mut map = loop_map();
1036 map["mappings"][3]["hitOrdinals"] = json!(["201", "302"]);
1037 scratch.write(map, &valid_events());
1038
1039 let error = read_rust_compiler_ctfe(&scratch.0, &loop_manifest(), 42)
1040 .expect_err("zero-iteration finish must not map to entered");
1041 assert!(error.to_string().contains("wrong coverage obligations"));
1042 }
1043
1044 #[test]
1045 fn rejects_condition_without_an_active_decision() {
1046 let scratch = Scratch::new();
1047 let mut events = valid_events();
1048 events.remove(1);
1049 scratch.write(valid_map(), &events);
1050
1051 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1052 .expect_err("condition without start must fail closed");
1053 assert!(error.to_string().contains("has no active frame"));
1054 }
1055
1056 #[test]
1057 fn rejects_exit_with_an_incomplete_decision() {
1058 let scratch = Scratch::new();
1059 let events = valid_events()
1060 .into_iter()
1061 .enumerate()
1062 .filter_map(|(index, event)| (index != 3).then_some(event))
1063 .collect::<Vec<_>>();
1064 scratch.write(valid_map(), &events);
1065
1066 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1067 .expect_err("invocation exit with an open decision must fail closed");
1068 assert!(error.to_string().contains("wrong or incomplete invocation"));
1069 }
1070
1071 #[test]
1072 fn rejects_legacy_partial_nonregular_and_truncated_units() {
1073 for (name, bytes, expected) in [
1074 ("ctfe-map-unit.json", b"{}".as_slice(), "unrecognized"),
1075 (".ctfe-unit-unit.partial", b"{}".as_slice(), "unrecognized"),
1076 ("ctfe-unit-unit.json", b"{", "EOF"),
1077 ] {
1078 let scratch = Scratch::new();
1079 scratch.write_raw(name, bytes);
1080 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1081 .expect_err("recognized invalid CTFE artifact must fail closed");
1082 assert!(
1083 error.to_string().contains(expected),
1084 "unexpected {name} error: {error}"
1085 );
1086 }
1087
1088 let scratch = Scratch::new();
1089 fs::create_dir(scratch.0.join("ctfe-unit-unit.json"))
1090 .expect("create nonregular CTFE artifact");
1091 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1092 .expect_err("nonregular CTFE artifact must fail closed");
1093 assert!(error.to_string().contains("not a regular file"));
1094 }
1095
1096 #[test]
1097 fn rejects_unknown_bundle_and_event_fields() {
1098 let scratch = Scratch::new();
1099 let mut map = valid_map();
1100 map["unknown"] = json!(true);
1101 scratch.write(map, &valid_events());
1102 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1103 .expect_err("unknown bundle field must fail closed");
1104 assert!(error.to_string().contains("unknown field"));
1105
1106 let scratch = Scratch::new();
1107 let mut events = valid_events();
1108 events[0]["unknown"] = json!(true);
1109 scratch.write(valid_map(), &events);
1110 let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1111 .expect_err("unknown event field must fail closed");
1112 assert!(error.to_string().contains("unknown field"));
1113 }
1114}