use std::num::NonZeroU32;
use super::records::{
Carrier, Connection, Device, DeviceKind, Hop, Line, LineKind, LineLife, LineState, Link,
LinkCounters, LinkState, Mount, Outcome, Station, StationKind, StationState, TapBatch,
TapMessage, TopologyDocument, canonical_bytes,
};
type TestResult = Result<(), Box<dyn std::error::Error>>;
const TOPOLOGY_GOLDEN: &[u8] = include_bytes!("../../tests/fixtures/console-records/topology.json");
const TAP_BATCH_GOLDEN: &[u8] =
include_bytes!("../../tests/fixtures/console-records/tap-batch.json");
const TOPOLOGY_CLOCK_FAULT_GOLDEN: &[u8] =
include_bytes!("../../tests/fixtures/console-records/topology-clock-fault.json");
fn device(id: &str, liminal: Option<&str>, started: Option<u64>) -> Device {
Device {
id: id.to_owned(),
name: id.to_owned(),
kind: DeviceKind::Device,
liminal: liminal.map(str::to_owned),
started,
started_fault: None,
parent: None,
zone: None,
}
}
fn topology_clock_fault() -> TopologyDocument {
let mut document = topology();
if let Some(this) = document.devices.first_mut() {
this.started = None;
this.started_fault =
Some("console clock: a time is outside u64 milliseconds since the epoch".to_owned());
}
document
}
fn worker(id: &str) -> Station {
Station {
id: id.to_owned(),
name: id.to_owned(),
device: "local".to_owned(),
kind: StationKind::Worker,
state: StationState::Live,
subjects: None,
activities: None,
}
}
fn topology() -> TopologyDocument {
TopologyDocument::new(
3,
"local".to_owned(),
vec![
device("local", Some("1.2.3"), Some(1_791_268_400_000)),
device("edge", None, None),
],
vec![
Station {
id: "local".to_owned(),
name: "local".to_owned(),
device: "local".to_owned(),
kind: StationKind::Exchange,
state: StationState::Live,
subjects: None,
activities: None,
},
Station {
id: "local.depot".to_owned(),
name: "local.depot".to_owned(),
device: "local".to_owned(),
kind: StationKind::Depot,
state: StationState::Live,
subjects: None,
activities: None,
},
Station {
id: "orders.reply".to_owned(),
name: "orders.reply".to_owned(),
device: "local".to_owned(),
kind: StationKind::Service,
state: StationState::Live,
subjects: None,
activities: None,
},
worker("orders.worker"),
worker("orders.ledger"),
worker("orders.audit"),
],
vec![
Line {
id: "orders".to_owned(),
kind: LineKind::Stream,
life: LineLife::Kept,
state: LineState::Live,
stops: [
"orders.worker",
"local.depot",
"local",
"orders.ledger",
"orders.audit",
]
.map(str::to_owned)
.to_vec(),
},
Line {
id: "audit".to_owned(),
kind: LineKind::Stream,
life: LineLife::Passing,
state: LineState::Quiesced,
stops: vec!["local".to_owned()],
},
],
vec![Link::to_peer("local", "edge")],
vec![Connection {
id: "7".to_owned(),
mount: Mount::Websocket,
participant: Some("alice".to_owned()),
}],
)
}
fn message(
id: &str,
size: u64,
outcome: Outcome,
t: u64,
reply_to: Option<&str>,
from_station: Option<&str>,
) -> TapMessage {
TapMessage {
id: id.to_owned(),
subject: "orders".to_owned(),
size,
outcome,
t,
from: "7".to_owned(),
from_station: from_station.map(str::to_owned),
to: "local".to_owned(),
to_stations: from_station.map(|_| vec!["orders.ledger".to_owned()]),
hops: Vec::new(),
reply_to: reply_to.map(str::to_owned),
held: false,
diverted: None,
fields: None,
}
}
fn hops(chain: &[(&str, u64)]) -> Vec<Hop> {
chain
.iter()
.map(|(node, at_ms)| Hop {
node: (*node).to_owned(),
at_ms: *at_ms,
})
.collect()
}
fn tap_batch() -> Result<TapBatch, Box<dyn std::error::Error>> {
Ok(TapBatch::new(
"orders".to_owned(),
7,
NonZeroU32::new(4).ok_or("4 is not zero")?,
2,
vec![
TapMessage {
to_stations: Some(vec!["orders.ledger".to_owned(), "orders.audit".to_owned()]),
hops: hops(&[
("orders.worker", 0),
("local.depot", 2),
("local", 2),
("orders.ledger", 4),
("orders.audit", 6),
]),
..message(
"local:41",
12,
Outcome::Delivered,
1_791_272_000_123,
Some("q-1"),
Some("orders.worker"),
)
},
TapMessage {
to_stations: Some(vec![]),
..message(
"local:x9",
3,
Outcome::Refused,
1_791_272_000_456,
None,
None,
)
},
TapMessage {
from: "liminal.local".to_owned(),
to_stations: Some(vec![]),
hops: hops(&[("local.depot", 1), ("local", 1)]),
..message(
"local:42",
5,
Outcome::Delivered,
1_791_272_000_789,
None,
None,
)
},
message(
"local:43",
4,
Outcome::Delivered,
1_791_272_000_901,
None,
None,
),
],
))
}
fn normalized(bytes: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let normalizer = liminal::channel::Schema::new(serde_json::json!({}))?;
Ok(normalizer.validate_and_apply_defaults(bytes)?)
}
#[test]
fn console_records_match_their_golden_bytes() -> TestResult {
for (name, encoded, golden) in [
(
"topology",
canonical_bytes(&topology(), "topology")?,
TOPOLOGY_GOLDEN,
),
(
"tap-batch",
canonical_bytes(&tap_batch()?, "tap.batch")?,
TAP_BATCH_GOLDEN,
),
(
"topology-clock-fault",
canonical_bytes(&topology_clock_fault(), "topology")?,
TOPOLOGY_CLOCK_FAULT_GOLDEN,
),
] {
let delivered = normalized(&encoded)?;
assert_eq!(
delivered,
golden,
"{name}: delivered bytes differ from the golden file; delivered:\n{}",
String::from_utf8_lossy(&delivered)
);
assert_eq!(encoded, delivered, "{name}: the encoder is not canonical");
}
Ok(())
}
#[test]
fn console_records_carry_the_console_spellings() -> TestResult {
let topology: serde_json::Value = serde_json::from_slice(TOPOLOGY_GOLDEN)?;
assert_eq!(topology["v"], 1);
assert_eq!(topology["type"], "topology");
assert_eq!(
topology["links"][0]["ends"],
serde_json::json!(["edge", "local"])
);
assert!(topology["links"][0]["rtt_ms"].is_null());
assert!(topology["links"][0].get("refusals").is_none());
assert!(topology.get("proposals").is_none());
assert!(topology["stations"][0].get("sub").is_none());
assert!(topology["stations"][0].get("product").is_none());
assert!(topology["devices"][1].get("started").is_none());
assert!(topology["devices"][1].get("liminal").is_none());
let batch: serde_json::Value = serde_json::from_slice(TAP_BATCH_GOLDEN)?;
assert_eq!(batch["v"], 1);
assert_eq!(batch["type"], "tap.batch");
assert_eq!(batch["sampled"], 4);
assert_eq!(batch["messages"][0]["reply_to"], "q-1");
assert!(batch["messages"][1].get("reply_to").is_none());
assert!(batch["messages"][1].get("diverted").is_none());
assert_eq!(batch["messages"][0]["hops"][0]["at_ms"], 0);
Ok(())
}
#[test]
fn link_ends_are_sorted_from_either_end() {
let from_local = Link::to_peer("local", "edge");
let from_edge = Link::to_peer("edge", "local");
assert_eq!(from_local.ends, ["edge".to_owned(), "local".to_owned()]);
assert_eq!(from_local.ends, from_edge.ends);
}
#[test]
fn each_server_outcome_has_its_console_word() -> TestResult {
use crate::tap::TapOutcome;
for (outcome, word) in [
(TapOutcome::Delivered, "delivered"),
(TapOutcome::NoSubscriber, "nobody"),
(TapOutcome::NoneAccepted, "unaccepted"),
(TapOutcome::Unresolved, "unresolved"),
(TapOutcome::Refused, "refused"),
] {
assert_eq!(serde_json::to_value(Outcome::from(outcome))?, word);
}
Ok(())
}
#[test]
fn a_tap_batch_becomes_its_record() -> TestResult {
use std::sync::Arc;
use std::time::{Duration, Instant};
use super::records::tap_batch_record;
use super::wall::WallBase;
use crate::tap::{self, TapIdent, TapOutcome, TapSource};
let base = Instant::now();
let wall = WallBase::fixed(1_791_272_000_000, base);
let message =
|ident, source, station: Option<&str>, outcome, after_ms, reply_to: Option<&str>| {
tap::TapMessage {
ident,
subject: "orders".to_owned(),
size: 5,
outcome,
seen_at: base + Duration::from_millis(after_ms),
source,
station: station.map(Arc::from),
to_stations: station.map(|_| {
Arc::from([tap::Reached {
station: Arc::from("orders.ledger"),
queued_at: base + Duration::from_millis(after_ms),
}])
}),
hops: vec![tap::Hop {
node: Arc::from("local"),
at_ms: 0,
}],
reply_to: reply_to.map(str::to_owned),
held: false,
call: None,
payload: Some(b"never on the wire".to_vec()),
}
};
let batch = tap::TapBatch {
seq: 3,
sampled: NonZeroU32::new(2).ok_or("2 is not zero")?,
dropped: 1,
messages: vec![
message(
TapIdent::Message(41),
TapSource::Connection(7),
Some("orders.worker"),
TapOutcome::Delivered,
123,
Some("q-1"),
),
tap::TapMessage {
to_stations: Some(Arc::from(Vec::new())),
..message(
TapIdent::Observation(9),
TapSource::Server,
None,
TapOutcome::Refused,
456,
None,
)
},
],
};
let record = tap_batch_record("local", "orders", &batch, &wall)?;
let bytes = canonical_bytes(&record, "tap.batch")?;
let value: serde_json::Value = serde_json::from_slice(&bytes)?;
assert_eq!(value["type"], "tap.batch");
assert_eq!(value["target"], "orders");
assert_eq!(
(
value["seq"].as_u64(),
value["sampled"].as_u64(),
value["dropped"].as_u64()
),
(Some(3), Some(2), Some(1))
);
let first = &value["messages"][0];
assert_eq!(first["id"], "local:41");
assert_eq!(first["from"], "7");
assert_eq!(first["from_station"], "orders.worker");
assert_eq!(first["to_stations"], serde_json::json!(["orders.ledger"]));
assert_eq!(first["to"], "local");
assert_eq!(first["t"], 1_791_272_000_123_u64);
assert_eq!(first["reply_to"], "q-1");
assert_eq!(first["outcome"], "delivered");
let second = &value["messages"][1];
assert_eq!(second["id"], "local:x9");
assert_eq!(second["from"], "liminal.local");
assert_eq!(second["t"], 1_791_272_000_456_u64);
assert_eq!(second["outcome"], "refused");
assert!(second.get("reply_to").is_none());
assert!(second.get("from_station").is_none());
assert_eq!(
second["to_stations"],
serde_json::json!([]),
"a recorded fan-out that reached no worker"
);
assert!(
!String::from_utf8_lossy(&bytes).contains("never on the wire"),
"a payload never rides the record"
);
Ok(())
}
#[path = "records_tests/shape.rs"]
mod shape;