#[cfg(any(test, verus))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResourceState {
pub desired_hash: String,
pub current_hash: Option<String>,
pub converged: bool,
}
#[cfg(any(test, verus))]
#[derive(Debug, Clone)]
pub struct SystemState {
pub resources: Vec<ResourceState>,
pub iteration: usize,
}
#[cfg(any(test, verus))]
pub fn observe(state: &SystemState) -> Vec<Option<String>> {
state
.resources
.iter()
.map(|r| r.current_hash.clone())
.collect()
}
#[cfg(any(test, verus))]
pub fn diff(state: &SystemState) -> Vec<bool> {
state
.resources
.iter()
.map(|r| r.current_hash.as_ref() != Some(&r.desired_hash))
.collect()
}
#[cfg(any(test, verus))]
pub fn apply(state: &mut SystemState) {
for r in &mut state.resources {
if r.current_hash.as_ref() != Some(&r.desired_hash) {
r.current_hash = Some(r.desired_hash.clone());
r.converged = true;
}
}
state.iteration += 1;
}
#[cfg(any(test, verus))]
pub fn is_converged(state: &SystemState) -> bool {
state.resources.iter().all(|r| r.converged)
}
#[cfg(any(test, verus))]
pub fn changes_needed(state: &SystemState) -> usize {
diff(state).iter().filter(|&&b| b).count()
}
#[cfg(any(test, verus))]
pub fn reconcile(state: &mut SystemState) -> usize {
let max_iterations = state.resources.len() + 1;
for _ in 0..max_iterations {
if changes_needed(state) == 0 {
return state.iteration;
}
apply(state);
}
state.iteration
}
#[cfg(any(test, verus))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlannerState {
pub desired_hash: String,
pub stored_hash: Option<String>,
pub converged: bool,
}
#[cfg(any(test, verus))]
impl PlannerState {
pub fn handler_invariant_holds(&self) -> bool {
self.stored_hash.as_ref() == Some(&self.desired_hash)
}
pub fn needs_apply(&self) -> bool {
!self.converged || !self.handler_invariant_holds()
}
pub fn apply(&mut self) {
self.stored_hash = Some(self.desired_hash.clone());
self.converged = true;
}
}
#[cfg(any(test, verus))]
#[derive(Debug, Clone)]
pub struct FleetPlannerState {
pub resources: Vec<PlannerState>,
}
#[cfg(any(test, verus))]
impl FleetPlannerState {
pub fn pending_count(&self) -> usize {
self.resources.iter().filter(|r| r.needs_apply()).count()
}
pub fn apply_all(&mut self) -> usize {
let mut count = 0;
for r in &mut self.resources {
if r.needs_apply() {
r.apply();
count += 1;
}
}
count
}
pub fn is_converged(&self) -> bool {
self.resources.iter().all(|r| !r.needs_apply())
}
}
#[cfg(verus)]
mod verus_specs {
use super::*;
#[requires(true)]
#[ensures(|result: Vec<Option<String>>| result.len() == state.resources.len())]
fn spec_observe(state: &SystemState) -> Vec<Option<String>> {
observe(state)
}
#[requires(true)]
#[ensures(|result: Vec<bool>| result.len() == state.resources.len())]
fn spec_diff(state: &SystemState) -> Vec<bool> {
diff(state)
}
#[requires(true)]
#[ensures(|_| state.iteration > old(state.iteration))]
fn spec_apply(state: &mut SystemState) {
apply(state)
}
#[requires(true)]
#[ensures(|result: bool| result == state.resources.iter().all(|r| r.converged))]
fn spec_is_converged(state: &SystemState) -> bool {
is_converged(state)
}
#[requires(true)]
#[ensures(|result: usize| result <= state.resources.len())]
fn spec_changes_needed(state: &SystemState) -> usize {
changes_needed(state)
}
#[requires(state.resources.len() > 0)]
#[ensures(|_| is_converged(state))]
#[ensures(|result: usize| result <= state.resources.len() + 1)]
fn spec_reconcile(state: &mut SystemState) -> usize {
reconcile(state)
}
#[requires(is_converged(state))]
#[ensures(|result: usize| result == 0)]
fn spec_idempotency(state: &SystemState) -> usize {
changes_needed(state)
}
#[requires(state.resources[idx].converged)]
#[ensures(|_| state.resources[idx].converged)]
fn spec_monotonicity(state: &mut SystemState, idx: usize) {
apply(state)
}
#[requires(state.resources.len() > 0)]
#[ensures(|result: usize| result <= state.resources.len() + 1)]
#[decreases(state.resources.len())]
fn spec_bounded_reconcile(state: &mut SystemState) -> usize {
reconcile(state)
}
#[requires(true)]
#[ensures(|_| state == old(state))]
fn spec_observe_pure(state: &SystemState) {
let _ = observe(state);
}
}
#[cfg(verus)]
mod verus_proofs {
use super::*;
verus! {
proof fn proof_termination(state: &SystemState)
ensures
reconcile(state).iteration <= state.resources.len() + 1,
{
}
proof fn proof_convergence(state: &SystemState)
ensures
forall |i: usize| i < state.resources.len() ==>
state.resources[i].converged,
{
}
proof fn proof_idempotency(state: &SystemState)
requires
is_converged(state),
ensures
changes_needed(state) == 0,
{
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_state(hashes: &[(&str, Option<&str>)]) -> SystemState {
SystemState {
resources: hashes
.iter()
.map(|(desired, current)| ResourceState {
desired_hash: desired.to_string(),
current_hash: current.map(|s| s.to_string()),
converged: current == &Some(*desired),
})
.collect(),
iteration: 0,
}
}
#[test]
fn test_observe() {
let state = make_state(&[("abc", Some("abc")), ("def", None)]);
let observed = observe(&state);
assert_eq!(observed, vec![Some("abc".to_string()), None]);
}
#[test]
fn test_diff_no_changes() {
let state = make_state(&[("abc", Some("abc"))]);
assert_eq!(diff(&state), vec![false]);
}
#[test]
fn test_diff_with_changes() {
let state = make_state(&[("abc", Some("old")), ("def", None)]);
assert_eq!(diff(&state), vec![true, true]);
}
#[test]
fn test_apply_converges() {
let mut state = make_state(&[("abc", Some("old")), ("def", None)]);
apply(&mut state);
assert!(is_converged(&state));
assert_eq!(changes_needed(&state), 0);
}
#[test]
fn test_idempotency_no_changes_on_converged() {
let mut state = make_state(&[("abc", Some("abc"))]);
let before = state.iteration;
reconcile(&mut state);
assert_eq!(state.iteration, before);
}
#[test]
fn test_reconcile_terminates() {
let mut state = make_state(&[("h1", None), ("h2", Some("old")), ("h3", Some("h3"))]);
let iters = reconcile(&mut state);
assert!(iters <= state.resources.len() + 1);
assert!(is_converged(&state));
}
#[test]
fn test_monotonicity() {
let mut state = make_state(&[("abc", Some("abc")), ("def", None)]);
assert!(state.resources[0].converged);
apply(&mut state);
assert!(state.resources[0].converged);
assert!(state.resources[1].converged);
}
#[test]
fn test_changes_needed_count() {
let state = make_state(&[("a", None), ("b", Some("b")), ("c", Some("old"))]);
assert_eq!(changes_needed(&state), 2); }
#[test]
fn test_planner_state_handler_invariant() {
let mut ps = PlannerState {
desired_hash: "abc".into(),
stored_hash: None,
converged: false,
};
assert!(!ps.handler_invariant_holds());
assert!(ps.needs_apply());
ps.apply();
assert!(ps.handler_invariant_holds());
assert!(!ps.needs_apply());
}
#[test]
fn test_planner_state_idempotency() {
let mut ps = PlannerState {
desired_hash: "hash1".into(),
stored_hash: Some("hash1".into()),
converged: true,
};
assert!(!ps.needs_apply());
ps.apply(); assert!(!ps.needs_apply());
}
#[test]
fn test_fleet_convergence() {
let mut fleet = FleetPlannerState {
resources: vec![
PlannerState {
desired_hash: "a".into(),
stored_hash: None,
converged: false,
},
PlannerState {
desired_hash: "b".into(),
stored_hash: Some("old".into()),
converged: true,
},
],
};
assert_eq!(fleet.pending_count(), 2);
assert!(!fleet.is_converged());
let applied = fleet.apply_all();
assert_eq!(applied, 2);
assert!(fleet.is_converged());
assert_eq!(fleet.pending_count(), 0);
let applied2 = fleet.apply_all();
assert_eq!(applied2, 0);
}
}