1387 lines
48 KiB
Rust
1387 lines
48 KiB
Rust
//! Changing a network port after it is made: the name other machines see, its UDP port and who
|
|
//! may join (FR-015, R-078).
|
|
|
|
#![allow(
|
|
clippy::expect_used,
|
|
clippy::indexing_slicing,
|
|
clippy::panic,
|
|
clippy::unwrap_used
|
|
)]
|
|
|
|
mod common;
|
|
|
|
use midi_harbor_core::endpoint::{EndpointKind, InvitationPolicy, NetworkSession};
|
|
use midi_harbor_core::failure::FailureReason;
|
|
use midi_harbor_core::ids::EndpointId;
|
|
use midi_harbor_core::state::ConnectionPhase;
|
|
use midi_harbor_daemon::{Daemon, DaemonError, NetworkPortChange};
|
|
use midi_harbor_platform::fake::FakeMidiPlatform;
|
|
use midi_harbor_platform::midi::MidiPlatform;
|
|
use std::net::{SocketAddr, UdpSocket};
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
/// Starts a daemon standing in for one machine, over a scratch directory of its own.
|
|
async fn machine(label: &str) -> Arc<Daemon> {
|
|
let root = common::scratch("midi-harbor-network-ports")
|
|
.join(format!("{label}-{}", uuid::Uuid::new_v4()));
|
|
Daemon::start(
|
|
common::quiet(root),
|
|
Arc::new(FakeMidiPlatform::new()) as Arc<dyn MidiPlatform>,
|
|
)
|
|
.await
|
|
.expect("the daemon starts over a scratch directory")
|
|
}
|
|
|
|
/// Returns a network port's stored settings.
|
|
async fn settings(daemon: &Arc<Daemon>, id: EndpointId) -> NetworkSession {
|
|
daemon
|
|
.read(
|
|
|config, _| match config.endpoint(id).map(|e| e.kind.clone()) {
|
|
Some(EndpointKind::NetworkSession(session)) => session,
|
|
_ => panic!("not a network port"),
|
|
},
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Returns an even UDP port that is free, with the one above it free too for the data port.
|
|
///
|
|
/// Chosen at random below the ports systems hand out for port zero, which start at 32768 on Linux
|
|
/// and 49152 on Windows. Windows hands those out in order, so a pair the system had just chosen
|
|
/// and released here was often the next one given to a daemon starting in another test, and the
|
|
/// move to it was refused.
|
|
fn free_pair() -> u16 {
|
|
for _ in 0..200 {
|
|
// Even ports from 20000 to 29998.
|
|
let port = 20_000 + 2 * rand::random_range(0..5_000u16);
|
|
let free = |port: u16| {
|
|
UdpSocket::bind(("0.0.0.0", port)).is_ok() && UdpSocket::bind(("::", port)).is_ok()
|
|
};
|
|
if free(port) && free(port + 1) {
|
|
return port;
|
|
}
|
|
}
|
|
panic!("no free pair of UDP ports");
|
|
}
|
|
|
|
/// Waits for a network port to be connected, reporting whether it was.
|
|
async fn connected(daemon: &Arc<Daemon>, id: EndpointId) -> bool {
|
|
for _ in 0..100 {
|
|
if daemon
|
|
.session_status(id)
|
|
.await
|
|
.is_some_and(|status| status.state.phase() == ConnectionPhase::Connected)
|
|
{
|
|
return true;
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Builds Stage on a near machine connected to Front of House on a far one.
|
|
async fn joined() -> (Arc<Daemon>, EndpointId, Arc<Daemon>, EndpointId) {
|
|
let far = machine("far").await;
|
|
let front = far
|
|
.create_network_session("Front of House", 0, InvitationPolicy::AcceptAll)
|
|
.await
|
|
.expect("the far network port is created");
|
|
let port = far
|
|
.session_status(front.id)
|
|
.await
|
|
.expect("the far network port is listening")
|
|
.control_port;
|
|
let near = machine("near").await;
|
|
let stage = near
|
|
.create_network_session("Stage", 0, InvitationPolicy::Prompt)
|
|
.await
|
|
.expect("the near network port is created");
|
|
near.connect_peer(stage.id, SocketAddr::from(([127, 0, 0, 1], port)))
|
|
.await
|
|
.expect("the near network port invites the far one");
|
|
assert!(
|
|
connected(&near, stage.id).await,
|
|
"a far network port accepting everyone lets the near one in"
|
|
);
|
|
(near, stage.id, far, front.id)
|
|
}
|
|
|
|
/// Proves that a new Bonjour name is applied to a running network port without dropping the
|
|
/// machine connected to it, that a change naming only the Bonjour name leaves every other setting
|
|
/// as it was, and that a machine joining afterwards is told the new name.
|
|
///
|
|
/// The far port accepts everyone, so a change that reset the unnamed settings to their defaults
|
|
/// would show as the invitation policy falling back to asking first.
|
|
#[tokio::test]
|
|
async fn a_new_bonjour_name_leaves_the_connected_machine_connected() {
|
|
let (near, stage, far, front) = joined().await;
|
|
let peer = far
|
|
.session_status(front)
|
|
.await
|
|
.expect("the far network port is running")
|
|
.peer_address;
|
|
let before = settings(&far, front).await;
|
|
|
|
far.update_network_port(
|
|
front,
|
|
NetworkPortChange {
|
|
local_name: Some("Main Stage".to_owned()),
|
|
..NetworkPortChange::default()
|
|
},
|
|
)
|
|
.await
|
|
.expect("the far network port takes the new Bonjour name");
|
|
|
|
let after = settings(&far, front).await;
|
|
assert_eq!(
|
|
after.local_name.as_str(),
|
|
"Main Stage",
|
|
"the new Bonjour name is stored"
|
|
);
|
|
assert_eq!(
|
|
after.invitation_policy,
|
|
InvitationPolicy::AcceptAll,
|
|
"a change that says nothing about the invitation policy leaves it alone"
|
|
);
|
|
assert_eq!(
|
|
after.control_port, before.control_port,
|
|
"a change that says nothing about the UDP port leaves it alone"
|
|
);
|
|
assert_eq!(
|
|
after.automatic_port, before.automatic_port,
|
|
"a change that says nothing about the automatic port leaves it alone"
|
|
);
|
|
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
let status = far
|
|
.session_status(front)
|
|
.await
|
|
.expect("the far network port is still running");
|
|
assert_eq!(
|
|
status.state.phase(),
|
|
ConnectionPhase::Connected,
|
|
"a new Bonjour name does not restart the network port"
|
|
);
|
|
assert_eq!(status.peer_address, peer, "the machine was dropped");
|
|
assert!(
|
|
connected(&near, stage).await,
|
|
"the near machine stays connected across the far port's rename"
|
|
);
|
|
|
|
// A machine joining now is told the new name.
|
|
let port = status.control_port;
|
|
let late = machine("late").await;
|
|
let booth = late
|
|
.create_network_session("Booth", 0, InvitationPolicy::Prompt)
|
|
.await
|
|
.expect("a third network port is created");
|
|
late.connect_peer(booth.id, SocketAddr::from(([127, 0, 0, 1], port)))
|
|
.await
|
|
.expect("the third network port invites the far one");
|
|
let mut told = None;
|
|
for _ in 0..100 {
|
|
told = late
|
|
.session_status(booth.id)
|
|
.await
|
|
.and_then(|status| status.peer_name);
|
|
if told.is_some() {
|
|
break;
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
}
|
|
assert_eq!(
|
|
told.as_deref(),
|
|
Some("Main Stage"),
|
|
"a machine joining after the rename is told the new Bonjour name"
|
|
);
|
|
}
|
|
|
|
/// Proves that a new UDP port moves the listener, releases the old port and connects again to
|
|
/// the machine the network port had connected to, so moving a port does not cost the connection.
|
|
#[tokio::test]
|
|
async fn a_new_udp_port_moves_the_listener_and_reconnects_its_machine() {
|
|
let (near, stage, _far, _front) = joined().await;
|
|
let old = near
|
|
.session_status(stage)
|
|
.await
|
|
.expect("the near network port is running")
|
|
.control_port;
|
|
let new = free_pair();
|
|
|
|
near.update_network_port(
|
|
stage,
|
|
NetworkPortChange {
|
|
control_port: Some(new),
|
|
..NetworkPortChange::default()
|
|
},
|
|
)
|
|
.await
|
|
.expect("the near network port moves to a free UDP port");
|
|
|
|
assert_eq!(
|
|
settings(&near, stage).await.control_port,
|
|
new,
|
|
"the new UDP port is stored so a restart comes back on it"
|
|
);
|
|
let status = near
|
|
.session_status(stage)
|
|
.await
|
|
.expect("the near network port is running");
|
|
assert_eq!(
|
|
status.control_port, new,
|
|
"the running network port listens on the new UDP port"
|
|
);
|
|
assert!(
|
|
UdpSocket::bind(("0.0.0.0", old)).is_ok(),
|
|
"the old UDP port is still held"
|
|
);
|
|
assert!(
|
|
connected(&near, stage).await,
|
|
"the machine it had connected to was not connected to again"
|
|
);
|
|
}
|
|
|
|
/// Proves that a change whose UDP port cannot be bound is refused whole, before the running
|
|
/// network port is touched.
|
|
///
|
|
/// Stopping it and starting it again on the old port would leave a window in which a daemon
|
|
/// starting beside it could take that port (R-082), so the listener's state must not even have
|
|
/// restarted.
|
|
#[tokio::test]
|
|
async fn a_udp_port_that_cannot_be_bound_leaves_the_network_port_where_it_was() {
|
|
let daemon = machine("taken").await;
|
|
let stage = daemon
|
|
.create_network_session("Stage", 0, InvitationPolicy::Prompt)
|
|
.await
|
|
.expect("the network port is created");
|
|
let old = settings(&daemon, stage.id).await.control_port;
|
|
let listening_since = daemon
|
|
.session_status(stage.id)
|
|
.await
|
|
.expect("the network port is running")
|
|
.state
|
|
.since();
|
|
let taken = free_pair();
|
|
let _holder = (
|
|
UdpSocket::bind(("0.0.0.0", taken)).expect("the test holds the UDP port it will ask for"),
|
|
UdpSocket::bind(("::", taken)).ok(),
|
|
);
|
|
|
|
let refused = daemon
|
|
.update_network_port(
|
|
stage.id,
|
|
NetworkPortChange {
|
|
control_port: Some(taken),
|
|
local_name: Some("Main Stage".to_owned()),
|
|
..NetworkPortChange::default()
|
|
},
|
|
)
|
|
.await;
|
|
|
|
assert!(refused.is_err(), "a UDP port in use was accepted");
|
|
let kept = settings(&daemon, stage.id).await;
|
|
assert_eq!(
|
|
kept.control_port, old,
|
|
"the refused UDP port was stored anyway"
|
|
);
|
|
assert_eq!(
|
|
kept.local_name.as_str(),
|
|
"Stage",
|
|
"the refused change was half applied"
|
|
);
|
|
assert_eq!(
|
|
daemon
|
|
.session_status(stage.id)
|
|
.await
|
|
.expect("the network port is still running")
|
|
.control_port,
|
|
old,
|
|
"the network port was left down"
|
|
);
|
|
assert_eq!(
|
|
daemon
|
|
.session_status(stage.id)
|
|
.await
|
|
.expect("the network port is still running")
|
|
.state
|
|
.since(),
|
|
listening_since,
|
|
"the network port was stopped and started again"
|
|
);
|
|
}
|
|
|
|
/// What is tried against Stage, on a daemon where Booth is another network port.
|
|
#[derive(Debug)]
|
|
enum Attempt {
|
|
/// Moves Stage to the UDP port Booth listens on.
|
|
MoveToBoothsPort,
|
|
/// Moves Stage to an even UDP port nothing holds.
|
|
MoveToFreePort,
|
|
/// Changes Stage as given.
|
|
Change(NetworkPortChange),
|
|
/// Makes another network port.
|
|
Create {
|
|
name: &'static str,
|
|
local_name: Option<&'static str>,
|
|
control_port: u16,
|
|
},
|
|
}
|
|
|
|
/// Proves that a network port cannot take a Bonjour name or UDP port another network port holds,
|
|
/// nor an odd UDP port, while a name or port nobody holds is taken.
|
|
///
|
|
/// Two network ports advertising one name look like one machine to every peer, and two on one UDP
|
|
/// port cannot both listen. RTP-MIDI's data port is the one above the control port, so an odd
|
|
/// control port would be moved up one without saying; it is refused instead, naming the even
|
|
/// ports either side.
|
|
#[tokio::test]
|
|
async fn a_name_or_udp_port_another_network_port_holds_is_refused() {
|
|
struct Case {
|
|
name: &'static str,
|
|
attempt: Attempt,
|
|
refused: bool,
|
|
/// Text the refusal must contain, so the user can tell what to change.
|
|
naming: Option<&'static str>,
|
|
}
|
|
let cases = [
|
|
Case {
|
|
name: "a UDP port another network port listens on",
|
|
attempt: Attempt::MoveToBoothsPort,
|
|
refused: true,
|
|
naming: Some("Booth"),
|
|
},
|
|
Case {
|
|
name: "a Bonjour name another network port advertises",
|
|
attempt: Attempt::Change(NetworkPortChange {
|
|
local_name: Some("Booth".to_owned()),
|
|
..NetworkPortChange::default()
|
|
}),
|
|
refused: true,
|
|
naming: None,
|
|
},
|
|
Case {
|
|
name: "an odd UDP port",
|
|
attempt: Attempt::Change(NetworkPortChange {
|
|
control_port: Some(5005),
|
|
..NetworkPortChange::default()
|
|
}),
|
|
refused: true,
|
|
naming: None,
|
|
},
|
|
Case {
|
|
name: "a new network port on an odd UDP port",
|
|
attempt: Attempt::Create {
|
|
name: "Odd",
|
|
local_name: None,
|
|
control_port: 5005,
|
|
},
|
|
refused: true,
|
|
naming: None,
|
|
},
|
|
Case {
|
|
name: "a new network port advertising Stage",
|
|
attempt: Attempt::Create {
|
|
name: "Studio",
|
|
local_name: Some("Stage"),
|
|
control_port: 0,
|
|
},
|
|
refused: true,
|
|
naming: None,
|
|
},
|
|
Case {
|
|
name: "a Bonjour name nobody advertises",
|
|
attempt: Attempt::Change(NetworkPortChange {
|
|
local_name: Some("Main Stage".to_owned()),
|
|
..NetworkPortChange::default()
|
|
}),
|
|
refused: false,
|
|
naming: None,
|
|
},
|
|
Case {
|
|
name: "an even UDP port nothing holds",
|
|
attempt: Attempt::MoveToFreePort,
|
|
refused: false,
|
|
naming: None,
|
|
},
|
|
Case {
|
|
name: "a new network port advertising a name of its own",
|
|
attempt: Attempt::Create {
|
|
name: "Studio",
|
|
local_name: Some("Studio"),
|
|
control_port: 0,
|
|
},
|
|
refused: false,
|
|
naming: None,
|
|
},
|
|
];
|
|
|
|
for case in cases {
|
|
let daemon = machine("held").await;
|
|
let stage = daemon
|
|
.create_network_session("Stage", 0, InvitationPolicy::Prompt)
|
|
.await
|
|
.expect("Stage is created");
|
|
let booth = daemon
|
|
.create_network_session("Booth", 0, InvitationPolicy::Prompt)
|
|
.await
|
|
.expect("Booth is created beside Stage");
|
|
let before = settings(&daemon, stage.id).await;
|
|
|
|
let result = match case.attempt {
|
|
Attempt::MoveToBoothsPort => {
|
|
let port = settings(&daemon, booth.id).await.control_port;
|
|
let change = NetworkPortChange {
|
|
control_port: Some(port),
|
|
..NetworkPortChange::default()
|
|
};
|
|
daemon.update_network_port(stage.id, change).await.map(drop)
|
|
}
|
|
Attempt::MoveToFreePort => {
|
|
let change = NetworkPortChange {
|
|
control_port: Some(free_pair()),
|
|
..NetworkPortChange::default()
|
|
};
|
|
daemon.update_network_port(stage.id, change).await.map(drop)
|
|
}
|
|
Attempt::Change(change) => daemon.update_network_port(stage.id, change).await.map(drop),
|
|
Attempt::Create {
|
|
name,
|
|
local_name,
|
|
control_port,
|
|
} => daemon
|
|
.create_network_port(
|
|
name,
|
|
local_name,
|
|
control_port,
|
|
InvitationPolicy::Prompt,
|
|
true,
|
|
)
|
|
.await
|
|
.map(drop),
|
|
};
|
|
|
|
assert_eq!(
|
|
result.is_err(),
|
|
case.refused,
|
|
"{}: refused should be {}, got {result:?}",
|
|
case.name,
|
|
case.refused
|
|
);
|
|
if let (Some(text), Err(error)) = (case.naming, &result) {
|
|
assert!(
|
|
error.to_string().contains(text),
|
|
"{}: the refusal does not say which network port holds it: {error}",
|
|
case.name
|
|
);
|
|
}
|
|
if case.refused {
|
|
let after = settings(&daemon, stage.id).await;
|
|
assert_eq!(
|
|
after.local_name, before.local_name,
|
|
"{}: a refused change still renamed Stage",
|
|
case.name
|
|
);
|
|
assert_eq!(
|
|
after.control_port, before.control_port,
|
|
"{}: a refused change still moved Stage",
|
|
case.name
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Proves that a new invitation policy reaches the running network port: a machine held waiting
|
|
/// under "ask first" is let in once the network port accepts anyone, without restarting it.
|
|
#[tokio::test]
|
|
async fn a_new_invitation_policy_applies_to_the_running_network_port() {
|
|
let far = machine("policy-far").await;
|
|
let front = far
|
|
.create_network_session("Front of House", 0, InvitationPolicy::Prompt)
|
|
.await
|
|
.expect("the far network port is created");
|
|
let port = far
|
|
.session_status(front.id)
|
|
.await
|
|
.expect("the far network port is listening")
|
|
.control_port;
|
|
let near = machine("policy-near").await;
|
|
let stage = near
|
|
.create_network_session("Stage", 0, InvitationPolicy::Prompt)
|
|
.await
|
|
.expect("the near network port is created");
|
|
near.connect_peer(stage.id, SocketAddr::from(([127, 0, 0, 1], port)))
|
|
.await
|
|
.expect("the near network port invites the far one");
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
assert_ne!(
|
|
far.session_status(front.id)
|
|
.await
|
|
.expect("the far network port is running")
|
|
.state
|
|
.phase(),
|
|
ConnectionPhase::Connected,
|
|
"let in without being asked"
|
|
);
|
|
|
|
far.set_invitation_policy(front.id, InvitationPolicy::AcceptAll)
|
|
.await
|
|
.expect("the far network port's policy changes to accepting everyone");
|
|
|
|
// The near side invites again on its own backoff, so this waits out a few of its tries.
|
|
let mut let_in = false;
|
|
for _ in 0..3 {
|
|
if connected(&near, stage.id).await {
|
|
let_in = true;
|
|
break;
|
|
}
|
|
}
|
|
assert!(
|
|
let_in,
|
|
"the new policy did not reach the running network port"
|
|
);
|
|
}
|
|
|
|
/// Waits for a network port's machines to satisfy a condition, returning the last seen.
|
|
async fn machines_until(
|
|
daemon: &Arc<Daemon>,
|
|
id: EndpointId,
|
|
wanted: impl Fn(&[midi_harbor_daemon::session::Machine]) -> bool,
|
|
) -> Vec<midi_harbor_daemon::session::Machine> {
|
|
let mut seen = Vec::new();
|
|
for _ in 0..200 {
|
|
seen = daemon
|
|
.session_status(id)
|
|
.await
|
|
.map(|status| status.machines)
|
|
.unwrap_or_default();
|
|
if wanted(&seen) {
|
|
break;
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
}
|
|
seen
|
|
}
|
|
|
|
/// Proves that a second machine invited beside the peer is carried and remembered beside it,
|
|
/// that disconnecting the peer leaves the second machine joined and remembered as the peer in its
|
|
/// place, and that a machine no longer taking part cannot be disconnected again.
|
|
///
|
|
/// The peer disconnected by the user is forgotten, so it is not connected to again on the next
|
|
/// start.
|
|
#[tokio::test]
|
|
async fn a_second_machine_joins_beside_the_first_and_each_can_be_disconnected_alone() {
|
|
let (near, stage, far, front) = joined().await;
|
|
let far_port = far
|
|
.session_status(front)
|
|
.await
|
|
.expect("the far network port is running")
|
|
.control_port;
|
|
let booth_machine = machine("booth").await;
|
|
let booth = booth_machine
|
|
.create_network_session("Booth", 0, InvitationPolicy::AcceptAll)
|
|
.await
|
|
.expect("a third network port is created");
|
|
let booth_port = booth_machine
|
|
.session_status(booth.id)
|
|
.await
|
|
.expect("the third network port is listening")
|
|
.control_port;
|
|
let (to_front, to_booth) = (
|
|
SocketAddr::from(([127, 0, 0, 1], far_port)),
|
|
SocketAddr::from(([127, 0, 0, 1], booth_port)),
|
|
);
|
|
assert!(
|
|
settings(&near, stage).await.peer.is_some(),
|
|
"the machine connected first is remembered as the peer"
|
|
);
|
|
|
|
near.invite_machine(stage, to_booth)
|
|
.await
|
|
.expect("the near network port invites a second machine");
|
|
let both = machines_until(&near, stage, |machines| {
|
|
machines.len() == 2 && machines.iter().all(|machine| machine.joined)
|
|
})
|
|
.await;
|
|
assert_eq!(
|
|
both.iter()
|
|
.map(|machine| machine.address)
|
|
.collect::<Vec<_>>(),
|
|
vec![to_front, to_booth],
|
|
"both machines are joined, the peer first: {both:?}"
|
|
);
|
|
|
|
let remembered = settings(&near, stage).await;
|
|
assert_eq!(
|
|
remembered.other_peers.len(),
|
|
1,
|
|
"the second machine is not remembered beside the first"
|
|
);
|
|
let second = remembered.other_peers[0];
|
|
|
|
// The peer goes, and is forgotten. The second machine stays, remembered as the peer.
|
|
near.disconnect_machine(stage, to_front)
|
|
.await
|
|
.expect("the near network port disconnects the first machine");
|
|
let remembered = settings(&near, stage).await;
|
|
assert_eq!(
|
|
remembered.peer,
|
|
Some(second),
|
|
"the machine that took the peer's place is not remembered as the peer"
|
|
);
|
|
assert!(
|
|
remembered.other_peers.is_empty(),
|
|
"the machine promoted to peer is still remembered beside it: {remembered:?}"
|
|
);
|
|
let left = machines_until(&near, stage, |machines| {
|
|
machines.len() == 1 && machines[0].joined
|
|
})
|
|
.await;
|
|
assert_eq!(
|
|
left.len(),
|
|
1,
|
|
"only the second machine is left joined: {left:?}"
|
|
);
|
|
assert_eq!(
|
|
left[0].address, to_booth,
|
|
"disconnecting the first machine left the second one carried"
|
|
);
|
|
|
|
assert!(
|
|
near.disconnect_machine(stage, to_front).await.is_err(),
|
|
"a machine no longer taking part was disconnected again"
|
|
);
|
|
}
|
|
|
|
/// Starts a far machine with a network port accepting everyone, returning it, its network port
|
|
/// and the address to reach that network port at.
|
|
async fn accepting_machine(label: &str, name: &str) -> (Arc<Daemon>, EndpointId, SocketAddr) {
|
|
let daemon = machine(label).await;
|
|
let port = daemon
|
|
.create_network_session(name, 0, InvitationPolicy::AcceptAll)
|
|
.await
|
|
.expect("the far network port is created");
|
|
let control_port = daemon
|
|
.session_status(port.id)
|
|
.await
|
|
.expect("the far network port is listening")
|
|
.control_port;
|
|
(
|
|
daemon,
|
|
port.id,
|
|
SocketAddr::from(([127, 0, 0, 1], control_port)),
|
|
)
|
|
}
|
|
|
|
/// Reports whether every machine listed is carrying MIDI and they are exactly `expected`.
|
|
fn all_joined(machines: &[midi_harbor_daemon::session::Machine], expected: &[SocketAddr]) -> bool {
|
|
machines.iter().all(|machine| machine.joined)
|
|
&& machines
|
|
.iter()
|
|
.map(|machine| machine.address)
|
|
.collect::<Vec<_>>()
|
|
== expected
|
|
}
|
|
|
|
/// Proves that a machine invited straight after the peer is connected is remembered beside the
|
|
/// peer rather than in its place.
|
|
///
|
|
/// Regression: the session's status was read to tell whether the machine became the peer, before
|
|
/// the session had acted on the connect ahead of it. The machine was remembered as the peer, the
|
|
/// peer was forgotten, and after a restart only the machine came back. On one thread the session
|
|
/// cannot act between the two calls, so the old reading fails every time.
|
|
#[tokio::test]
|
|
async fn a_machine_invited_straight_after_connecting_is_remembered_beside_the_peer() {
|
|
let (_front_machine, _, to_front) = accepting_machine("straight-front", "Front of House").await;
|
|
let (_booth_machine, _, to_booth) = accepting_machine("straight-booth", "Booth").await;
|
|
let near = machine("straight-near").await;
|
|
let stage = near
|
|
.create_network_session("Stage", 0, InvitationPolicy::Prompt)
|
|
.await
|
|
.expect("the near network port is created")
|
|
.id;
|
|
near.connect_peer(stage, to_front)
|
|
.await
|
|
.expect("the near network port connects to Front of House");
|
|
near.invite_machine(stage, to_booth)
|
|
.await
|
|
.expect("the near network port invites Booth beside it");
|
|
|
|
let remembered = settings(&near, stage).await;
|
|
let peer = remembered
|
|
.peer
|
|
.expect("the machine connected first is remembered as the peer");
|
|
assert_eq!(
|
|
addresses_of(&near, peer).await,
|
|
vec![to_front.to_string()],
|
|
"the machine invited took the peer's place: {remembered:?}"
|
|
);
|
|
assert_eq!(
|
|
remembered.other_peers.len(),
|
|
1,
|
|
"the invited machine is remembered beside the peer: {remembered:?}"
|
|
);
|
|
assert_eq!(
|
|
addresses_of(&near, remembered.other_peers[0]).await,
|
|
vec![to_booth.to_string()],
|
|
"the machine remembered beside the peer is the one invited"
|
|
);
|
|
}
|
|
|
|
/// Returns the addresses a remembered machine is reached at.
|
|
async fn addresses_of(daemon: &Arc<Daemon>, peer: midi_harbor_core::ids::PeerId) -> Vec<String> {
|
|
daemon
|
|
.read(move |config, _| {
|
|
config
|
|
.peers
|
|
.iter()
|
|
.find(|known| known.id == peer)
|
|
.map(|known| known.addresses.clone())
|
|
.unwrap_or_default()
|
|
})
|
|
.await
|
|
}
|
|
|
|
/// Proves that every machine connected comes back after a restart, and that one the user
|
|
/// disconnected does not (FR-015i).
|
|
///
|
|
/// Regression: only the first machine was remembered, so a second one connected beside it had to
|
|
/// be connected again by hand after every restart.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn every_machine_connected_comes_back_after_a_restart_until_it_is_disconnected() {
|
|
let (_front_machine, _, to_front) = accepting_machine("restart-front", "Front of House").await;
|
|
let (_booth_machine, _, to_booth) = accepting_machine("restart-booth", "Booth").await;
|
|
let root = common::scratch("midi-harbor-network-ports")
|
|
.join(format!("restart-near-{}", uuid::Uuid::new_v4()));
|
|
// Quieted once: `quiet` rewrites the configuration file, which a restart must find as it
|
|
// was left.
|
|
let paths = common::quiet(root);
|
|
let start = || {
|
|
Daemon::start(
|
|
paths.clone(),
|
|
Arc::new(FakeMidiPlatform::new()) as Arc<dyn MidiPlatform>,
|
|
)
|
|
};
|
|
|
|
let first = start()
|
|
.await
|
|
.expect("the near daemon starts over a scratch directory");
|
|
let stage = first
|
|
.create_network_session("Stage", 0, InvitationPolicy::Prompt)
|
|
.await
|
|
.expect("the near network port is created")
|
|
.id;
|
|
first
|
|
.connect_peer(stage, to_front)
|
|
.await
|
|
.expect("the near network port connects to Front of House");
|
|
first
|
|
.invite_machine(stage, to_booth)
|
|
.await
|
|
.expect("the near network port invites Booth beside it");
|
|
let both = machines_until(&first, stage, |machines| {
|
|
all_joined(machines, &[to_front, to_booth])
|
|
})
|
|
.await;
|
|
assert!(
|
|
all_joined(&both, &[to_front, to_booth]),
|
|
"both machines join before the restart: {both:?}"
|
|
);
|
|
|
|
// Both come back after a restart.
|
|
first.end_sessions().await;
|
|
let second = start()
|
|
.await
|
|
.expect("the near daemon starts again over the same directory");
|
|
let back = machines_until(&second, stage, |machines| {
|
|
all_joined(machines, &[to_front, to_booth])
|
|
})
|
|
.await;
|
|
assert!(
|
|
all_joined(&back, &[to_front, to_booth]),
|
|
"only some machines came back: {back:?}"
|
|
);
|
|
|
|
// The second machine, disconnected, is forgotten; the first is not.
|
|
second
|
|
.disconnect_machine(stage, to_booth)
|
|
.await
|
|
.expect("the near network port disconnects Booth");
|
|
let remembered = settings(&second, stage).await;
|
|
assert!(
|
|
remembered.peer.is_some(),
|
|
"disconnecting the second machine forgot the peer too: {remembered:?}"
|
|
);
|
|
assert!(
|
|
remembered.other_peers.is_empty(),
|
|
"the machine the user disconnected is still remembered: {remembered:?}"
|
|
);
|
|
|
|
second.end_sessions().await;
|
|
let third = start()
|
|
.await
|
|
.expect("the near daemon starts a third time over the same directory");
|
|
assert!(
|
|
connected(&third, stage).await,
|
|
"the first did not come back"
|
|
);
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
let machines = machines_until(&third, stage, |machines| machines.len() == 1).await;
|
|
assert_eq!(
|
|
machines
|
|
.iter()
|
|
.map(|machine| machine.address)
|
|
.collect::<Vec<_>>(),
|
|
vec![to_front],
|
|
"a machine the user disconnected came back after a restart"
|
|
);
|
|
}
|
|
|
|
/// Proves that a machine connected beside the peer, whose network port goes away, stays listed
|
|
/// and waiting without disturbing the peer, and is connected again without being asked when it
|
|
/// returns on the same UDP port.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn a_machine_connected_beside_the_first_comes_back_when_its_link_returns() {
|
|
let (_front_machine, _, to_front) = accepting_machine("drop-front", "Front of House").await;
|
|
let (booth_machine, booth, to_booth) = accepting_machine("drop-booth", "Booth").await;
|
|
let near = machine("drop-near").await;
|
|
let stage = near
|
|
.create_network_session("Stage", 0, InvitationPolicy::Prompt)
|
|
.await
|
|
.expect("the near network port is created")
|
|
.id;
|
|
near.connect_peer(stage, to_front)
|
|
.await
|
|
.expect("the near network port connects to Front of House");
|
|
near.invite_machine(stage, to_booth)
|
|
.await
|
|
.expect("the near network port invites Booth beside it");
|
|
let both = machines_until(&near, stage, |machines| {
|
|
all_joined(machines, &[to_front, to_booth])
|
|
})
|
|
.await;
|
|
assert!(
|
|
all_joined(&both, &[to_front, to_booth]),
|
|
"both machines join before Booth goes away: {both:?}"
|
|
);
|
|
|
|
// The second machine's network port goes away.
|
|
booth_machine
|
|
.set_enabled(booth, false)
|
|
.await
|
|
.expect("Booth's network port is switched off");
|
|
let waiting = machines_until(&near, stage, |machines| {
|
|
machines
|
|
.iter()
|
|
.any(|machine| machine.address == to_booth && !machine.joined)
|
|
})
|
|
.await;
|
|
assert!(
|
|
waiting
|
|
.iter()
|
|
.any(|machine| machine.address == to_booth && !machine.joined),
|
|
"the second machine was let go: {waiting:?}"
|
|
);
|
|
assert!(
|
|
connected(&near, stage).await,
|
|
"the first machine was dropped"
|
|
);
|
|
|
|
// It returns on the same UDP port.
|
|
booth_machine
|
|
.set_enabled(booth, true)
|
|
.await
|
|
.expect("Booth's network port is switched on again");
|
|
let back = machines_until(&near, stage, |machines| {
|
|
all_joined(machines, &[to_front, to_booth])
|
|
})
|
|
.await;
|
|
assert!(
|
|
all_joined(&back, &[to_front, to_booth]),
|
|
"the second machine did not come back: {back:?}"
|
|
);
|
|
}
|
|
|
|
/// Proves that a machine invited while nothing is connected becomes the remembered peer, so it
|
|
/// is connected to again after a restart like a machine connected to directly.
|
|
#[tokio::test]
|
|
async fn inviting_a_machine_with_nothing_connected_makes_it_the_remembered_peer() {
|
|
let (_far, _, to_far) = accepting_machine("alone-far", "Front of House").await;
|
|
let near = machine("alone-near").await;
|
|
let stage = near
|
|
.create_network_session("Stage", 0, InvitationPolicy::Prompt)
|
|
.await
|
|
.expect("the near network port is created");
|
|
|
|
near.invite_machine(stage.id, to_far)
|
|
.await
|
|
.expect("the near network port invites Front of House");
|
|
|
|
assert!(
|
|
connected(&near, stage.id).await,
|
|
"a machine accepting everyone lets the invited one in"
|
|
);
|
|
assert!(
|
|
settings(&near, stage.id).await.peer.is_some(),
|
|
"the only machine is not connected again after a restart"
|
|
);
|
|
}
|
|
|
|
/// Builds a far machine asking about invitations and a near one inviting it, returning the far
|
|
/// daemon, its network port, and the near side's network port with the far port to invite.
|
|
async fn asking(label: &str) -> (Arc<Daemon>, EndpointId, Arc<Daemon>, EndpointId, SocketAddr) {
|
|
let far = machine(&format!("{label}-far")).await;
|
|
let front = far
|
|
.create_network_session("Front of House", 0, InvitationPolicy::Prompt)
|
|
.await
|
|
.expect("the far network port is created");
|
|
let port = far
|
|
.session_status(front.id)
|
|
.await
|
|
.expect("the far network port is listening")
|
|
.control_port;
|
|
let near = machine(&format!("{label}-near")).await;
|
|
let stage = near
|
|
.create_network_session("Stage", 0, InvitationPolicy::Prompt)
|
|
.await
|
|
.expect("the near network port is created");
|
|
(
|
|
far,
|
|
front.id,
|
|
near,
|
|
stage.id,
|
|
SocketAddr::from(([127, 0, 0, 1], port)),
|
|
)
|
|
}
|
|
|
|
/// Waits for an invitation to be waiting on the user, returning its identifier.
|
|
async fn asked(daemon: &Arc<Daemon>) -> Option<String> {
|
|
for _ in 0..100 {
|
|
if let Some(invitation) = daemon.pending_invitations().await.first() {
|
|
return Some(invitation.id.clone());
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
}
|
|
None
|
|
}
|
|
|
|
/// How the far machine came to let the near one in.
|
|
#[derive(Clone, Copy, Debug)]
|
|
enum Grant {
|
|
/// The user accepted its invitation once, for this run.
|
|
AnsweredOnce,
|
|
/// It was added as a known machine and trusted.
|
|
Trusted,
|
|
/// The user accepted its invitation once, and then it was added as a trusted machine named
|
|
/// "Stage machine".
|
|
AnsweredOnceAndTrusted,
|
|
}
|
|
|
|
/// How the far machine took that permission away.
|
|
#[derive(Clone, Copy, Debug)]
|
|
enum Withdraw {
|
|
/// Trust was switched off for its address.
|
|
TrustSwitchedOff,
|
|
/// It was added again as a known machine named "Stage machine", untrusted.
|
|
AddedAgainUntrusted,
|
|
/// The known machine "Stage machine" was forgotten.
|
|
Forgotten,
|
|
}
|
|
|
|
/// Proves that once permission to join is withdrawn, however it was given and however it was
|
|
/// withdrawn, the machine's next invitation is asked about rather than let in.
|
|
///
|
|
/// Each row connects, disconnects with `disconnect_peer`, withdraws permission and connects
|
|
/// again, so it also proves that a session disconnected and connected again sends a fresh
|
|
/// invitation, and that a machine answered once is connected. Regression: an invitation accepted
|
|
/// once let the machine in until the daemon restarted, whatever trust later said, and a machine
|
|
/// added again untrusted kept the trust it had.
|
|
#[tokio::test]
|
|
async fn once_trust_is_withdrawn_the_next_invitation_is_asked_about() {
|
|
struct Case {
|
|
name: &'static str,
|
|
grant: Grant,
|
|
withdraw: Withdraw,
|
|
}
|
|
let cases = [
|
|
Case {
|
|
name: "answered once, then added untrusted",
|
|
grant: Grant::AnsweredOnce,
|
|
withdraw: Withdraw::AddedAgainUntrusted,
|
|
},
|
|
Case {
|
|
name: "answered once and trusted, then trust switched off",
|
|
grant: Grant::AnsweredOnceAndTrusted,
|
|
withdraw: Withdraw::TrustSwitchedOff,
|
|
},
|
|
Case {
|
|
name: "answered once and trusted, then forgotten",
|
|
grant: Grant::AnsweredOnceAndTrusted,
|
|
withdraw: Withdraw::Forgotten,
|
|
},
|
|
Case {
|
|
name: "trusted, then trust switched off",
|
|
grant: Grant::Trusted,
|
|
withdraw: Withdraw::TrustSwitchedOff,
|
|
},
|
|
Case {
|
|
name: "trusted, then added again untrusted",
|
|
grant: Grant::Trusted,
|
|
withdraw: Withdraw::AddedAgainUntrusted,
|
|
},
|
|
];
|
|
|
|
for case in cases {
|
|
let (far, front, near, stage, to_far) = asking("trust").await;
|
|
|
|
// Grant permission, and connect under it.
|
|
match case.grant {
|
|
Grant::AnsweredOnce | Grant::AnsweredOnceAndTrusted => {
|
|
near.connect_peer(stage, to_far)
|
|
.await
|
|
.expect("the near network port invites the far one");
|
|
let invitation = asked(&far).await.unwrap_or_else(|| {
|
|
panic!("{}: an unknown machine was not asked about", case.name)
|
|
});
|
|
far.respond_to_invitation(&invitation, true, false)
|
|
.await
|
|
.expect("the far machine accepts the invitation once");
|
|
if matches!(case.grant, Grant::AnsweredOnceAndTrusted) {
|
|
far.add_known_machine("127.0.0.1", Some("Stage machine".to_owned()), true)
|
|
.await
|
|
.expect("the far machine remembers the near one as trusted");
|
|
}
|
|
}
|
|
Grant::Trusted => {
|
|
far.add_known_machine("127.0.0.1", None, true)
|
|
.await
|
|
.expect("the far machine trusts the near one");
|
|
}
|
|
}
|
|
near.connect_peer(stage, to_far)
|
|
.await
|
|
.expect("the near network port invites the far one again");
|
|
assert!(
|
|
connected(&near, stage).await,
|
|
"{}: a machine given permission was not let in",
|
|
case.name
|
|
);
|
|
|
|
// Withdraw it, and invite again.
|
|
near.disconnect_peer(stage)
|
|
.await
|
|
.expect("the near network port leaves the far one");
|
|
match case.withdraw {
|
|
Withdraw::TrustSwitchedOff => {
|
|
let known = far
|
|
.set_peer_trusted("127.0.0.1", false)
|
|
.await
|
|
.expect("the far machine switches trust off");
|
|
assert!(
|
|
!known.trusted,
|
|
"{}: switching trust off reports the machine as still trusted",
|
|
case.name
|
|
);
|
|
}
|
|
Withdraw::AddedAgainUntrusted => {
|
|
let again = far
|
|
.add_known_machine("127.0.0.1", Some("Stage machine".to_owned()), false)
|
|
.await
|
|
.expect("the far machine adds the near one again, untrusted");
|
|
assert!(
|
|
!again.trusted,
|
|
"{}: the second add left it trusted",
|
|
case.name
|
|
);
|
|
assert_eq!(
|
|
again.name, "Stage machine",
|
|
"{}: adding a known machine again takes the name given",
|
|
case.name
|
|
);
|
|
}
|
|
Withdraw::Forgotten => {
|
|
far.remove_peer("Stage machine")
|
|
.await
|
|
.expect("the far machine forgets the near one");
|
|
}
|
|
}
|
|
near.connect_peer(stage, to_far)
|
|
.await
|
|
.expect("the near network port invites the far one a third time");
|
|
|
|
assert!(
|
|
asked(&far).await.is_some(),
|
|
"{}: a machine whose permission was withdrawn was let in without asking",
|
|
case.name
|
|
);
|
|
assert_ne!(
|
|
far.session_status(front)
|
|
.await
|
|
.expect("the far network port is running")
|
|
.state
|
|
.phase(),
|
|
ConnectionPhase::Connected,
|
|
"{}: the far network port connected while the invitation waited on the user",
|
|
case.name
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Proves that deleting a network port drops the invitations waiting on it, so the user is not
|
|
/// asked about joining something that no longer exists.
|
|
#[tokio::test]
|
|
async fn deleting_a_network_port_drops_the_invitations_waiting_on_it() {
|
|
let (far, front, near, stage, to_far) = asking("deleted").await;
|
|
near.connect_peer(stage, to_far)
|
|
.await
|
|
.expect("the near network port invites the far one");
|
|
assert!(
|
|
asked(&far).await.is_some(),
|
|
"a far network port that asks first holds the invitation for the user"
|
|
);
|
|
|
|
far.delete_network_port(front)
|
|
.await
|
|
.expect("the far network port is deleted");
|
|
|
|
assert!(
|
|
far.pending_invitations().await.is_empty(),
|
|
"an invitation to a deleted network port is still waiting"
|
|
);
|
|
}
|
|
|
|
/// Returns how many endpoints hold a name.
|
|
async fn holding(daemon: &Arc<Daemon>, name: &str) -> usize {
|
|
daemon
|
|
.read(|config, _| {
|
|
config
|
|
.endpoints
|
|
.iter()
|
|
.filter(|e| e.name.as_str() == name)
|
|
.count()
|
|
})
|
|
.await
|
|
}
|
|
|
|
/// A name claimed on a daemon holding the port Keys, the network port Stage, and the network port
|
|
/// Booth without its automatic port.
|
|
#[derive(Debug)]
|
|
enum Claim {
|
|
/// Makes a port.
|
|
Port(&'static str),
|
|
/// Makes a network port, with or without its automatic port.
|
|
NetworkPort {
|
|
name: &'static str,
|
|
automatic_port: bool,
|
|
},
|
|
/// Renames an endpoint.
|
|
Rename {
|
|
from: &'static str,
|
|
to: &'static str,
|
|
},
|
|
/// Imports a configuration, merged with the one held.
|
|
Import(&'static str),
|
|
}
|
|
|
|
/// What a claim should come to.
|
|
#[derive(Debug)]
|
|
enum Outcome {
|
|
/// It is taken.
|
|
Accepted,
|
|
/// It is refused as a name already in use, naming that name.
|
|
Taken(&'static str),
|
|
/// It is refused with a message containing this text.
|
|
Reported(&'static str),
|
|
}
|
|
|
|
/// Proves that ports and network ports share one namespace, whichever is made, renamed or
|
|
/// imported second, while an endpoint keeping its own name is not a clash with itself.
|
|
///
|
|
/// Other applications see a network port as a port of its name, so the two would show as
|
|
/// identical ports. A network port without its automatic port holds its name too, because
|
|
/// switching the automatic port on later would bring the clash back. A merging import matches
|
|
/// endpoints by kind and name, so it would add the network port beside the port of its name. A
|
|
/// rename that keeps the name is what an edit saved unchanged sends.
|
|
#[tokio::test]
|
|
async fn a_port_and_a_network_port_cannot_share_a_name() {
|
|
struct Case {
|
|
name: &'static str,
|
|
claim: Claim,
|
|
want: Outcome,
|
|
}
|
|
let cases = [
|
|
Case {
|
|
name: "a port under a network port's name",
|
|
claim: Claim::Port("Stage"),
|
|
want: Outcome::Taken("Stage"),
|
|
},
|
|
Case {
|
|
name: "a port under a network port's name with spaces around it",
|
|
claim: Claim::Port(" Stage "),
|
|
want: Outcome::Taken("Stage"),
|
|
},
|
|
Case {
|
|
name: "a port under the name of a network port without its automatic port",
|
|
claim: Claim::Port("Booth"),
|
|
want: Outcome::Taken("Booth"),
|
|
},
|
|
Case {
|
|
name: "a network port under a port's name",
|
|
claim: Claim::NetworkPort {
|
|
name: "Keys",
|
|
automatic_port: true,
|
|
},
|
|
want: Outcome::Taken("Keys"),
|
|
},
|
|
Case {
|
|
name: "a network port without its automatic port under a port's name",
|
|
claim: Claim::NetworkPort {
|
|
name: "Keys",
|
|
automatic_port: false,
|
|
},
|
|
want: Outcome::Taken("Keys"),
|
|
},
|
|
Case {
|
|
name: "a port renamed to a network port's name",
|
|
claim: Claim::Rename {
|
|
from: "Keys",
|
|
to: "Stage",
|
|
},
|
|
want: Outcome::Taken("Stage"),
|
|
},
|
|
Case {
|
|
name: "a network port renamed to a port's name",
|
|
claim: Claim::Rename {
|
|
from: "Stage",
|
|
to: "Keys",
|
|
},
|
|
want: Outcome::Taken("Keys"),
|
|
},
|
|
Case {
|
|
name: "a network port imported under a port's name",
|
|
claim: Claim::Import(
|
|
"endpoints:\n\
|
|
- name: Keys\n\
|
|
\x20 kind: network_port\n\
|
|
\x20 control_port: 0\n",
|
|
),
|
|
want: Outcome::Reported("both named 'Keys'"),
|
|
},
|
|
Case {
|
|
name: "a port renamed to its own name",
|
|
claim: Claim::Rename {
|
|
from: "Keys",
|
|
to: "Keys",
|
|
},
|
|
want: Outcome::Accepted,
|
|
},
|
|
Case {
|
|
name: "a network port renamed to its own name",
|
|
claim: Claim::Rename {
|
|
from: "Stage",
|
|
to: "Stage",
|
|
},
|
|
want: Outcome::Accepted,
|
|
},
|
|
];
|
|
|
|
for case in cases {
|
|
let daemon = machine("names").await;
|
|
daemon
|
|
.create_virtual_port("Keys", 1, 1)
|
|
.await
|
|
.expect("the port Keys is created");
|
|
daemon
|
|
.create_network_session("Stage", 0, InvitationPolicy::Prompt)
|
|
.await
|
|
.expect("the network port Stage is created");
|
|
daemon
|
|
.create_network_port("Booth", None, 0, InvitationPolicy::Prompt, false)
|
|
.await
|
|
.expect("the network port Booth is created without its automatic port");
|
|
|
|
let result = match case.claim {
|
|
Claim::Port(name) => daemon.create_virtual_port(name, 1, 1).await.map(drop),
|
|
Claim::NetworkPort {
|
|
name,
|
|
automatic_port,
|
|
} => daemon
|
|
.create_network_port(name, None, 0, InvitationPolicy::Prompt, automatic_port)
|
|
.await
|
|
.map(drop),
|
|
Claim::Rename { from, to } => {
|
|
let id = daemon
|
|
.resolve(from)
|
|
.await
|
|
.expect("the endpoint to rename exists");
|
|
daemon.rename_endpoint(id, to, true).await.map(drop)
|
|
}
|
|
Claim::Import(text) => daemon.import_configuration(text, false).await.map(drop),
|
|
};
|
|
|
|
let as_wanted = match (&case.want, &result) {
|
|
(Outcome::Accepted, Ok(())) => true,
|
|
(
|
|
Outcome::Taken(name),
|
|
Err(DaemonError::Failure(FailureReason::NameConflict { name: taken })),
|
|
) => taken == name,
|
|
(Outcome::Reported(text), Err(error)) => error.to_string().contains(text),
|
|
_ => false,
|
|
};
|
|
assert!(
|
|
as_wanted,
|
|
"{}: expected {:?}, got {result:?}",
|
|
case.name, case.want
|
|
);
|
|
for held in ["Keys", "Stage", "Booth"] {
|
|
assert_eq!(
|
|
holding(&daemon, held).await,
|
|
1,
|
|
"{}: {held} is no longer held by exactly one endpoint",
|
|
case.name
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Proves that a hand-written configuration holding a port and a network port of one name keeps
|
|
/// both and records the clash in the history as an error.
|
|
///
|
|
/// Dropping either would lose part of the user's setup, and saying nothing would leave two
|
|
/// identical ports with no explanation.
|
|
#[tokio::test]
|
|
async fn a_hand_written_port_and_network_port_of_one_name_are_kept_and_reported() {
|
|
let root = common::scratch("midi-harbor-network-ports")
|
|
.join(format!("hand-written-{}", uuid::Uuid::new_v4()));
|
|
let paths = midi_harbor_core::paths::Paths::rooted_at(root);
|
|
std::fs::create_dir_all(paths.config_dir()).expect("the configuration directory is created");
|
|
std::fs::write(
|
|
paths.config_file(),
|
|
"preferences:\n\
|
|
\x20 advertise_sessions: false\n\
|
|
endpoints:\n\
|
|
- name: Stage\n\
|
|
\x20 kind: virtual_port\n\
|
|
- name: Stage\n\
|
|
\x20 kind: network_port\n\
|
|
\x20 control_port: 0\n",
|
|
)
|
|
.expect("the hand-written configuration is written");
|
|
|
|
let daemon = Daemon::start(
|
|
paths,
|
|
Arc::new(FakeMidiPlatform::new()) as Arc<dyn MidiPlatform>,
|
|
)
|
|
.await
|
|
.expect("the daemon starts over the hand-written configuration");
|
|
|
|
assert_eq!(
|
|
holding(&daemon, "Stage").await,
|
|
2,
|
|
"both endpoints named Stage are kept"
|
|
);
|
|
let reported = daemon.events(None, 100).await.into_iter().any(|event| {
|
|
event.severity == midi_harbor_core::events::Severity::Error
|
|
&& event.detail.contains("both named 'Stage'")
|
|
});
|
|
assert!(reported, "the clash was not recorded in the history");
|
|
}
|