facett-simview/src/mock.rs
//! **An in-memory stand-in for the server side of the contract** — a population with
//! persons in municipalities and their records, Generate and Permutate as jobs that are
//! busy for a few polls and then done, and a log of every request it was asked. The
//! tests drive the app against it; the native window and the dev server serve it.
//!
//! The model is a SHAPE, not knut's generator: persons with a `kommun`, each owning a
//! handful of records of the population's other labels, and a permutation that moves
//! 10% of the persons to another `kommun` and stamps them with the new round. What the
//! viewer is tested on - counts, the lit round, the ego network - is all it needs.
use std::sync::{Arc, Mutex};
use crate::backend::Backend;
use crate::contract::{self, EdgeRow, Meta, NodeRow, Req, Resp};
/// Twenty municipalities - the 3D view clusters persons by these.
pub const KOMMUNER: [&str; 20] = [
"Stockholm", "Göteborg", "Malmö", "Uppsala", "Linköping", "Örebro", "Västerås", "Helsingborg", "Norrköping",
"Jönköping", "Umeå", "Lund", "Borås", "Huddinge", "Eskilstuna", "Nacka", "Gävle", "Södertälje", "Halmstad",
"Karlstad",
];
/// The other labels a person owns one of (the shape of knut's model).
const RECORDS: [(&str, &str); 4] =
[("Address", "BOR_PA"), ("TaxRecord", "DEKLARERAR"), ("Vehicle", "AGER_FORDON"), ("Loan", "HAR_LAN")];
/// The mock's whole state.
#[derive(Debug, Clone)]
pub struct MockServer {
pub persons: usize,
pub round: i64,
nodes: Vec<NodeRow>,
edges: Vec<EdgeRow>,
/// A running job: (verb, polls left before it is done).
job: Option<(&'static str, u32)>,
last: Option<(String, bool, String)>,
/// Polls a job stays busy for.
pub job_polls: u32,
/// Every request, in order - what a test asserts on.
pub log: Vec<Req>,
}
impl MockServer {
/// A generated population of `persons` persons, round 0.
pub fn new(persons: usize) -> Self {
let mut s =
MockServer { persons, round: 0, nodes: Vec::new(), edges: Vec::new(), job: None, last: None, job_polls: 2, log: Vec::new() };
s.generate();
s
}
fn generate(&mut self) {
self.round = 0;
self.nodes.clear();
self.edges.clear();
// Persons take ids 0..persons, their records the ids after.
for p in 0..self.persons {
self.nodes.push(NodeRow {
id: p as i64,
label: "Person".into(),
name: format!("person {p}"),
kommun: Some(KOMMUNER[p % KOMMUNER.len()].into()),
round: 0,
});
}
let mut next = self.persons as i64;
for p in 0..self.persons {
for (label, etype) in RECORDS {
self.nodes.push(NodeRow { id: next, label: label.into(), name: format!("{label} {next}"), kommun: None, round: 0 });
self.edges.push(EdgeRow { src: p as i64, dst: next, etype: etype.into() });
next += 1;
}
}
}
/// 10% of the persons move to the next municipality and carry the new round.
fn permutate(&mut self) {
self.round += 1;
let r = self.round;
let step = 10usize;
let offset = (r as usize) % step;
for p in (offset..self.persons).step_by(step) {
let n = &mut self.nodes[p];
let k = KOMMUNER.iter().position(|k| Some(*k) == n.kommun.as_deref()).unwrap_or(0);
n.kommun = Some(KOMMUNER[(k + 1) % KOMMUNER.len()].into());
n.round = r;
}
}
/// Persons stamped with the latest round.
pub fn changed_in_latest_round(&self) -> usize {
self.nodes.iter().filter(|n| n.label == "Person" && n.round == self.round).count()
}
/// `GET /sim/api/nodes.arrow?labels=..&limit=..` (empty `labels` = every label).
pub fn nodes_arrow(&self, labels: &[&str], limit: usize) -> Vec<u8> {
let rows: Vec<NodeRow> =
self.nodes.iter().filter(|n| labels.is_empty() || labels.contains(&n.label.as_str())).take(limit).cloned().collect();
contract::encode_nodes(&rows)
}
/// `GET /sim/api/edges.arrow?types=..&limit=..` (empty `types` = every type).
pub fn edges_arrow(&self, types: &[&str], limit: usize) -> Vec<u8> {
let rows: Vec<EdgeRow> =
self.edges.iter().filter(|e| types.is_empty() || types.contains(&e.etype.as_str())).take(limit).cloned().collect();
contract::encode_edges(&rows)
}
fn power_json(&self) -> String {
let last = self.last.as_ref().map(|(v, ok, d)| serde_json::json!({"verb": v, "ok": ok, "ago_s": 0, "detail": d}));
let entry = match &self.job {
Some((verb, _)) => serde_json::json!({"id": contract::TARGET, "busy": true, "verb": verb, "for_s": 1, "last": last}),
None => serde_json::json!({"id": contract::TARGET, "busy": false, "last": last}),
};
serde_json::json!({"benches": [entry]}).to_string()
}
/// Answer one request, as the server would.
pub fn answer(&mut self, req: &Req) -> (u16, Vec<u8>) {
self.log.push(req.clone());
match req {
Req::Press(verb) => {
if self.job.is_some() {
return (409, b"a job is already running for falkordb".to_vec());
}
self.job = Some((verb, self.job_polls));
(202, serde_json::json!({"accepted": true, "id": contract::TARGET, "verb": verb}).to_string().into_bytes())
}
Req::Power => {
if let Some((verb, left)) = self.job {
if left == 0 {
if verb == contract::VERB_PERMUTATE {
self.permutate();
} else {
self.generate();
}
self.job = None;
self.last = Some((verb.into(), true, format!("{verb} falkordb: done")));
} else {
self.job = Some((verb, left - 1));
}
}
(200, self.power_json().into_bytes())
}
Req::Meta => {
let persons = self.persons as i64;
let m = Meta { round: self.round, persons, nodes: self.nodes.len() as i64, edges: self.edges.len() as i64 };
(200, m.to_json().into_bytes())
}
Req::Persons => {
let rows: Vec<NodeRow> =
self.nodes.iter().filter(|n| n.label == "Person").take(contract::NODE_LIMIT).cloned().collect();
(200, contract::encode_nodes(&rows))
}
Req::Ego(id) => {
let around: std::collections::BTreeSet<i64> = self
.edges
.iter()
.filter(|e| e.src == *id || e.dst == *id)
.flat_map(|e| [e.src, e.dst])
.chain(std::iter::once(*id))
.collect();
let rows: Vec<EdgeRow> =
self.edges.iter().filter(|e| around.contains(&e.src) && around.contains(&e.dst)).cloned().collect();
(200, contract::encode_edges(&rows))
}
Req::NodesByIds(ids) => {
let want: std::collections::BTreeSet<&i64> = ids.iter().collect();
let rows: Vec<NodeRow> = self.nodes.iter().filter(|n| want.contains(&n.id)).cloned().collect();
(200, contract::encode_nodes(&rows))
}
}
}
}
/// The mock as a [`Backend`]: every request is answered at once and handed back on the
/// next [`drain`](Backend::drain), the way a fast network would.
#[derive(Clone)]
pub struct MockBackend {
pub server: Arc<Mutex<MockServer>>,
inbox: Arc<Mutex<Vec<Resp>>>,
}
impl MockBackend {
pub fn new(server: MockServer) -> Self {
Self { server: Arc::new(Mutex::new(server)), inbox: Arc::new(Mutex::new(Vec::new())) }
}
/// Every request the app made, in order.
pub fn log(&self) -> Vec<Req> {
self.server.lock().unwrap().log.clone()
}
}
impl Backend for MockBackend {
fn send(&self, req: Req) {
let (status, body) = self.server.lock().unwrap().answer(&req);
self.inbox.lock().unwrap().push(Resp { req, status, body });
}
fn drain(&self) -> Vec<Resp> {
std::mem::take(&mut *self.inbox.lock().unwrap())
}
}