Skip to main content

supercov_engine/
rust_compiler_evidence.rs

1//! Fail-closed projection of authenticated rustc transport records into the
2//! shared evidence-v3 runtime model.
3
4use std::collections::{BTreeMap, BTreeSet};
5
6use serde::Serialize;
7
8use crate::{
9    coverage_analysis::McdcVector,
10    coverage_report::{CoveragePhase, DecisionSnapshot, RuntimeEvent, RuntimeSnapshot},
11    rust_compiler_manifest::NormalizedRustCompilerManifest,
12    rust_phase_projection::{RustPhaseProjection, project_rust_assertion_phases},
13    rust_probe_transport::{RustTransportError, RustTransportRead},
14    rust_runtime::RustProbeObservation,
15};
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct RustCompilerTransportHealth {
20    pub committed: u64,
21    pub incomplete: u64,
22    pub dropped: u64,
23    pub attachments: u64,
24}
25
26impl RustCompilerTransportHealth {
27    pub fn is_complete(&self) -> bool {
28        self.incomplete == 0 && self.dropped == 0
29    }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
33#[serde(rename_all = "camelCase")]
34pub struct RustCompilerEvidenceProjection {
35    pub assertion_phases: Vec<CoveragePhase>,
36    pub attributed: RuntimeSnapshot,
37    pub background: RuntimeSnapshot,
38    pub health: RustCompilerTransportHealth,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum RustCompilerEvidenceError {
43    Transport(RustTransportError),
44    UnknownProbe(String),
45    UnknownOrdinal(u64),
46    NonEvidenceOrdinal(u64),
47    InvalidVector {
48        id: String,
49        expected: usize,
50        actual: usize,
51    },
52}
53
54impl std::fmt::Display for RustCompilerEvidenceError {
55    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        match self {
57            Self::Transport(error) => error.fmt(formatter),
58            Self::UnknownProbe(id) => write!(formatter, "unknown Rust compiler probe {id}"),
59            Self::UnknownOrdinal(ordinal) => {
60                write!(formatter, "unknown Rust compiler probe ordinal {ordinal}")
61            }
62            Self::NonEvidenceOrdinal(ordinal) => write!(
63                formatter,
64                "Rust compiler internal ordinal {ordinal} was emitted as coverage evidence"
65            ),
66            Self::InvalidVector {
67                id,
68                expected,
69                actual,
70            } => write!(
71                formatter,
72                "Rust compiler decision {id} expected {expected} conditions but observed {actual}"
73            ),
74        }
75    }
76}
77
78impl std::error::Error for RustCompilerEvidenceError {}
79
80impl From<RustTransportError> for RustCompilerEvidenceError {
81    fn from(error: RustTransportError) -> Self {
82        Self::Transport(error)
83    }
84}
85
86type DecisionVectorKey = (Vec<Option<bool>>, bool);
87
88#[derive(Default)]
89struct SnapshotBuilder {
90    hits: BTreeSet<String>,
91    decisions: BTreeMap<String, BTreeSet<DecisionVectorKey>>,
92    events: Vec<RuntimeEvent>,
93}
94
95impl SnapshotBuilder {
96    fn hit(&mut self, id: &str, phase_id: Option<&str>, timestamp_ms: i64) {
97        self.hits.insert(id.into());
98        self.events.push(RuntimeEvent {
99            event_type: "hit".into(),
100            id: id.into(),
101            vector: None,
102            // The mmap transport intentionally has no wall clock. The phase
103            // envelope time satisfies evidence-v3's required field; explicit
104            // phase identity, never this value, owns causal attribution.
105            timestamp_ms,
106            phase_id: phase_id.map(str::to_owned),
107            statement_id: None,
108            environment: "rust".into(),
109        });
110    }
111
112    fn decision(
113        &mut self,
114        id: &str,
115        vector: McdcVector,
116        phase_id: Option<&str>,
117        timestamp_ms: i64,
118    ) {
119        self.decisions
120            .entry(id.into())
121            .or_default()
122            .insert((vector.values.clone(), vector.outcome));
123        self.events.push(RuntimeEvent {
124            event_type: "decision".into(),
125            id: id.into(),
126            vector: Some(vector),
127            timestamp_ms,
128            phase_id: phase_id.map(str::to_owned),
129            statement_id: None,
130            environment: "rust".into(),
131        });
132    }
133
134    fn finish(
135        self,
136        decisions: &BTreeMap<&str, &crate::coverage_report::DecisionMeta>,
137    ) -> RuntimeSnapshot {
138        RuntimeSnapshot {
139            decisions: self
140                .decisions
141                .into_iter()
142                .map(|(id, vectors)| DecisionSnapshot {
143                    meta: (*decisions[&id.as_str()]).clone(),
144                    vectors: vectors
145                        .into_iter()
146                        .map(|(values, outcome)| McdcVector { values, outcome })
147                        .collect(),
148                })
149                .collect(),
150            hits: self.hits.into_iter().collect(),
151            events: self.events,
152            logicals: Vec::new(),
153            phase_id: None,
154        }
155    }
156}
157
158fn builder_and_phase<'builder, 'phase>(
159    context_id: u64,
160    base_context_id: u64,
161    base_phase_id: &'phase str,
162    phases: &'phase RustPhaseProjection,
163    attributed: &'builder mut SnapshotBuilder,
164    background: &'builder mut SnapshotBuilder,
165) -> Result<(&'builder mut SnapshotBuilder, Option<&'phase str>), RustCompilerEvidenceError> {
166    if context_id == 0 {
167        return Ok((background, None));
168    }
169    let phase_id = phases.phase_id_for_context(base_context_id, base_phase_id, context_id)?;
170    Ok((attributed, phase_id))
171}
172
173/// Project one supervisor-owned transport partition. The caller may supply a
174/// complete isolated-process transport or one exact-test partition from a
175/// shared stock-libtest transport. Context zero is preserved separately as
176/// background evidence and must not be inserted into an ultimately-passing
177/// test result by the caller.
178pub fn project_rust_compiler_evidence(
179    base_context_id: u64,
180    base_phase: &CoveragePhase,
181    read: &RustTransportRead,
182    normalized: &NormalizedRustCompilerManifest,
183) -> Result<RustCompilerEvidenceProjection, RustCompilerEvidenceError> {
184    let phases =
185        project_rust_assertion_phases(base_context_id, base_phase, read, &normalized.manifest)?;
186    let points_and_alternatives = normalized
187        .manifest
188        .points
189        .iter()
190        .map(|point| point.id.as_str())
191        .chain(normalized.manifest.branches.iter().flat_map(|branch| {
192            branch
193                .alternatives
194                .iter()
195                .map(|alternative| alternative.id.as_str())
196        }))
197        .collect::<BTreeSet<_>>();
198    let decisions = normalized
199        .manifest
200        .decisions
201        .iter()
202        .map(|decision| (decision.id.as_str(), decision))
203        .collect::<BTreeMap<_, _>>();
204    let mut attributed = SnapshotBuilder::default();
205    let mut background = SnapshotBuilder::default();
206
207    for record in &read.observations {
208        let (builder, phase_id) = builder_and_phase(
209            record.context_id,
210            base_context_id,
211            &base_phase.id,
212            &phases,
213            &mut attributed,
214            &mut background,
215        )?;
216        match &record.observation {
217            // Assertion markers come from the owned runtime only.
218            RustProbeObservation::Assertion { .. } => continue,
219            RustProbeObservation::Hit { id, .. } => {
220                if !points_and_alternatives.contains(id.as_str()) {
221                    return Err(RustCompilerEvidenceError::UnknownProbe(id.clone()));
222                }
223                builder.hit(id, phase_id, base_phase.started_at_ms);
224            }
225            RustProbeObservation::Decision {
226                id,
227                values,
228                outcome,
229                ..
230            } => {
231                let Some(meta) = decisions.get(id.as_str()) else {
232                    return Err(RustCompilerEvidenceError::UnknownProbe(id.clone()));
233                };
234                if values.len() != meta.conditions.len() {
235                    return Err(RustCompilerEvidenceError::InvalidVector {
236                        id: id.clone(),
237                        expected: meta.conditions.len(),
238                        actual: values.len(),
239                    });
240                }
241                if let Some(selections) = normalized.decision_logical_selection_obligations.get(id)
242                {
243                    for selection in selections {
244                        let alternative_id = if values[selection.right_condition_index].is_some() {
245                            &selection.right_evaluated_id
246                        } else {
247                            &selection.short_circuited_id
248                        };
249                        builder.hit(alternative_id, phase_id, base_phase.started_at_ms);
250                    }
251                }
252                builder.decision(
253                    id,
254                    McdcVector {
255                        values: values.clone(),
256                        outcome: *outcome,
257                    },
258                    phase_id,
259                    base_phase.started_at_ms,
260                );
261            }
262        }
263    }
264    for record in &read.ordinal_hits {
265        let (builder, phase_id) = builder_and_phase(
266            record.context_id,
267            base_context_id,
268            &base_phase.id,
269            &phases,
270            &mut attributed,
271            &mut background,
272        )?;
273        if normalized.internal_ordinals.contains(&record.ordinal) {
274            return Err(RustCompilerEvidenceError::NonEvidenceOrdinal(
275                record.ordinal,
276            ));
277        }
278        let Some(ids) = normalized.hit_obligations_by_ordinal.get(&record.ordinal) else {
279            return Err(RustCompilerEvidenceError::UnknownOrdinal(record.ordinal));
280        };
281        for id in ids {
282            builder.hit(id, phase_id, base_phase.started_at_ms);
283        }
284    }
285
286    Ok(RustCompilerEvidenceProjection {
287        assertion_phases: phases.phases,
288        attributed: attributed.finish(&decisions),
289        background: background.finish(&decisions),
290        health: RustCompilerTransportHealth {
291            committed: read.committed,
292            incomplete: read.incomplete,
293            dropped: read.dropped,
294            attachments: read.attachments,
295        },
296    })
297}
298
299#[cfg(test)]
300mod tests {
301    use crate::{
302        coverage_analysis::PointKind,
303        coverage_report::{
304            BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
305        },
306        rust_compiler_manifest::NormalizedRustCompilerManifest,
307        rust_probe_transport::{
308            RustOrdinalHit, RustPhaseContext, RustTransportObservation, RustTransportRead,
309            rust_assertion_context_id,
310        },
311    };
312
313    use super::*;
314
315    const BASE: u64 = 42;
316    const ASSERTION: &str = "rs:decision:0123456789abcdef01234567";
317
318    fn normalized() -> NormalizedRustCompilerManifest {
319        NormalizedRustCompilerManifest {
320            manifest: CoverageManifest {
321                unmeasured: Vec::new(),
322                decisions: vec![DecisionMeta {
323                    id: ASSERTION.into(),
324                    file: "src/lib.rs".into(),
325                    line: 4,
326                    column: 4,
327                    source: "assert!(value)".into(),
328                    conditions: vec!["value".into()],
329                    kind: "assertion".into(),
330                }],
331                points: vec![PointMeta {
332                    id: "rs:statement:111111111111111111111111".into(),
333                    kind: PointKind::Statement,
334                    file: "src/lib.rs".into(),
335                    line: 2,
336                    column: 4,
337                    source: "work();".into(),
338                    label: None,
339                }],
340                branches: vec![BranchMeta {
341                    id: "rs:branch:222222222222222222222222".into(),
342                    kind: "match-arm".into(),
343                    file: "src/lib.rs".into(),
344                    line: 3,
345                    column: 4,
346                    source: "first => work()".into(),
347                    alternatives: vec![
348                        BranchAlternativeMeta {
349                            id: "rs:branch-alternative:333333333333333333333333".into(),
350                            label: "selected".into(),
351                        },
352                        BranchAlternativeMeta {
353                            id: "rs:branch-alternative:444444444444444444444444".into(),
354                            label: "not selected".into(),
355                        },
356                    ],
357                }],
358                limitations: Vec::new(),
359                scope: None,
360            },
361            hit_obligations_by_ordinal: BTreeMap::from([
362                (10, vec!["rs:statement:111111111111111111111111".into()]),
363                (
364                    20,
365                    vec![
366                        "rs:branch-alternative:333333333333333333333333".into(),
367                        "rs:branch-alternative:444444444444444444444444".into(),
368                    ],
369                ),
370            ]),
371            internal_ordinals: BTreeSet::from([100]),
372            decision_outcome_obligations: BTreeMap::new(),
373            decision_loop_obligations: BTreeMap::new(),
374            decision_logical_selection_obligations: BTreeMap::new(),
375        }
376    }
377
378    fn base_phase() -> CoveragePhase {
379        CoveragePhase {
380            id: "test-phase".into(),
381            kind: "test".into(),
382            operation: "libtest test".into(),
383            source: Some("src/lib.rs".into()),
384            caused_by_phase_id: None,
385            started_at_ms: 10,
386            ended_at_ms: Some(20),
387            status: Some("passed".into()),
388            error: None,
389        }
390    }
391
392    #[test]
393    fn projects_exact_contexts_ordinals_background_and_health() {
394        let assertion = rust_assertion_context_id(BASE, ASSERTION, 0).unwrap();
395        let read = RustTransportRead {
396            observations: vec![RustTransportObservation {
397                process_id: 1,
398                context_id: assertion,
399                observation: RustProbeObservation::Decision {
400                    id: ASSERTION.into(),
401                    values: vec![Some(true)],
402                    outcome: true,
403                },
404            }],
405            ordinal_hits: vec![
406                RustOrdinalHit {
407                    process_id: 1,
408                    context_id: BASE,
409                    ordinal: 10,
410                },
411                RustOrdinalHit {
412                    process_id: 1,
413                    context_id: assertion,
414                    ordinal: 20,
415                },
416                RustOrdinalHit {
417                    process_id: 1,
418                    context_id: 0,
419                    ordinal: 10,
420                },
421            ],
422            phases: vec![RustPhaseContext {
423                process_id: 1,
424                child_context_id: assertion,
425                parent_context_id: BASE,
426                invocation_nonce: 0,
427                decision_id: ASSERTION.into(),
428            }],
429            committed: 5,
430            incomplete: 1,
431            dropped: 2,
432            attachments: 1,
433            ..RustTransportRead::empty()
434        };
435        let projection =
436            project_rust_compiler_evidence(BASE, &base_phase(), &read, &normalized()).unwrap();
437        assert_eq!(projection.assertion_phases.len(), 1);
438        assert_eq!(
439            projection.assertion_phases[0].status.as_deref(),
440            Some("passed")
441        );
442        assert_eq!(projection.attributed.hits.len(), 3);
443        assert_eq!(projection.background.hits.len(), 1);
444        assert_eq!(projection.attributed.decisions.len(), 1);
445        assert!(
446            projection
447                .attributed
448                .events
449                .iter()
450                .filter(|event| event.id.contains("branch-alternative"))
451                .all(|event| event.phase_id == Some(projection.assertion_phases[0].id.clone()))
452        );
453        assert!(!projection.health.is_complete());
454    }
455
456    #[test]
457    fn rejects_unknown_ordinals_and_vector_widths() {
458        let mut read = RustTransportRead {
459            observations: Vec::new(),
460            ordinal_hits: vec![RustOrdinalHit {
461                process_id: 1,
462                context_id: BASE,
463                ordinal: 999,
464            }],
465            phases: Vec::new(),
466            committed: 1,
467            incomplete: 0,
468            dropped: 0,
469            attachments: 1,
470            ..RustTransportRead::empty()
471        };
472        assert!(matches!(
473            project_rust_compiler_evidence(BASE, &base_phase(), &read, &normalized()),
474            Err(RustCompilerEvidenceError::UnknownOrdinal(999))
475        ));
476        read.ordinal_hits[0].ordinal = 100;
477        assert!(matches!(
478            project_rust_compiler_evidence(BASE, &base_phase(), &read, &normalized()),
479            Err(RustCompilerEvidenceError::NonEvidenceOrdinal(100))
480        ));
481        read.ordinal_hits.clear();
482        read.observations.push(RustTransportObservation {
483            process_id: 1,
484            context_id: BASE,
485            observation: RustProbeObservation::Decision {
486                id: ASSERTION.into(),
487                values: vec![Some(true), Some(false)],
488                outcome: false,
489            },
490        });
491        assert!(matches!(
492            project_rust_compiler_evidence(BASE, &base_phase(), &read, &normalized()),
493            Err(RustCompilerEvidenceError::InvalidVector {
494                expected: 1,
495                actual: 2,
496                ..
497            })
498        ));
499    }
500
501    #[test]
502    fn projects_logical_selection_hits_from_ternary_vectors_without_ordinals() {
503        let mut normalized = normalized();
504        normalized.manifest.decisions[0].conditions = vec!["left".into(), "right".into()];
505        normalized.manifest.branches.push(BranchMeta {
506            id: "logical".into(),
507            kind: "logical-selection".into(),
508            file: "src/lib.rs".into(),
509            line: 4,
510            column: 4,
511            source: "left && right".into(),
512            alternatives: vec![
513                BranchAlternativeMeta {
514                    id: "short".into(),
515                    label: "short-circuited".into(),
516                },
517                BranchAlternativeMeta {
518                    id: "evaluated".into(),
519                    label: "right operand evaluated".into(),
520                },
521            ],
522        });
523        normalized.decision_logical_selection_obligations.insert(
524            ASSERTION.into(),
525            vec![
526                crate::rust_compiler_manifest::NormalizedRustLogicalSelection {
527                    short_circuited_id: "short".into(),
528                    right_evaluated_id: "evaluated".into(),
529                    right_condition_index: 1,
530                },
531            ],
532        );
533        let read = RustTransportRead {
534            observations: vec![
535                RustTransportObservation {
536                    process_id: 1,
537                    context_id: BASE,
538                    observation: RustProbeObservation::Decision {
539                        id: ASSERTION.into(),
540                        values: vec![Some(false), None],
541                        outcome: false,
542                    },
543                },
544                RustTransportObservation {
545                    process_id: 1,
546                    context_id: BASE,
547                    observation: RustProbeObservation::Decision {
548                        id: ASSERTION.into(),
549                        values: vec![Some(true), Some(true)],
550                        outcome: true,
551                    },
552                },
553            ],
554            ordinal_hits: Vec::new(),
555            phases: Vec::new(),
556            committed: 2,
557            incomplete: 0,
558            dropped: 0,
559            attachments: 1,
560            ..RustTransportRead::empty()
561        };
562
563        let projection =
564            project_rust_compiler_evidence(BASE, &base_phase(), &read, &normalized).unwrap();
565        assert_eq!(
566            projection.attributed.hits,
567            vec!["evaluated".to_string(), "short".to_string()]
568        );
569        assert_eq!(
570            projection
571                .attributed
572                .events
573                .iter()
574                .filter(|event| event.event_type == "hit")
575                .map(|event| event.id.as_str())
576                .collect::<Vec<_>>(),
577            vec!["short", "evaluated"]
578        );
579    }
580}