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