use serde::{Deserialize, Serialize};
use crate::entity::{Entity, EntityId};
use crate::relation::{Relation, RelationView};
use crate::schema::{Availability, AvailabilityMatrix, Perturbation, SensorReport};
use crate::state::{ChannelId, ChannelSpec, StateMatrix};
pub const FORMAT_VERSION: u32 = 2;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MirrorSnapshot {
pub format_version: u32,
pub epoch: u64,
pub sequence: u64,
pub monotonic_ns: u64,
pub realtime_ns: u64,
pub entities: Vec<Entity>,
pub channels: Vec<ChannelSpec>,
pub relations: Vec<Relation>,
pub state: StateMatrix,
pub availability: AvailabilityMatrix,
pub sensors: Vec<SensorReport>,
}
impl MirrorSnapshot {
pub fn row_of(&self, id: EntityId) -> Option<u32> {
self.entities
.iter()
.position(|e| e.id == id)
.map(|i| i as u32)
}
pub fn row_of_key(&self, key: &str) -> Option<u32> {
self.entities
.iter()
.position(|e| e.key == key)
.map(|i| i as u32)
}
pub fn channel(&self, key: &str) -> Option<ChannelId> {
self.channels.iter().find(|c| c.key == key).map(|c| c.id)
}
pub fn value(&self, row: u32, channel: ChannelId) -> Option<f64> {
let (row, col) = (row as usize, channel.index());
if row >= self.state.rows() || col >= self.state.cols() {
return None;
}
let v = self.state.get(row, col);
if v.is_nan() {
None
} else {
Some(v)
}
}
pub fn lookup(&self, entity_key: &str, channel_key: &str) -> Option<f64> {
let row = self.row_of_key(entity_key)?;
let channel = self.channel(channel_key)?;
self.value(row, channel)
}
pub fn availability(&self, row: u32, channel: ChannelId) -> Availability {
self.availability.get(row as usize, channel.index())
}
pub fn privilege_gaps(&self) -> usize {
self.availability
.tally()
.iter()
.filter(|(availability, _)| availability.is_privilege_problem())
.map(|(_, count)| *count)
.sum()
}
pub fn relations(&self) -> RelationView<'_> {
RelationView::new(&self.relations)
}
pub fn observation_cost_ns(&self) -> u64 {
self.sensors.iter().map(|s| s.last_cost_ns).sum()
}
pub fn worst_perturbation(&self) -> Perturbation {
self.sensors
.iter()
.filter(|s| !s.inactive)
.map(|s| s.perturbation)
.max()
.unwrap_or(Perturbation::None)
}
pub fn coverage(&self) -> f64 {
let cells = self.state.rows() * self.state.cols();
if cells == 0 {
return 0.0;
}
self.state.observed_cells() as f64 / cells as f64
}
pub fn rows_of_class(&self, class: crate::entity::EntityClass) -> Vec<u32> {
self.entities
.iter()
.enumerate()
.filter(|(_, e)| e.class_hint == class)
.map(|(i, _)| i as u32)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entity::EntityClass;
use crate::relation::RelationKind;
use crate::test_support::fixture;
#[test]
fn entities_are_addressable_by_stable_id_and_by_key() {
let s = fixture();
let id = EntityId::derive("cpu/1");
assert_eq!(s.row_of(id), Some(3));
assert_eq!(s.row_of_key("cpu/1"), Some(3));
assert_eq!(s.row_of_key("cpu/99"), None);
}
#[test]
fn unobserved_cells_are_none_not_zero() {
let s = fixture();
assert_eq!(
s.lookup("cpu/0", "cpu.frequency.current"),
Some(3_600_000.0)
);
assert_eq!(s.lookup("cpu/1", "cpu.time.idle"), None);
}
#[test]
fn out_of_range_access_is_none_rather_than_a_panic() {
let s = fixture();
assert_eq!(s.value(99, ChannelId(0)), None);
assert_eq!(s.value(0, ChannelId(99)), None);
}
#[test]
fn the_snapshot_reports_what_it_cost_to_produce() {
let s = fixture();
assert_eq!(s.observation_cost_ns(), 57_000);
assert_eq!(
s.worst_perturbation(),
Perturbation::Low,
"the worst sensor sets the snapshot's perturbation"
);
}
#[test]
fn inactive_sensors_do_not_contribute_perturbation() {
let mut s = fixture();
s.sensors[0].inactive = true;
assert_eq!(s.worst_perturbation(), Perturbation::Negligible);
}
#[test]
fn coverage_describes_how_complete_this_reflection_is() {
let s = fixture();
assert!((s.coverage() - 3.0 / 8.0).abs() < 1e-9);
}
#[test]
fn class_hints_are_available_but_not_required() {
let s = fixture();
assert_eq!(s.rows_of_class(EntityClass::LogicalCpu), vec![2, 3]);
let view = s.relations();
assert_eq!(view.of_kind(RelationKind::Contains).count(), 3);
}
#[test]
fn snapshot_round_trips_through_json() {
let s = fixture();
let json = serde_json::to_string(&s).unwrap();
let back: MirrorSnapshot = serde_json::from_str(&json).unwrap();
assert_eq!(s.entities, back.entities);
assert_eq!(s.relations, back.relations);
assert_eq!(s.epoch, back.epoch);
assert_eq!(s.sequence, back.sequence);
}
}