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