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 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
173pub 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 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}