Skip to main content

supercov_engine/
rust_compiler_ctfe.rs

1//! Strict reconstruction of compiler-owned constant-evaluation evidence.
2//!
3//! rustc's interpreter step events expose no stable parent span. The exact
4//! companion therefore inserts entry/exit markers and records the compiler
5//! thread. This module reconstructs a nested invocation stack per compiler
6//! process and thread before any observation is allowed to cover an
7//! obligation.
8
9use 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}