#![allow(clippy::expect_used)]
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use bytes::Bytes;
use sipx_sip::build::{RequestBuilder, ResponseBuilder};
use sipx_sip::transaction::{
ClientState, Dispatch, Output, Reliability, ServerState, Timer, Timers, TransactionKey,
TransactionLayer, TuEvent,
};
use sipx_sip::{HeaderName, Host, HostName, Message, Method, Request, Response, StatusCode, Uri};
pub const SLOTS: u8 = 4;
pub const FIRST_LEGACY_SLOT: u8 = 2;
pub const STATUSES: [u16; 8] = [100, 180, 200, 302, 404, 486, 500, 603];
pub const TAGS: [&str; 3] = ["ta", "tb", "tc"];
pub const TIMERS: [Timer; 13] = [
Timer::A,
Timer::B,
Timer::D,
Timer::E,
Timer::F,
Timer::G,
Timer::H,
Timer::I,
Timer::J,
Timer::K,
Timer::L,
Timer::M,
Timer::Trying100,
];
fn method(index: u8) -> Method {
match index % METHOD_COUNT {
0 => Method::Invite,
1 => Method::Ack,
2 => Method::Bye,
3 => Method::Cancel,
4 => Method::Register,
_ => Method::Options,
}
}
const METHOD_COUNT: u8 = 6;
const STATUS_COUNT: u8 = 8;
const TAG_COUNT: u8 = 3;
const TIMER_COUNT: u8 = 13;
const _: () = {
assert!(STATUS_COUNT as usize == STATUSES.len());
assert!(TAG_COUNT as usize == TAGS.len());
assert!(TIMER_COUNT as usize == TIMERS.len());
};
const fn timer_row(timer: Timer) -> usize {
match timer {
Timer::A => 0,
Timer::B => 1,
Timer::D => 2,
Timer::E => 3,
Timer::F => 4,
Timer::G => 5,
Timer::H => 6,
Timer::I => 7,
Timer::J => 8,
Timer::K => 9,
Timer::L => 10,
Timer::M => 11,
Timer::Trying100 => 12,
}
}
#[cfg(test)]
mod timer_coverage {
use super::{TIMERS, timer_row};
#[test]
fn the_table_and_the_enum_agree_row_for_row() {
let mut seen = [false; 13];
for (row, (timer, named)) in TIMERS.iter().zip(seen.iter_mut()).enumerate() {
assert_eq!(
timer_row(*timer),
row,
"timer_row({timer:?}) does not round-trip"
);
*named = true;
}
assert!(
seen.iter().all(|used| *used),
"a row of TIMERS is unreachable from any variant"
);
}
}
const FOLDED_METHODS: usize = 5;
pub const MAX_LIVE_TRANSACTIONS: usize = 2 * SLOTS as usize * FOLDED_METHODS;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Space {
Client,
Server,
}
impl Space {
fn letter(self) -> char {
match self {
Self::Client => 'c',
Self::Server => 's',
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Event {
SendRequest {
slot: u8,
method: u8,
reliable: bool,
},
ReceiveRequest {
slot: u8,
method: u8,
reliable: bool,
to_tag: u8,
},
ReceiveResponse {
slot: u8,
method: u8,
status: u8,
to_tag: u8,
},
SendResponse {
slot: u8,
method: u8,
status: u8,
to_tag: u8,
},
FireTimer {
key: u8,
timer: u8,
any: bool,
},
TransportError {
key: u8,
},
Abandon {
key: u8,
},
}
const OPCODES: u8 = 7;
const RECORD: usize = 4;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Program {
pub events: Vec<Event>,
}
impl Program {
#[must_use]
pub fn decode(bytes: &[u8]) -> Self {
let events = bytes
.chunks_exact(RECORD)
.filter_map(|chunk| match chunk {
&[op, target, a, b] => Some(decode_event(op, target, a, b)),
_ => None,
})
.collect();
Self { events }
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(self.events.len() * RECORD);
for event in &self.events {
out.extend_from_slice(&encode_event(*event));
}
out
}
}
fn decode_event(op: u8, target: u8, a: u8, b: u8) -> Event {
match op % OPCODES {
0 => Event::SendRequest {
slot: target % SLOTS,
method: a % METHOD_COUNT,
reliable: b & 1 != 0,
},
1 => Event::ReceiveRequest {
slot: target % SLOTS,
method: a % METHOD_COUNT,
reliable: b & 1 != 0,
to_tag: (b >> 4) % TAG_COUNT,
},
2 => Event::ReceiveResponse {
slot: target % SLOTS,
method: a % METHOD_COUNT,
status: b % STATUS_COUNT,
to_tag: (b >> 4) % TAG_COUNT,
},
3 => Event::SendResponse {
slot: target % SLOTS,
method: a % METHOD_COUNT,
status: b % STATUS_COUNT,
to_tag: (b >> 4) % TAG_COUNT,
},
4 => Event::FireTimer {
key: target,
timer: a % TIMER_COUNT,
any: b & 1 != 0,
},
5 => Event::TransportError { key: target },
_ => Event::Abandon { key: target },
}
}
fn encode_event(event: Event) -> [u8; RECORD] {
match event {
Event::SendRequest {
slot,
method,
reliable,
} => [0, slot, method, u8::from(reliable)],
Event::ReceiveRequest {
slot,
method,
reliable,
to_tag,
} => [1, slot, method, u8::from(reliable) | (to_tag << 4)],
Event::ReceiveResponse {
slot,
method,
status,
to_tag,
} => [2, slot, method, status | (to_tag << 4)],
Event::SendResponse {
slot,
method,
status,
to_tag,
} => [3, slot, method, status | (to_tag << 4)],
Event::FireTimer { key, timer, any } => [4, key, timer, u8::from(any)],
Event::TransportError { key } => [5, key, 0, 0],
Event::Abandon { key } => [6, key, 0, 0],
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Invariant {
OutlivedTermination,
TimerForRemovedKey,
StoreGrowth,
UnnamedState,
UnroutableResponse,
}
#[derive(Debug, Clone)]
pub struct Violation {
pub step: usize,
pub invariant: Invariant,
pub detail: String,
}
impl std::fmt::Display for Violation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"step {}: {:?}: {}",
self.step, self.invariant, self.detail
)
}
}
#[derive(Debug, Clone)]
pub struct Run {
pub trace: Vec<String>,
pub violations: Vec<Violation>,
}
#[derive(Debug)]
struct Tracked {
key: TransactionKey,
space: Space,
invite: bool,
armed: Vec<Timer>,
label: String,
}
const DRAIN_ROUNDS: usize = 64;
struct Driver {
layer: TransactionLayer,
tracked: Vec<Tracked>,
trace: Vec<String>,
violations: Vec<Violation>,
#[allow(dead_code)]
suppressed: Vec<Known>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Known {}
pub const KNOWN_DEFECTS: [Known; 0] = [];
#[must_use]
pub fn run(program: &Program) -> Run {
run_with(program, &KNOWN_DEFECTS)
}
#[must_use]
pub fn run_strict(program: &Program) -> Run {
run_with(program, &[])
}
#[must_use]
pub fn run_with(program: &Program, suppressed: &[Known]) -> Run {
let mut driver = Driver {
layer: TransactionLayer::new(Timers::default()),
tracked: Vec::new(),
trace: Vec::new(),
violations: Vec::new(),
suppressed: suppressed.to_vec(),
};
for (step, event) in program.events.iter().enumerate() {
driver.step(step, *event);
driver.check_states(step);
driver.check_bound(step);
}
driver.drive_to_quiescence();
Run {
trace: driver.trace,
violations: driver.violations,
}
}
impl Driver {
fn step(&mut self, step: usize, event: Event) {
match event {
Event::SendRequest {
slot,
method: m,
reliable,
} => self.send_request(step, slot, m, reliable),
Event::ReceiveRequest {
slot,
method: m,
reliable,
to_tag,
} => self.receive_request(step, slot, m, reliable, to_tag),
Event::ReceiveResponse {
slot,
method: m,
status,
to_tag,
} => self.receive_response(step, slot, m, status, to_tag),
Event::SendResponse {
slot,
method: m,
status,
to_tag,
} => self.send_response(step, slot, m, status, to_tag),
Event::FireTimer { key, timer, any } => self.fire_timer(step, key, timer, any),
Event::TransportError { key } => self.transport_error(step, key),
Event::Abandon { key } => self.abandon(step, key),
}
}
fn send_request(&mut self, step: usize, slot: u8, method_index: u8, reliable: bool) {
let m = method(method_index);
let request = build_request(Space::Client, slot, &m, None);
let reliability = reliability(reliable);
let Some((key, outputs)) = self.layer.send_request(request, reliability) else {
self.record(
step,
&format!("SendRequest({}) no key", label(slot, &m)),
"",
);
return;
};
let index = self.track(key, Space::Client, slot, &m, m == Method::Invite);
let outcome = format!(
"created client {} {}",
self.tracked
.get(index)
.map_or_else(String::new, |t| t.label.clone()),
self.apply(index, &outputs)
);
self.record(
step,
&format!(
"SendRequest({} {})",
label(slot, &m),
transport_name(reliable)
),
&outcome,
);
self.check_termination(step, index, &outputs);
}
fn receive_request(
&mut self,
step: usize,
slot: u8,
method_index: u8,
reliable: bool,
to_tag: u8,
) {
let m = method(method_index);
let request = build_request(Space::Server, slot, &m, Some(to_tag));
let dispatch = self
.layer
.receive(Message::Request(request), reliability(reliable));
let outcome = self.absorb(step, dispatch, Space::Server, slot, &m);
self.record(
step,
&format!(
"ReceiveRequest({} {})",
label(slot, &m),
transport_name(reliable)
),
&outcome,
);
}
fn receive_response(
&mut self,
step: usize,
slot: u8,
method_index: u8,
status_index: u8,
to_tag: u8,
) {
let m = method(method_index);
let status = status(status_index);
let request = build_request(Space::Client, slot, &m, None);
let expected = TransactionKey::from_sent_request(&request);
let live = expected
.as_ref()
.and_then(|key| self.layer.client_state(key))
.is_some();
let response = build_response(&request, status, to_tag);
let dispatch = self
.layer
.receive(Message::Response(response), Reliability::Unreliable);
let matched = matches!(dispatch, Dispatch::Matched { .. });
let outcome = self.absorb(step, dispatch, Space::Client, slot, &m);
if live && !matched {
self.violate(
step,
Invariant::UnroutableResponse,
format!(
"a {} response for {} matched nothing, but its client transaction is live",
status.code(),
label(slot, &m)
),
);
}
self.record(
step,
&format!("ReceiveResponse({} {})", label(slot, &m), status.code()),
&outcome,
);
}
fn send_response(
&mut self,
step: usize,
slot: u8,
method_index: u8,
status_index: u8,
to_tag: u8,
) {
let m = method(method_index);
let status = status(status_index);
let request = build_request(Space::Server, slot, &m, Some(to_tag));
let Some(key) = TransactionKey::from_request(&request) else {
self.record(
step,
&format!("SendResponse({}) no key", label(slot, &m)),
"",
);
return;
};
let response = build_response(&request, status, to_tag);
let outputs = self.layer.send_response(&key, response);
let outcome = match self.index_of(&key) {
Some(index) => {
let label = self
.tracked
.get(index)
.map_or_else(String::new, |t| t.label.clone());
format!("{label} {}", self.apply(index, &outputs))
}
None => format!("no transaction store={}", store(&self.layer)),
};
self.record(
step,
&format!("SendResponse({} {})", label(slot, &m), status.code()),
&outcome,
);
if let Some(index) = self.index_of(&key) {
self.check_termination(step, index, &outputs);
}
}
fn fire_timer(&mut self, step: usize, key_index: u8, timer_index: u8, any: bool) {
if self.tracked.is_empty() {
self.record(step, "FireTimer(no keys)", "");
return;
}
let index = key_index as usize % self.tracked.len();
let Some(entry) = self.tracked.get(index) else {
return;
};
let armed = entry.armed.clone();
let key = entry.key.clone();
let label = entry.label.clone();
let space = entry.space;
let timer = if any || armed.is_empty() {
*TIMERS
.get(timer_index as usize % TIMERS.len())
.expect("the timer index is taken modulo the table")
} else {
*armed
.get(timer_index as usize % armed.len())
.expect("the armed index is taken modulo a non-empty list")
};
let live = self.is_live(space, &key);
let before = self.layer.len();
if let Some(entry) = self.tracked.get_mut(index) {
entry.armed.retain(|t| *t != timer);
}
let outputs = self.layer.on_timer(&key, timer);
let after = self.layer.len();
if !live && (!outputs.is_empty() || before != after) {
self.violate(
step,
Invariant::TimerForRemovedKey,
format!(
"timer {timer:?} for {label}, whose transaction is gone, produced \
{} output(s) and left the store at {after:?} (was {before:?})",
outputs.len()
),
);
}
let outcome = format!("{label} {}", self.apply(index, &outputs));
self.record(
step,
&format!(
"FireTimer({label} {timer:?}{})",
if live { "" } else { " stale" }
),
&outcome,
);
self.check_termination(step, index, &outputs);
}
fn transport_error(&mut self, step: usize, key_index: u8) {
if self.tracked.is_empty() {
self.record(step, "TransportError(no keys)", "");
return;
}
let index = key_index as usize % self.tracked.len();
let Some(entry) = self.tracked.get(index) else {
return;
};
let key = entry.key.clone();
let label = entry.label.clone();
let outputs = self.layer.on_transport_error(&key);
let outcome = format!("{label} {}", self.apply(index, &outputs));
self.record(step, &format!("TransportError({label})"), &outcome);
self.check_termination(step, index, &outputs);
}
fn abandon(&mut self, step: usize, key_index: u8) {
if self.tracked.is_empty() {
self.record(step, "Abandon(no keys)", "");
return;
}
let index = key_index as usize % self.tracked.len();
let Some(entry) = self.tracked.get(index) else {
return;
};
let key = entry.key.clone();
let label = entry.label.clone();
let gone = self.layer.abandon(&key);
if let Some(entry) = self.tracked.get_mut(index).filter(|_| gone) {
entry.armed.clear();
}
self.record(
step,
&format!("Abandon({label})"),
&format!(
"{} store={}",
if gone { "dropped" } else { "absent" },
store(&self.layer)
),
);
}
fn absorb(
&mut self,
step: usize,
dispatch: Dispatch,
space: Space,
slot: u8,
m: &Method,
) -> String {
match dispatch {
Dispatch::Created { key, outputs } => {
let index = self.track(key, space, slot, m, m == &Method::Invite);
let label = self
.tracked
.get(index)
.map_or_else(String::new, |t| t.label.clone());
let summary = format!("created server {label} {}", self.apply(index, &outputs));
self.check_termination(step, index, &outputs);
summary
}
Dispatch::Matched { key, outputs } => match self.index_of(&key) {
Some(index) => {
let label = self
.tracked
.get(index)
.map_or_else(String::new, |t| t.label.clone());
let summary = format!("matched {label} {}", self.apply(index, &outputs));
self.check_termination(step, index, &outputs);
summary
}
None => format!("matched untracked {}", summarise(&outputs)),
},
Dispatch::Unmatched(_) => format!("unmatched store={}", store(&self.layer)),
}
}
fn track(
&mut self,
key: TransactionKey,
space: Space,
slot: u8,
m: &Method,
invite: bool,
) -> usize {
if let Some(index) = self.index_of(&key) {
if let Some(entry) = self.tracked.get_mut(index) {
entry.invite = invite;
entry.armed.clear();
}
return index;
}
self.tracked.push(Tracked {
key,
space,
invite,
armed: Vec::new(),
label: format!("{}{slot}/{}", space.letter(), method_name(m)),
});
self.tracked.len() - 1
}
fn index_of(&self, key: &TransactionKey) -> Option<usize> {
self.tracked.iter().position(|t| &t.key == key)
}
fn apply(&mut self, index: usize, outputs: &[Output]) -> String {
if let Some(entry) = self.tracked.get_mut(index) {
for output in outputs {
match output {
Output::SetTimer { timer, .. } => {
if !entry.armed.contains(timer) {
entry.armed.push(*timer);
}
}
Output::ClearTimer(timer) => entry.armed.retain(|t| t != timer),
Output::Terminated(_) => entry.armed.clear(),
Output::Send(_) | Output::ToTu(_) => {}
}
}
}
let state =
self.tracked
.get(index)
.map_or_else(String::new, |entry| match self.state_of(entry) {
Some(state) => format!(" state={state}"),
None => String::new(),
});
format!("{}{state} store={}", summarise(outputs), store(&self.layer))
}
fn check_termination(&mut self, step: usize, index: usize, outputs: &[Output]) {
let Some(entry) = self.tracked.get(index) else {
return;
};
if !outputs.iter().any(|o| matches!(o, Output::Terminated(_))) {
return;
}
let label = entry.label.clone();
let still_there = self.state_of(entry).is_some();
if still_there {
self.violate(
step,
Invariant::OutlivedTermination,
format!("{label} reported Terminated and is still in the store"),
);
}
if let Some(timer) = outputs.iter().find_map(|o| match o {
Output::SetTimer { timer, .. } => Some(*timer),
_ => None,
}) {
self.violate(
step,
Invariant::OutlivedTermination,
format!("{label} armed timer {timer:?} in the batch that retired it"),
);
}
}
#[allow(clippy::match_same_arms)]
fn check_states(&mut self, step: usize) {
let mut found = Vec::new();
for entry in &self.tracked {
let Some(state) = self.state_of(entry) else {
continue;
};
let legal = match (entry.space, entry.invite) {
(Space::Client, true) => {
["Calling", "Proceeding", "Completed", "Accepted"].as_slice()
}
(Space::Client, false) => ["Trying", "Proceeding", "Completed"].as_slice(),
(Space::Server, true) => {
["Proceeding", "Completed", "Confirmed", "Accepted"].as_slice()
}
(Space::Server, false) => ["Trying", "Proceeding", "Completed"].as_slice(),
};
if !legal.contains(&state.as_str()) {
found.push(format!(
"{} is in {state}, which §17 does not name for {} machine",
entry.label,
machine_name(entry.space, entry.invite)
));
}
}
for detail in found {
self.violate(step, Invariant::UnnamedState, detail);
}
}
fn check_bound(&mut self, step: usize) {
let (client, server) = self.layer.len();
let live = client + server;
if live > self.tracked.len() {
self.violate(
step,
Invariant::StoreGrowth,
format!(
"{live} transactions in flight, but the layer has only reported \
{} keys",
self.tracked.len()
),
);
}
}
fn drive_to_quiescence(&mut self) {
for _ in 0..DRAIN_ROUNDS {
let mut fired = false;
for index in 0..self.tracked.len() {
let Some(entry) = self.tracked.get(index) else {
continue;
};
if self.state_of(entry).is_none() {
continue;
}
let key = entry.key.clone();
let armed = entry.armed.clone();
for timer in armed {
if let Some(entry) = self.tracked.get_mut(index) {
entry.armed.retain(|t| *t != timer);
}
let outputs = self.layer.on_timer(&key, timer);
let _ = self.apply(index, &outputs);
fired = true;
}
}
if !fired {
break;
}
}
let mut found = Vec::new();
for entry in &self.tracked {
let Some(state) = self.state_of(entry) else {
continue;
};
let waiting_on_the_application = match entry.space {
Space::Server => state == "Trying" || state == "Proceeding",
Space::Client => entry.invite && state == "Proceeding",
};
if !waiting_on_the_application {
found.push(format!(
"{} is still in {state} with every timer fired; nothing will retire it",
entry.label
));
}
}
for detail in found {
self.violate(usize::MAX, Invariant::StoreGrowth, detail);
}
self.trace.push(format!(
"quiescent store={} live={:?}",
store(&self.layer),
self.tracked
.iter()
.filter_map(|entry| self.state_of(entry).map(|s| format!("{}={s}", entry.label)))
.collect::<Vec<_>>()
));
}
fn state_of(&self, entry: &Tracked) -> Option<String> {
match entry.space {
Space::Client => self.layer.client_state(&entry.key).map(client_state_name),
Space::Server => self.layer.server_state(&entry.key).map(server_state_name),
}
}
fn is_live(&self, space: Space, key: &TransactionKey) -> bool {
match space {
Space::Client => self.layer.client_state(key).is_some(),
Space::Server => self.layer.server_state(key).is_some(),
}
}
fn record(&mut self, step: usize, event: &str, outcome: &str) {
let mut line = String::new();
let _ = write!(line, "{step:03} {event}");
if !outcome.is_empty() {
let _ = write!(line, " | {outcome}");
}
self.trace.push(line);
}
fn violate(&mut self, step: usize, invariant: Invariant, detail: String) {
self.violations.push(Violation {
step,
invariant,
detail,
});
}
}
fn reliability(reliable: bool) -> Reliability {
if reliable {
Reliability::Reliable
} else {
Reliability::Unreliable
}
}
fn transport_name(reliable: bool) -> &'static str {
if reliable { "tcp" } else { "udp" }
}
fn status(index: u8) -> StatusCode {
let code = *STATUSES
.get(index as usize % STATUSES.len())
.expect("the status index is taken modulo the table");
StatusCode::new(code).expect("the status table holds valid codes")
}
fn branch(space: Space, slot: u8) -> String {
if slot < FIRST_LEGACY_SLOT {
format!("z9hG4bK-fz{}{slot}", space.letter())
} else {
format!("fz{}{slot}-rfc2543", space.letter())
}
}
fn build_request(space: Space, slot: u8, m: &Method, to_tag: Option<u8>) -> Request {
let uri = Uri::sip(Host::Name(
HostName::new("example.com").expect("a valid host"),
));
let to = match to_tag.and_then(|index| TAGS.get(index as usize % TAGS.len())) {
Some(tag) => format!("<sip:callee@example.com>;tag={tag}"),
None => "<sip:callee@example.com>".to_owned(),
};
RequestBuilder::new(m.clone(), uri)
.header(
HeaderName::Via,
Bytes::from(format!(
"SIP/2.0/UDP host.example.net;branch={}",
branch(space, slot)
)),
)
.expect("a valid Via")
.header(
HeaderName::From,
Bytes::from(format!("<sip:caller@example.net>;tag=f{slot}")),
)
.expect("a valid From")
.header(HeaderName::To, Bytes::from(to))
.expect("a valid To")
.header(
HeaderName::CallId,
Bytes::from(format!("fuzz-{slot}@example.net")),
)
.expect("a valid Call-ID")
.cseq(1, m)
.expect("a valid CSeq")
.max_forwards(70)
.build()
}
fn build_response(request: &Request, status: StatusCode, to_tag: u8) -> Response {
let mut builder = ResponseBuilder::to_request(request, status, "Sequence")
.expect("a response can be built for any request the harness makes");
if !status.is_provisional() {
let tag = TAGS
.get(to_tag as usize % TAGS.len())
.expect("the tag index is taken modulo the table");
builder = builder
.set_header(
&HeaderName::To,
Bytes::from(format!("<sip:callee@example.com>;tag={tag}")),
)
.expect("a valid To");
}
builder.build()
}
fn method_name(m: &Method) -> String {
String::from_utf8_lossy(m.as_bytes()).into_owned()
}
fn label(slot: u8, m: &Method) -> String {
format!("{slot}/{}", method_name(m))
}
fn machine_name(space: Space, invite: bool) -> &'static str {
match (space, invite) {
(Space::Client, true) => "the INVITE client",
(Space::Client, false) => "the non-INVITE client",
(Space::Server, true) => "the INVITE server",
(Space::Server, false) => "the non-INVITE server",
}
}
fn client_state_name(state: ClientState) -> String {
match state {
ClientState::Calling => "Calling",
ClientState::Trying => "Trying",
ClientState::Proceeding => "Proceeding",
ClientState::Completed => "Completed",
ClientState::Accepted => "Accepted",
ClientState::Terminated => "Terminated",
}
.to_owned()
}
fn server_state_name(state: ServerState) -> String {
match state {
ServerState::Trying => "Trying",
ServerState::Proceeding => "Proceeding",
ServerState::Completed => "Completed",
ServerState::Confirmed => "Confirmed",
ServerState::Accepted => "Accepted",
ServerState::Terminated => "Terminated",
}
.to_owned()
}
fn store(layer: &TransactionLayer) -> String {
let (client, server) = layer.len();
format!("{client}/{server}")
}
fn summarise(outputs: &[Output]) -> String {
let mut parts = Vec::new();
let sends = outputs
.iter()
.filter(|o| matches!(o, Output::Send(_)))
.count();
if sends > 0 {
parts.push(format!("send={sends}"));
}
let events: Vec<&str> = outputs
.iter()
.filter_map(|o| match o {
Output::ToTu(event) => Some(match event.as_ref() {
TuEvent::Request(_) => "request",
TuEvent::Response(_) => "response",
TuEvent::Ack(_) => "ack",
TuEvent::Timeout => "timeout",
TuEvent::TransportError => "transport-error",
}),
_ => None,
})
.collect();
if !events.is_empty() {
parts.push(format!("tu=[{}]", events.join(",")));
}
let set: Vec<String> = outputs
.iter()
.filter_map(|o| match o {
Output::SetTimer { timer, .. } => Some(format!("{timer:?}")),
_ => None,
})
.collect();
if !set.is_empty() {
parts.push(format!("set=[{}]", set.join(",")));
}
let cleared: Vec<String> = outputs
.iter()
.filter_map(|o| match o {
Output::ClearTimer(timer) => Some(format!("{timer:?}")),
_ => None,
})
.collect();
if !cleared.is_empty() {
parts.push(format!("clear=[{}]", cleared.join(",")));
}
if let Some(reason) = outputs.iter().find_map(|o| match o {
Output::Terminated(reason) => Some(*reason),
_ => None,
}) {
parts.push(format!("terminated={reason:?}"));
}
if parts.is_empty() {
"absorbed".to_owned()
} else {
parts.join(" ")
}
}
#[derive(Debug, Clone)]
pub struct Seed {
pub name: &'static str,
pub program: Program,
}
pub const CORPUS_PATH: &str = "crates/sipx-testkit/corpus/transaction-sequences";
#[must_use]
pub fn corpus_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("corpus")
.join("transaction-sequences")
}
pub fn write_corpus() -> std::io::Result<usize> {
let dir = corpus_dir();
std::fs::create_dir_all(&dir)?;
let seeds = seeds();
for seed in &seeds {
std::fs::write(dir.join(seed.name), seed.program.encode())?;
}
Ok(seeds.len())
}
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn seeds() -> Vec<Seed> {
const INVITE: u8 = 0;
const ACK: u8 = 1;
const BYE: u8 = 2;
const CANCEL: u8 = 3;
const REGISTER: u8 = 4;
const OPTIONS: u8 = 5;
const S100: u8 = 0;
const S180: u8 = 1;
const S200: u8 = 2;
const S486: u8 = 5;
const S500: u8 = 6;
fn timer(key: u8, which: Timer) -> Event {
let index =
u8::try_from(timer_row(which)).expect("the timer table is shorter than 256 entries");
Event::FireTimer {
key,
timer: index,
any: true,
}
}
fn send(slot: u8, method: u8) -> Event {
Event::SendRequest {
slot,
method,
reliable: false,
}
}
fn recv(slot: u8, method: u8) -> Event {
Event::ReceiveRequest {
slot,
method,
reliable: false,
to_tag: 0,
}
}
fn answer(slot: u8, method: u8, status: u8) -> Event {
Event::SendResponse {
slot,
method,
status,
to_tag: 0,
}
}
fn reply(slot: u8, method: u8, status: u8, to_tag: u8) -> Event {
Event::ReceiveResponse {
slot,
method,
status,
to_tag,
}
}
let mut seeds = Vec::new();
let mut seed = |name: &'static str, events: Vec<Event>| {
seeds.push(Seed {
name,
program: Program { events },
});
};
seed(
"t1-invite-client-retransmits-then-times-out",
vec![
send(0, INVITE),
timer(0, Timer::A),
timer(0, Timer::A),
timer(0, Timer::A),
timer(0, Timer::B),
],
);
seed(
"t2-invite-client-acks-a-non-2xx",
vec![
send(0, INVITE),
reply(0, INVITE, S486, 0),
reply(0, INVITE, S486, 0),
timer(0, Timer::D),
],
);
seed(
"t3-invite-client-2xx-is-not-acked-and-a-fork-answers-twice",
vec![
send(0, INVITE),
reply(0, INVITE, S200, 0),
reply(0, INVITE, S200, 1),
timer(0, Timer::M),
],
);
seed(
"invite-client-waits-in-proceeding-with-no-timeout",
vec![
send(0, INVITE),
reply(0, INVITE, S180, 0),
timer(0, Timer::B),
reply(0, INVITE, S200, 0),
timer(0, Timer::M),
],
);
seed(
"non-invite-client-backs-off-then-times-out",
vec![
send(1, OPTIONS),
timer(0, Timer::E),
timer(0, Timer::E),
timer(0, Timer::E),
timer(0, Timer::F),
],
);
seed(
"non-invite-client-completes-on-a-final-response",
vec![
send(1, BYE),
reply(1, BYE, S200, 0),
reply(1, BYE, S200, 0),
timer(0, Timer::K),
],
);
seed(
"t5-server-absorbs-request-retransmissions",
vec![
recv(0, REGISTER),
recv(0, REGISTER),
answer(0, REGISTER, S200),
recv(0, REGISTER),
timer(0, Timer::J),
],
);
seed(
"t7-invite-server-sends-100-then-absorbs-the-ack",
vec![
recv(0, INVITE),
timer(0, Timer::Trying100),
answer(0, INVITE, S486),
timer(0, Timer::G),
recv(0, ACK),
timer(0, Timer::I),
],
);
seed(
"t8-invite-server-2xx-hands-the-ack-to-the-tu",
vec![
recv(0, INVITE),
answer(0, INVITE, S180),
timer(0, Timer::Trying100),
answer(0, INVITE, S200),
recv(0, ACK),
timer(0, Timer::L),
],
);
seed(
"invite-server-times-out-waiting-for-an-ack",
vec![
recv(0, INVITE),
answer(0, INVITE, S500),
timer(0, Timer::G),
timer(0, Timer::H),
],
);
seed(
"t9-reliable-transport-sets-no-retransmission-timers",
vec![
Event::SendRequest {
slot: 0,
method: INVITE,
reliable: true,
},
reply(0, INVITE, S486, 0),
timer(0, Timer::D),
Event::ReceiveRequest {
slot: 1,
method: OPTIONS,
reliable: true,
to_tag: 0,
},
answer(1, OPTIONS, S200),
timer(1, Timer::J),
],
);
seed(
"t13-legacy-branch-matching",
vec![
recv(2, OPTIONS),
recv(2, OPTIONS),
answer(2, OPTIONS, S200),
recv(2, OPTIONS),
timer(0, Timer::J),
],
);
seed(
"t14-cancel-runs-in-its-own-transaction",
vec![
recv(0, INVITE),
recv(0, CANCEL),
answer(0, CANCEL, S200),
answer(0, INVITE, S486),
recv(0, ACK),
timer(0, Timer::I),
timer(1, Timer::J),
],
);
seed(
"transport-error-terminates-and-tells-the-tu",
vec![send(0, OPTIONS), Event::TransportError { key: 0 }],
);
seed(
"abandon-a-server-transaction-the-application-never-answered",
vec![recv(1, REGISTER), Event::Abandon { key: 0 }],
);
seed(
"stale-timers-fire-after-the-transaction-is-gone",
vec![
send(1, OPTIONS),
reply(1, OPTIONS, S200, 0),
timer(0, Timer::K),
timer(0, Timer::K),
timer(0, Timer::F),
timer(0, Timer::E),
],
);
seed(
"non-invite-client-times-out-from-proceeding-too",
vec![
send(1, REGISTER),
reply(1, REGISTER, S100, 0),
timer(0, Timer::E),
timer(0, Timer::F),
],
);
seeds
}