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