Skip to main content

rig_core/test_utils/
observations.rs

1//! Helpers for comparing deterministic observation test fixtures.
2//! These checks are not an execution-equivalence contract.
3
4use crate::observe::{Action, Observation, ObservationTrace, Reason};
5use serde::{Deserialize, Serialize};
6
7/// Where two traces first disagree on a semantic field.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct Divergence {
10    /// The index of the first observation that differs (or the length of
11    /// the shorter trace when one is a prefix of the other).
12    pub index: usize,
13    /// The expected fact, if any.
14    pub expected: Option<Observation>,
15    /// The actual fact, if any.
16    pub actual: Option<Observation>,
17}
18
19/// The result of comparing two traces.
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21#[serde(tag = "comparison", rename_all = "snake_case")]
22pub enum Comparison {
23    /// Every semantic field agrees.
24    Equal,
25    /// The traces diverge here.
26    Diverged(Divergence),
27    /// The traces cannot be compared: one is incomplete.
28    Incomparable {
29        /// Why.
30        reason: Reason,
31    },
32}
33
34/// Compare two traces on their semantic fields: subject, stage, emitter,
35/// action and sequence. Adapter analysis, measurements ([`Observation::at`]) and the session
36/// name are ignored. An incomplete trace (dropped facts) is incomparable,
37/// never equal: a missing fact is not evidence of agreement.
38pub fn compare(expected: &ObservationTrace, actual: &ObservationTrace) -> Comparison {
39    if !expected.is_complete() {
40        return Comparison::Incomparable {
41            reason: Reason::with_detail(
42                "incomplete_expected",
43                format!("the expected trace dropped {} facts", expected.dropped),
44            ),
45        };
46    }
47    if !actual.is_complete() {
48        return Comparison::Incomparable {
49            reason: Reason::with_detail(
50                "incomplete_actual",
51                format!("the actual trace dropped {} facts", actual.dropped),
52            ),
53        };
54    }
55    let semantic = |observation: &Observation| {
56        let mut observation = observation.clone();
57        observation.at = None;
58        if let Action::Adapter { observation } = &mut observation.action {
59            observation.analysis = None;
60        }
61        observation
62    };
63    let longest = expected.observations.len().max(actual.observations.len());
64    for index in 0..longest {
65        let left = expected.observations.get(index).map(semantic);
66        let right = actual.observations.get(index).map(semantic);
67        if left != right {
68            return Comparison::Diverged(Divergence {
69                index,
70                expected: left,
71                actual: right,
72            });
73        }
74    }
75    Comparison::Equal
76}