midi-harbor/crates/core/src/router.rs
2026-09-28 13:59:10 -05:00

1004 lines
38 KiB
Rust

//! The route graph: which endpoints feed which.
//!
//! Delivery is **direct only**. MIDI arriving at an endpoint goes to that endpoint's immediate
//! destinations and no further. Transitive delivery would mean adding one route silently changes
//! where existing traffic goes, which is the opposite of what a patchbay should do — and it turns
//! any cycle in the configuration into unbounded amplification.
//!
//! Cycles are still detected, because a user who draws one has almost certainly made a mistake
//! and FR-033 requires telling them. The loop that actually multiplies messages spans two
//! machines, and is caught by the session identity carried on the wire rather than here.
use crate::endpoint::{Endpoint, EndpointKind};
use crate::ids::{EndpointId, RouteId};
use std::collections::{BTreeSet, HashMap, HashSet};
/// Whether a route can currently deliver.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RouteValidity {
/// Both endpoints exist and the route can carry MIDI.
Valid,
/// One or both endpoints are missing, named here so the user knows what to restore.
Broken {
/// The endpoint names that matched nothing.
missing: Vec<String>,
},
/// The route takes part in a cycle.
///
/// Not an error: the route still delivers, because delivery is direct and a cycle cannot
/// multiply messages. It is surfaced because it is almost always a mistake.
LoopDetected {
/// The routes forming the cycle, in order.
cycle: Vec<RouteId>,
},
/// Both endpoints exist, but one of them is not running.
///
/// Distinct from broken: nothing is missing and nothing needs restoring, so the user has
/// nothing to repair. It is distinct from valid because the route is carrying nothing, and a
/// route that reads as fine while dropping everything is the worst of the three to debug.
Suspended {
/// The endpoint names that exist but are not available.
waiting_on: Vec<String>,
},
}
impl RouteValidity {
/// Reports whether MIDI can flow along this route right now.
pub fn can_deliver(&self) -> bool {
!matches!(self, Self::Broken { .. } | Self::Suspended { .. })
}
/// Reports whether the user has something to repair.
///
/// A suspended route needs nothing: it resumes on its own when the endpoint comes back.
pub fn needs_repair(&self) -> bool {
matches!(self, Self::Broken { .. })
}
}
/// Why a route could not be created.
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum RouteError {
/// A route from an endpoint to itself would do nothing useful.
#[error("a route cannot start and end at the same endpoint")]
SelfRoute,
/// The source cannot produce MIDI.
#[error("{0} has no input side, so it cannot be a route source")]
NotASource(String),
/// The destination cannot accept MIDI.
#[error("{0} has no output side, so it cannot be a route destination")]
NotADestination(String),
/// A connector was named that the port does not have.
#[error("{name} has no {kind} {number}")]
NoSuchConnector {
/// The port's name.
name: String,
/// "MIDI In" or "MIDI Out".
kind: &'static str,
/// The connector asked for, counting from one.
number: u16,
},
/// The same pair is already routed.
#[error("{from} is already routed to {to}")]
Duplicate {
/// The source name.
from: String,
/// The destination name.
to: String,
},
}
/// One resolved route between two endpoints.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedRoute {
/// Derived from the endpoint pair, so it is stable across restarts.
pub id: RouteId,
/// The source endpoint's name, as the configuration stores it.
pub from: String,
/// The destination endpoint's name.
pub to: String,
/// The source endpoint, when it exists.
pub source: Option<EndpointId>,
/// The destination endpoint, when it exists.
pub destination: Option<EndpointId>,
/// Which of the source's MIDI In connectors it starts from, counting from zero.
pub from_connector: u8,
/// Which of the destination's MIDI Out connectors it ends at, counting from zero.
pub to_connector: u8,
/// Whether it also carries MIDI back, from the destination's MIDI In `to_connector` to the
/// source's MIDI Out `from_connector`.
pub both_ways: bool,
/// Whether the user wants it delivering.
pub enabled: bool,
/// Whether it currently can.
pub validity: RouteValidity,
}
impl ResolvedRoute {
/// Reports whether MIDI should actually be carried along this route right now.
pub fn is_live(&self) -> bool {
self.enabled && self.validity.can_deliver()
}
}
/// One delivery dispatch should make: where the message goes, and which route carries it.
///
/// The route travels with the destination because dispatch is the only place that knows which
/// route delivered a message, and that is what per-route traffic has to be attributed to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Delivery {
/// The endpoint to deliver to.
pub destination: EndpointId,
/// Which of its MIDI Out connectors to deliver through, counting from zero.
pub connector: u8,
/// The route responsible for the delivery.
pub route: RouteId,
}
/// The route graph, prepared for dispatch.
#[derive(Debug, Default)]
pub struct Router {
routes: Vec<ResolvedRoute>,
/// Deliveries per source and MIDI In connector, so dispatch is a lookup rather than a scan.
fan_out: HashMap<(EndpointId, u8), Vec<Delivery>>,
}
impl Router {
/// Builds a router from configured routes and the endpoints that exist.
/// `running` names the endpoints that could carry MIDI this instant — a virtual port that is
/// open, hardware that is attached, a session that exists. An endpoint missing from it is
/// configured but not available, which suspends every route touching it.
pub fn build(
configured: &[crate::config::RouteConfig],
endpoints: &[Endpoint],
running: &HashSet<EndpointId>,
) -> Self {
let mut routes: Vec<ResolvedRoute> = configured
.iter()
.map(|route| {
// A name several endpoints share, with no kind written to choose between them,
// means none of them: picking one bound the route to whichever came last.
let source = resolve(endpoints, &route.from, |e| route.comes_from(e));
let destination = resolve(endpoints, &route.to, |e| route.goes_to(e));
let mut missing = Vec::new();
if let Err(reason) = &source {
missing.push(reason.clone());
}
if let Err(reason) = &destination {
missing.push(reason.clone());
}
let source = source.ok();
let destination = destination.ok();
// A connector the port no longer has is missing, as an endpoint would be, so the
// route waits for it rather than delivering somewhere else.
let (from_connector, to_connector) = (route.from_index(), route.to_index());
if let Some(endpoint) = source
&& from_connector >= connectors(endpoint).0
{
missing.push(connector_label(&route.from, "MIDI In", from_connector));
}
if let Some(endpoint) = destination
&& to_connector >= connectors(endpoint).1
{
missing.push(connector_label(&route.to, "MIDI Out", to_connector));
}
// The way back needs the same numbers the other way round.
if route.both_ways {
if let Some(endpoint) = destination
&& to_connector >= connectors(endpoint).0
{
missing.push(connector_label(&route.to, "MIDI In", to_connector));
}
if let Some(endpoint) = source
&& from_connector >= connectors(endpoint).1
{
missing.push(connector_label(&route.from, "MIDI Out", from_connector));
}
}
ResolvedRoute {
id: route.id(),
from: route.from.clone(),
to: route.to.clone(),
source: source.map(|e| e.id),
destination: destination.map(|e| e.id),
from_connector,
to_connector,
both_ways: route.both_ways,
enabled: route.enabled,
validity: if missing.is_empty() {
RouteValidity::Valid
} else {
RouteValidity::Broken { missing }
},
}
})
.collect();
mark_cycles(&mut routes);
mark_suspended(&mut routes, running);
// Only live routes take part in dispatch, so a disabled or broken one costs nothing.
let mut fan_out: HashMap<(EndpointId, u8), Vec<Delivery>> = HashMap::new();
for route in routes.iter().filter(|r| r.is_live()) {
if let (Some(source), Some(destination)) = (route.source, route.destination) {
fan_out
.entry((source, route.from_connector))
.or_default()
.push(Delivery {
destination,
connector: route.to_connector,
route: route.id,
});
// The way back travels under the same route, so what it carries and the notes it
// leaves sounding are the route's.
if route.both_ways {
fan_out
.entry((destination, route.to_connector))
.or_default()
.push(Delivery {
destination: source,
connector: route.from_connector,
route: route.id,
});
}
}
}
Self { routes, fan_out }
}
/// Returns every resolved route, including broken and disabled ones.
///
/// A broken route is kept rather than dropped, so it resumes when its endpoint comes back.
pub fn routes(&self) -> &[ResolvedRoute] {
&self.routes
}
/// Returns where MIDI arriving at one of `source`'s MIDI In connectors, counting from zero,
/// should be delivered, and along which route.
///
/// Direct destinations only. Empty for an endpoint with no live routes, which is the common
/// case and costs a single lookup.
pub fn deliveries(&self, source: EndpointId, connector: u8) -> &[Delivery] {
self.fan_out
.get(&(source, connector))
.map(Vec::as_slice)
.unwrap_or_default()
}
/// Returns the routes that take part in a cycle.
pub fn looping(&self) -> Vec<&ResolvedRoute> {
self.routes
.iter()
.filter(|route| matches!(route.validity, RouteValidity::LoopDetected { .. }))
.collect()
}
/// Returns the routes that cannot deliver because an endpoint is missing.
pub fn broken(&self) -> Vec<&ResolvedRoute> {
self.routes
.iter()
.filter(|route| matches!(route.validity, RouteValidity::Broken { .. }))
.collect()
}
/// Checks whether a proposed route may be created, between connectors counted from zero,
/// carrying MIDI back as well when `both_ways`.
pub fn validate(
existing: &[crate::config::RouteConfig],
source: &Endpoint,
from_connector: u8,
destination: &Endpoint,
to_connector: u8,
both_ways: bool,
) -> Result<(), RouteError> {
if source.id == destination.id {
return Err(RouteError::SelfRoute);
}
if !source.can_source() {
return Err(RouteError::NotASource(source.name.as_str().to_owned()));
}
if !destination.can_sink() {
return Err(RouteError::NotADestination(
destination.name.as_str().to_owned(),
));
}
// Both ways, each end has to be the other as well.
if both_ways && !destination.can_source() {
return Err(RouteError::NotASource(destination.name.as_str().to_owned()));
}
if both_ways && !source.can_sink() {
return Err(RouteError::NotADestination(source.name.as_str().to_owned()));
}
if from_connector >= connectors(source).0 {
return Err(RouteError::NoSuchConnector {
name: source.name.as_str().to_owned(),
kind: "MIDI In",
number: u16::from(from_connector) + 1,
});
}
if to_connector >= connectors(destination).1 {
return Err(RouteError::NoSuchConnector {
name: destination.name.as_str().to_owned(),
kind: "MIDI Out",
number: u16::from(to_connector) + 1,
});
}
if both_ways && to_connector >= connectors(destination).0 {
return Err(RouteError::NoSuchConnector {
name: destination.name.as_str().to_owned(),
kind: "MIDI In",
number: u16::from(to_connector) + 1,
});
}
if both_ways && from_connector >= connectors(source).1 {
return Err(RouteError::NoSuchConnector {
name: source.name.as_str().to_owned(),
kind: "MIDI Out",
number: u16::from(from_connector) + 1,
});
}
// A route already carrying either way the new one would is a duplicate: a two-way route
// carries its reverse too, and delivering one message twice doubles every note.
let duplicate = existing.iter().any(|route| {
carries(route, source, from_connector, destination, to_connector)
|| (both_ways && carries(route, destination, to_connector, source, from_connector))
});
if duplicate {
return Err(RouteError::Duplicate {
from: source.name.as_str().to_owned(),
to: destination.name.as_str().to_owned(),
});
}
Ok(())
}
}
/// Reports whether a configured route already carries MIDI from `source`'s MIDI In
/// `from_connector` to `destination`'s MIDI Out `to_connector`, either as it is written or, for a
/// two-way route, the other way round.
fn carries(
route: &crate::config::RouteConfig,
source: &Endpoint,
from_connector: u8,
destination: &Endpoint,
to_connector: u8,
) -> bool {
let forward = route.comes_from(source)
&& route.goes_to(destination)
&& route.from_index() == from_connector
&& route.to_index() == to_connector;
let back = route.both_ways
&& route.comes_from(destination)
&& route.goes_to(source)
&& route.from_index() == to_connector
&& route.to_index() == from_connector;
forward || back
}
/// Returns how many MIDI In and MIDI Out connectors an endpoint has. Only a virtual port may have
/// more than one of either.
pub fn connectors(endpoint: &Endpoint) -> (u8, u8) {
match &endpoint.kind {
EndpointKind::VirtualPort(port) => (port.inputs, port.outputs),
_ => (1, 1),
}
}
/// Names a connector a route needs and its endpoint lacks, as a broken route reports it.
fn connector_label(name: &str, kind: &str, index: u8) -> String {
format!("{name} {kind} {}", u16::from(index) + 1)
}
/// Finds the one endpoint a route's end designates, or says why there is none.
fn resolve<'a>(
endpoints: &'a [Endpoint],
name: &str,
designates: impl Fn(&Endpoint) -> bool,
) -> Result<&'a Endpoint, String> {
let mut found = endpoints.iter().filter(|endpoint| designates(endpoint));
match (found.next(), found.next()) {
(Some(endpoint), None) => Ok(endpoint),
(None, _) => Err(name.to_owned()),
(Some(_), Some(_)) => Err(format!("{name} (more than one endpoint has this name)")),
}
}
/// Marks every route whose endpoints exist but are not currently able to carry anything.
///
/// Applied only to routes that are otherwise valid: a route that is broken or part of a cycle has
/// something to say that matters more, and both of those outlast whatever is switched off today.
fn mark_suspended(routes: &mut [ResolvedRoute], running: &HashSet<EndpointId>) {
for route in routes.iter_mut() {
if !matches!(route.validity, RouteValidity::Valid) {
continue;
}
let mut waiting_on = Vec::new();
if route.source.is_some_and(|id| !running.contains(&id)) {
waiting_on.push(route.from.clone());
}
if route.destination.is_some_and(|id| !running.contains(&id)) {
waiting_on.push(route.to.clone());
}
if !waiting_on.is_empty() {
route.validity = RouteValidity::Suspended { waiting_on };
}
}
}
/// Marks every route that takes part in a cycle.
///
/// Walks the graph by endpoint name, so a cycle drawn through an endpoint that does not exist yet
/// is still reported rather than hidden behind a broken route.
fn mark_cycles(routes: &mut [ResolvedRoute]) {
// Adjacency by name, since a cycle is a property of the configuration rather than of what
// happens to be plugged in right now.
// A two-way route is an edge each way. Its own reverse is never taken as a way back: a
// message goes where its source is routed and no further, so A to B and back is not a loop.
let mut edges: HashMap<&str, Vec<(usize, &str)>> = HashMap::new();
for (index, route) in routes.iter().enumerate() {
edges
.entry(route.from.as_str())
.or_default()
.push((index, route.to.as_str()));
if route.both_ways {
edges
.entry(route.to.as_str())
.or_default()
.push((index, route.from.as_str()));
}
}
let mut in_cycle: BTreeSet<usize> = BTreeSet::new();
for start in 0..routes.len() {
let Some(route) = routes.get(start) else {
continue;
};
let mut ways = vec![(route.from.as_str(), route.to.as_str())];
if route.both_ways {
ways.push((route.to.as_str(), route.from.as_str()));
}
for (origin, first) in ways {
// A cycle exists when the far end can reach the near end again.
let mut visited: HashSet<&str> = HashSet::new();
let mut frontier = vec![(first, vec![start])];
while let Some((node, path)) = frontier.pop() {
if node == origin {
in_cycle.extend(path.iter().copied());
break;
}
if !visited.insert(node) {
continue;
}
for (next, to) in edges.get(node).map(Vec::as_slice).unwrap_or_default() {
if *next == start {
continue;
}
let mut extended = path.clone();
extended.push(*next);
frontier.push((to, extended));
}
}
}
}
let cycle: Vec<RouteId> = in_cycle
.iter()
.filter_map(|index| routes.get(*index).map(|r| r.id))
.collect();
for index in in_cycle {
if let Some(route) = routes.get_mut(index) {
// A broken route keeps its more useful reason: naming the missing endpoint tells the
// user what to fix, where a loop warning would not.
if matches!(route.validity, RouteValidity::Valid) {
route.validity = RouteValidity::LoopDetected {
cycle: cycle.clone(),
};
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::RouteConfig;
use crate::endpoint::{
Direction, EndpointKind, EndpointName, InvitationPolicy, KindTag, NetworkSession,
VirtualPort,
};
fn name(text: &str) -> EndpointName {
EndpointName::new(text).expect("a valid name")
}
fn endpoint(text: &str, direction: Direction) -> Endpoint {
Endpoint {
direction,
..port(text)
}
}
fn port(text: &str) -> Endpoint {
port_with(text, 1, 1)
}
fn port_with(text: &str, inputs: u8, outputs: u8) -> Endpoint {
Endpoint::new(
name(text),
EndpointKind::VirtualPort(VirtualPort::with_connectors(inputs, outputs)),
)
}
fn session(text: &str) -> Endpoint {
Endpoint::new(
name(text),
EndpointKind::NetworkSession(NetworkSession::new(
name(text),
0,
InvitationPolicy::Prompt,
)),
)
}
fn route(from: &str, to: &str) -> RouteConfig {
RouteConfig {
from: from.to_owned(),
to: to.to_owned(),
from_kind: None,
to_kind: None,
from_connector: None,
to_connector: None,
both_ways: false,
enabled: true,
}
}
fn connected(from: &str, from_connector: u8, to: &str, to_connector: u8) -> RouteConfig {
RouteConfig {
from_connector: Some(from_connector),
to_connector: Some(to_connector),
..route(from, to)
}
}
fn both_ways(route: RouteConfig) -> RouteConfig {
RouteConfig {
both_ways: true,
..route
}
}
fn names(list: &[&str]) -> Vec<String> {
list.iter().map(|text| (*text).to_owned()).collect()
}
/// A route is valid, broken or suspended by what it names and what is running, and only a
/// valid one takes part in dispatch.
///
/// Broken names what matched nothing, so the user knows what to restore, and outranks
/// suspended because it outlasts whatever is switched off today. A name two kinds share
/// designates neither unless the route writes the kind, since resolving by name alone bound
/// the route to whichever came last. A connector the port lacks is missing like an endpoint,
/// so the route waits rather than delivering somewhere nobody asked; a two-way route needs
/// the connectors of the way back too, the destination's MIDI In and the source's MIDI Out
/// of the same numbers.
#[test]
fn a_route_is_valid_broken_or_suspended_by_what_it_names_and_what_runs() {
struct Case {
name: &'static str,
endpoints: Vec<Endpoint>,
route: RouteConfig,
running: Option<&'static [&'static str]>,
want: RouteValidity,
}
let cases = [
Case {
name: "both ends exist and run",
endpoints: vec![port("Keyboard"), port("Synth")],
route: route("Keyboard", "Synth"),
running: None,
want: RouteValidity::Valid,
},
Case {
name: "the destination is not running",
endpoints: vec![port("Keyboard"), port("Synth")],
route: route("Keyboard", "Synth"),
running: Some(&["Keyboard"]),
want: RouteValidity::Suspended {
waiting_on: names(&["Synth"]),
},
},
Case {
name: "neither end is running",
endpoints: vec![port("Keyboard"), port("Synth")],
route: route("Keyboard", "Synth"),
running: Some(&[]),
want: RouteValidity::Suspended {
waiting_on: names(&["Keyboard", "Synth"]),
},
},
Case {
name: "the destination is missing",
endpoints: vec![port("Keyboard")],
route: route("Keyboard", "Gone Away"),
running: None,
want: RouteValidity::Broken {
missing: names(&["Gone Away"]),
},
},
Case {
name: "a missing end outranks one that is not running",
endpoints: vec![port("Keyboard")],
route: route("Keyboard", "Gone Away"),
running: Some(&[]),
want: RouteValidity::Broken {
missing: names(&["Gone Away"]),
},
},
Case {
name: "a name two kinds share, with the kind written",
endpoints: vec![port("Keystation"), session("Keystation"), port("Synth")],
route: RouteConfig {
from_kind: Some(KindTag::VirtualPort),
..route("Keystation", "Synth")
},
running: None,
want: RouteValidity::Valid,
},
Case {
name: "a name two kinds share, with no kind written",
endpoints: vec![port("Keystation"), session("Keystation"), port("Synth")],
route: route("Keystation", "Synth"),
running: None,
want: RouteValidity::Broken {
missing: names(&["Keystation (more than one endpoint has this name)"]),
},
},
Case {
name: "a MIDI In the port no longer has",
endpoints: vec![port("Keys"), port("Synth")],
route: connected("Keys", 2, "Synth", 1),
running: None,
want: RouteValidity::Broken {
missing: names(&["Keys MIDI In 2"]),
},
},
Case {
name: "a two-way route without the destination's MIDI In of the way back",
endpoints: vec![port_with("Keys", 2, 2), port_with("Synth", 1, 2)],
route: both_ways(connected("Keys", 2, "Synth", 2)),
running: None,
want: RouteValidity::Broken {
missing: names(&["Synth MIDI In 2"]),
},
},
Case {
name: "a two-way route without the source's MIDI Out of the way back",
endpoints: vec![port_with("Keys", 2, 1), port_with("Synth", 2, 2)],
route: both_ways(connected("Keys", 2, "Synth", 2)),
running: None,
want: RouteValidity::Broken {
missing: names(&["Keys MIDI Out 2"]),
},
},
];
for case in cases {
let running: HashSet<EndpointId> = case
.endpoints
.iter()
.filter(|e| case.running.is_none_or(|on| on.contains(&e.name.as_str())))
.map(|e| e.id)
.collect();
let router =
Router::build(std::slice::from_ref(&case.route), &case.endpoints, &running);
let resolved = &router.routes()[0];
assert_eq!(
resolved.validity, case.want,
"{}: the route must say why it can or cannot carry MIDI",
case.name
);
let delivers = resolved.source.is_some_and(|source| {
!router
.deliveries(source, case.route.from_index())
.is_empty()
});
assert_eq!(
delivers,
case.want == RouteValidity::Valid,
"{}: only a valid route may take part in dispatch",
case.name
);
}
}
/// Every route in a cycle is reported, and none outside it, while each still delivers
/// (FR-033).
///
/// Delivery is direct, so a cycle cannot multiply messages and refusing it would be stricter
/// than necessary, but one drawn by hand is almost always a mistake. A two-way route is an
/// edge each way, yet never its own way back: what reaches B from A is not sent on, so A to
/// B and back is no loop. A route to a missing endpoint keeps the broken reason, which says
/// what to restore where a loop warning would not.
#[test]
fn every_route_in_a_cycle_is_reported_and_still_delivers() {
let cases = [
(
"two routes each way",
&["A", "B"][..],
vec![route("A", "B"), route("B", "A")],
&["A->B", "B->A"][..],
),
(
"three routes round",
&["A", "B", "C"][..],
vec![route("A", "B"), route("B", "C"), route("C", "A")],
&["A->B", "B->C", "C->A"][..],
),
(
"a route leaving the cycle",
&["A", "B", "C"][..],
vec![route("A", "B"), route("B", "A"), route("A", "C")],
&["A->B", "B->A"][..],
),
(
"a two-way route alone",
&["A", "B"][..],
vec![both_ways(route("A", "B"))],
&[][..],
),
(
"a two-way route closing a cycle with two others",
&["A", "B", "C"][..],
vec![both_ways(route("A", "B")), route("B", "C"), route("C", "A")],
&["A->B", "B->C", "C->A"][..],
),
(
"a cycle through the ways back of two two-way routes",
&["A", "B", "C"][..],
vec![
both_ways(route("B", "A")),
both_ways(route("C", "B")),
route("C", "A"),
],
&["B->A", "C->A", "C->B"][..],
),
(
"a cycle through a missing endpoint",
&["A"][..],
vec![route("A", "B"), route("B", "A")],
&[][..],
),
];
for (case, present, configured, want) in cases {
let endpoints: Vec<Endpoint> = present.iter().map(|text| port(text)).collect();
let running = endpoints.iter().map(|e| e.id).collect();
let router = Router::build(&configured, &endpoints, &running);
let looping = router.looping();
let mut found: Vec<String> = looping
.iter()
.map(|route| format!("{}->{}", route.from, route.to))
.collect();
found.sort();
assert_eq!(
found, want,
"{case}: exactly the routes in the cycle must be reported"
);
assert!(
looping.iter().all(|route| route.is_live()),
"{case}: a route in a cycle must still deliver"
);
}
}
/// A proposed route is refused exactly when it could not carry MIDI as asked, or would carry
/// what an existing route already does.
///
/// The source must send and the destination receive, and a two-way route needs each end to
/// do both. Connectors are counted from zero here and named from one in the refusal, so
/// connector index 1 is "MIDI In 2". A route carrying the same way as an existing one, as
/// written or as the way back of a two-way route, would deliver every message twice. The
/// reverse direction, and the same ports through other connectors, are other routes.
#[test]
fn a_proposed_route_is_refused_exactly_when_it_cannot_or_need_not_exist() {
struct Case {
name: &'static str,
existing: Vec<RouteConfig>,
source: Endpoint,
from: u8,
destination: Endpoint,
to: u8,
both_ways: bool,
want: Result<(), RouteError>,
}
let itself = port("A");
let missing_connector = |name: &str, kind: &'static str| {
Err(RouteError::NoSuchConnector {
name: name.to_owned(),
kind,
number: 2,
})
};
let duplicate = |from: &str, to: &str| {
Err(RouteError::Duplicate {
from: from.to_owned(),
to: to.to_owned(),
})
};
let cases = [
Case {
name: "an endpoint routed to itself",
existing: vec![],
source: itself.clone(),
from: 0,
destination: itself,
to: 0,
both_ways: false,
want: Err(RouteError::SelfRoute),
},
Case {
name: "an input-only endpoint to an output-only one",
existing: vec![],
source: endpoint("Keyboard", Direction::Input),
from: 0,
destination: endpoint("Speaker", Direction::Output),
to: 0,
both_ways: false,
want: Ok(()),
},
Case {
name: "an output-only endpoint as the source",
existing: vec![],
source: endpoint("Speaker", Direction::Output),
from: 0,
destination: port("Synth"),
to: 0,
both_ways: false,
want: Err(RouteError::NotASource("Speaker".to_owned())),
},
Case {
name: "an input-only endpoint as the destination",
existing: vec![],
source: port("Keys"),
from: 0,
destination: endpoint("Pads", Direction::Input),
to: 0,
both_ways: false,
want: Err(RouteError::NotADestination("Pads".to_owned())),
},
Case {
name: "two-way to an endpoint that sends nothing",
existing: vec![],
source: port("Keys"),
from: 0,
destination: endpoint("Speaker", Direction::Output),
to: 0,
both_ways: true,
want: Err(RouteError::NotASource("Speaker".to_owned())),
},
Case {
name: "two-way from an endpoint that receives nothing",
existing: vec![],
source: endpoint("Pads", Direction::Input),
from: 0,
destination: port("Synth"),
to: 0,
both_ways: true,
want: Err(RouteError::NotADestination("Pads".to_owned())),
},
Case {
name: "a second MIDI Out the destination has",
existing: vec![],
source: port("Keys"),
from: 0,
destination: port_with("Synth", 1, 2),
to: 1,
both_ways: false,
want: Ok(()),
},
Case {
name: "a second MIDI In the source lacks",
existing: vec![],
source: port("Keys"),
from: 1,
destination: port_with("Synth", 1, 2),
to: 0,
both_ways: false,
want: missing_connector("Keys", "MIDI In"),
},
Case {
name: "two-way without the destination's MIDI In of the way back",
existing: vec![],
source: port_with("Keys", 2, 2),
from: 1,
destination: port_with("Synth", 1, 2),
to: 1,
both_ways: true,
want: missing_connector("Synth", "MIDI In"),
},
Case {
name: "two-way without the source's MIDI Out of the way back",
existing: vec![],
source: port_with("Keys", 2, 1),
from: 1,
destination: port_with("Synth", 2, 2),
to: 1,
both_ways: true,
want: missing_connector("Keys", "MIDI Out"),
},
Case {
name: "the same route again",
existing: vec![route("A", "B")],
source: port("A"),
from: 0,
destination: port("B"),
to: 0,
both_ways: false,
want: duplicate("A", "B"),
},
Case {
name: "the reverse of an existing route",
existing: vec![route("A", "B")],
source: port("B"),
from: 0,
destination: port("A"),
to: 0,
both_ways: false,
want: Ok(()),
},
Case {
name: "the same ports through another connector",
existing: vec![connected("Keys", 1, "Synth", 1)],
source: port("Keys"),
from: 0,
destination: port_with("Synth", 1, 2),
to: 1,
both_ways: false,
want: Ok(()),
},
Case {
name: "the way back of an existing two-way route",
existing: vec![both_ways(route("Keys", "Stage"))],
source: port("Stage"),
from: 0,
destination: port("Keys"),
to: 0,
both_ways: false,
want: duplicate("Stage", "Keys"),
},
Case {
name: "two-way over an existing route the other way",
existing: vec![route("Stage", "Keys")],
source: port("Keys"),
from: 0,
destination: port("Stage"),
to: 0,
both_ways: true,
want: duplicate("Keys", "Stage"),
},
];
for case in cases {
assert_eq!(
Router::validate(
&case.existing,
&case.source,
case.from,
&case.destination,
case.to,
case.both_ways,
),
case.want,
"{}: a route must be refused exactly when it cannot or need not exist",
case.name
);
}
}
}