crates/gunnar-front/tests/clock_both_dialects.rs
//! **A Gunnar front answers an old box and a new box** (lane L10, 2026-10-21).
//!
//! Gunnar's estates move from its own gunnar-clock onto edda estate-clock over
//! one release, front first: for a while the front runs this build and its
//! boxes still run the release before, which asks in `GUNNAR-TIME/1`. This is
//! the front as the medium's PID 1 starts it (`gunnar_front::clock::start`,
//! the offer it hands the installer), on a loopback "utility" address, asked
//! by:
//!
//! * the OLD box: gunnar-clock's own `query`, verbatim
//! (`clock_both_dialects/gunnar_clock_peer.rs`), pinning the offered key;
//! * the NEW box: estate-clock's `peer::query`, as gunnar-server asks.
//!
//! Its own test binary: it sets `GUNNAR_CLOCK_TRUST_SYSTEM` (the kit's Gunnar
//! spelling, read as the alias of `ESTATE_CLOCK_TRUST_SYSTEM`) for the process,
//! so the front signs at once without asking the internet for the time.
#[allow(dead_code)]
#[path = "clock_both_dialects/gunnar_clock_peer.rs"]
mod gunnar_clock;
use std::net::{Ipv4Addr, SocketAddr};
use std::time::{Duration, Instant};
use estate_clock::config::DEFAULT_PEER_PORT;
use estate_clock::peer::{self, Dialect};
/// Ask until the front's listener is up (it binds on its own thread).
fn asked<T>(what: &str, mut ask: impl FnMut() -> Result<T, String>) -> T {
let t0 = Instant::now();
loop {
match ask() {
Ok(v) => return v,
Err(why)
if t0.elapsed() < Duration::from_secs(20)
&& why.contains("did not accept a connection") =>
{
std::thread::sleep(Duration::from_millis(100))
}
Err(why) => panic!("{what}: {why}"),
}
}
}
#[test]
fn a_gunnar_front_answers_an_old_gunnar_time_box_and_a_new_box() {
// Set before any thread of this test binary reads the environment (one test).
std::env::remove_var("ESTATE_CLOCK");
std::env::remove_var("GUNNAR_CLOCK");
std::env::remove_var("ESTATE_CLOCK_TRUST_SYSTEM");
std::env::set_var("GUNNAR_CLOCK_TRUST_SYSTEM", "1");
let root = tempfile::tempdir().unwrap();
// A loopback address of this process's own, so a parallel run does not meet it.
let pid = std::process::id();
let utility = Ipv4Addr::new(127, 77, (pid >> 8) as u8, (pid as u8).max(2));
let offer = gunnar_front::clock::start(root.path(), Some(utility), "")
.expect("the front's clock starts");
assert_eq!(
offer.peer.as_deref(),
Some(format!("peer://{utility}:{DEFAULT_PEER_PORT}").as_str())
);
let rows = offer.rows();
assert_eq!(rows[gunnar_front::clock::OFFER_CLOCK_KEY], offer.key_line);
let addr = SocketAddr::from((utility, DEFAULT_PEER_PORT));
// The OLD box, gunnar-clock verbatim: GUNNAR-TIME/1, gunnar-time-v1@gunnar.rs.
let old_key = gunnar_clock::PeerKey::parse(&offer.key_line).unwrap();
let old = asked("the old GUNNAR-TIME/1 box", || {
gunnar_clock::query(addr, &old_key)
});
assert!(
old.offset_ms.abs() < 5_000,
"the old box read the front's time: {old:?}"
);
// The NEW box, as gunnar-server asks (ESTATE-TIME/1 first).
let key = peer::PeerKey::parse(&offer.key_line).unwrap();
let new = asked("the new box", || peer::query(addr, &key));
assert!(
new.offset_ms.abs() < 5_000,
"the new box read the front's time: {new:?}"
);
// Each dialect alone is answered in itself.
for d in Dialect::ALL {
let s = asked(&format!("{d:?}"), || peer::query_in(d, addr, &key));
assert!(s.offset_ms.abs() < 5_000, "{d:?}: {s:?}");
}
// The front's offer is what a Gunnar box parses as its clock.
let c = estate_clock::config::Config::parse(
offer.peer.as_deref(),
Some(&offer.key_line),
estate_clock::config::Role::Appliance,
)
.unwrap();
assert!(
matches!(
c,
estate_clock::config::Config::Sources {
peer_key: Some(_),
..
}
),
"{c:?}"
);
}