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