Reorganize protocol test modules

This commit is contained in:
Michael Mikovsky
2026-06-01 13:57:56 -06:00
parent 921ea838c4
commit 2d5f04a024
35 changed files with 1519 additions and 1336 deletions
+1 -5
View File
@@ -32,8 +32,4 @@ pub type PacketQueue = VecDeque<Packet>;
type RouteMap = Vec<(EndpointName, PacketQueue)>; type RouteMap = Vec<(EndpointName, PacketQueue)>;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests;
mod merkle_sync;
mod oneshot;
mod packet;
}
@@ -0,0 +1,94 @@
use alloc::vec;
use crate::protocol::{Endpoint, EndpointError, RouteDirection};
use super::super::support::{
assertions::{assert_hook_present, assert_hook_removed},
endpoints::{ENDPOINT_A, ENDPOINT_B, ENDPOINT_C, endpoint_at, single_outbound_packet},
packets::{echo_packet, echo_packet_with_end},
};
#[test]
fn inbound_downward_packet_routes_to_immediate_child() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.add_connection(ENDPOINT_A, true);
endpoint.add_connection(ENDPOINT_C, false);
endpoint
.add_inbound_from(
ENDPOINT_A,
echo_packet(vec![ENDPOINT_A, ENDPOINT_B, ENDPOINT_C], hook_id),
)
.unwrap();
let packet = single_outbound_packet(&endpoint, ENDPOINT_C);
assert!(!packet.end_hook);
assert_eq!(packet.path, vec![ENDPOINT_A, ENDPOINT_B, ENDPOINT_C]);
assert_hook_present(&endpoint, hook_id);
assert_eq!(endpoint.hook_peer(hook_id), Some(ENDPOINT_C));
assert!(!Endpoint::route_contains(ENDPOINT_A, &endpoint.outbound));
}
#[test]
fn outbound_downward_packet_routes_to_immediate_child() {
let mut endpoint = endpoint_at(ENDPOINT_A, vec![ENDPOINT_A]);
let hook_id = endpoint.get_hook_id();
endpoint.accept_hook(hook_id, ENDPOINT_B);
endpoint.add_connection(ENDPOINT_B, false);
endpoint
.add_outbound(echo_packet_with_end(
vec![ENDPOINT_A, ENDPOINT_B, ENDPOINT_C],
hook_id,
true,
))
.unwrap();
let packet = single_outbound_packet(&endpoint, ENDPOINT_B);
assert!(packet.end_hook);
assert_eq!(packet.path, vec![ENDPOINT_A, ENDPOINT_B, ENDPOINT_C]);
assert_hook_removed(&endpoint, hook_id);
assert!(!Endpoint::route_contains(ENDPOINT_C, &endpoint.outbound));
}
#[test]
fn downward_outbound_without_hook_is_allowed() {
let mut endpoint = endpoint_at(ENDPOINT_A, vec![ENDPOINT_A]);
endpoint.add_connection(ENDPOINT_B, false);
let new_hook = endpoint.get_hook_id();
endpoint
.add_outbound(echo_packet(vec![ENDPOINT_A, ENDPOINT_B], new_hook))
.unwrap();
assert_eq!(
Endpoint::route_get(ENDPOINT_B, &endpoint.outbound)
.unwrap()
.len(),
1
);
assert_hook_present(&endpoint, new_hook);
assert_eq!(endpoint.hook_peer(new_hook), Some(ENDPOINT_B));
}
#[test]
fn downward_route_without_connection_is_rejected() {
let mut endpoint = endpoint_at(ENDPOINT_A, vec![ENDPOINT_A]);
let hook_id = endpoint.get_hook_id();
let error = endpoint
.add_outbound(echo_packet(vec![ENDPOINT_A, ENDPOINT_B], hook_id))
.unwrap_err();
assert!(matches!(
error,
EndpointError::MissingConnection {
next_hop: ENDPOINT_B,
direction: RouteDirection::Downward,
}
));
assert_hook_removed(&endpoint, hook_id);
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
@@ -0,0 +1,48 @@
use alloc::vec;
use crate::protocol::{Endpoint, EndpointError, RouteDirection};
use super::super::support::{
assertions::{assert_hook_present, assert_hook_removed},
endpoints::{ENDPOINT_A, ENDPOINT_B, endpoint_at, single_outbound_packet},
packets::echo_packet_with_end,
};
#[test]
fn end_hook_removes_hook_after_packet_is_queued() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.accept_hook(hook_id, ENDPOINT_A);
endpoint.add_connection(ENDPOINT_A, true);
endpoint
.add_outbound(echo_packet_with_end(vec![ENDPOINT_A], hook_id, true))
.unwrap();
assert_hook_removed(&endpoint, hook_id);
assert_eq!(
single_outbound_packet(&endpoint, ENDPOINT_A).hook_id,
hook_id
);
}
#[test]
fn failed_end_hook_route_keeps_hook_state() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.accept_hook(hook_id, ENDPOINT_A);
let error = endpoint
.add_outbound(echo_packet_with_end(vec![ENDPOINT_A], hook_id, true))
.unwrap_err();
assert!(matches!(
error,
EndpointError::MissingConnection {
next_hop: ENDPOINT_A,
direction: RouteDirection::Upward,
}
));
assert_hook_present(&endpoint, hook_id);
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
@@ -0,0 +1,70 @@
use alloc::vec;
use crate::protocol::{Endpoint, EndpointError};
use super::super::support::{
assertions::{assert_hook_present, assert_hook_removed},
endpoints::{ENDPOINT_A, ENDPOINT_B, endpoint_at, single_inbound_packet},
packets::echo_packet,
};
#[test]
fn inbound_downward_packet_for_local_endpoint_opens_hook() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.add_connection(ENDPOINT_A, true);
endpoint
.add_inbound_from(
ENDPOINT_A,
echo_packet(vec![ENDPOINT_A, ENDPOINT_B], hook_id),
)
.unwrap();
let packet = single_inbound_packet(&endpoint, ENDPOINT_B);
assert!(!packet.end_hook);
assert_eq!(packet.path, vec![ENDPOINT_A, ENDPOINT_B]);
assert_hook_present(&endpoint, hook_id);
assert_eq!(endpoint.hook_peer(hook_id), Some(ENDPOINT_A));
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn outbound_packet_for_local_endpoint_is_delivered_locally() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint
.add_outbound(echo_packet(vec![ENDPOINT_A, ENDPOINT_B], hook_id))
.unwrap();
let packet = single_inbound_packet(&endpoint, ENDPOINT_B);
assert!(!packet.end_hook);
assert_eq!(packet.data, "ABC123".as_bytes());
assert_hook_removed(&endpoint, hook_id);
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn inbound_without_absolute_path_is_rejected() {
let mut endpoint = Endpoint::new(ENDPOINT_A);
let error = endpoint
.add_inbound(echo_packet(vec![ENDPOINT_A], 1))
.unwrap_err();
assert!(matches!(error, EndpointError::EndpointPathUnset));
assert!(Endpoint::routes_is_empty(&endpoint.inbound));
}
#[test]
fn outbound_without_absolute_path_is_rejected() {
let mut endpoint = Endpoint::new(ENDPOINT_A);
let error = endpoint
.add_outbound(echo_packet(vec![ENDPOINT_A], 1))
.unwrap_err();
assert!(matches!(error, EndpointError::EndpointPathUnset));
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
@@ -0,0 +1,112 @@
use alloc::vec;
use crate::protocol::{Endpoint, EndpointError, Leaf};
use super::super::support::{
assertions::assert_hook_present,
endpoints::{ENDPOINT_A, ENDPOINT_B, ENDPOINT_C, endpoint_at, single_inbound_packet},
packets::{echo_packet, echo_packet_with_end},
transport::CommsLeaf,
};
#[test]
fn forged_sideways_packet_is_rejected_as_incorrect_path() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.accept_hook(hook_id, ENDPOINT_A);
endpoint.add_connection(ENDPOINT_A, true);
let error = endpoint
.add_inbound_from(
ENDPOINT_A,
echo_packet(vec![ENDPOINT_A, ENDPOINT_C], hook_id),
)
.unwrap_err();
assert!(matches!(error, EndpointError::DestinationOutsideLocalTree));
assert_hook_present(&endpoint, hook_id);
assert!(Endpoint::routes_is_empty(&endpoint.inbound));
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn malformed_frame_is_dropped_by_comms_leaf() {
let (tx_to_endpoint, rx_for_endpoint) = crossbeam_channel::unbounded();
let (tx_unused, _rx_unused) = crossbeam_channel::unbounded();
let mut endpoint = Endpoint::new(ENDPOINT_B);
let mut comms = CommsLeaf {
tx: tx_unused,
rx: rx_for_endpoint,
remote_id: ENDPOINT_A,
is_authority: true,
started: false,
};
endpoint.path = vec![ENDPOINT_A, ENDPOINT_B];
tx_to_endpoint.send(vec![0, 1, 2, 3]).unwrap();
comms.update(&mut endpoint);
assert!(Endpoint::routes_is_empty(&endpoint.inbound));
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn malformed_frame_does_not_block_following_valid_packet() {
let (tx_to_endpoint, rx_for_endpoint) = crossbeam_channel::unbounded();
let (tx_unused, _rx_unused) = crossbeam_channel::unbounded();
let hook_id = 42;
let mut endpoint = Endpoint::new(ENDPOINT_B);
let mut comms = CommsLeaf {
tx: tx_unused,
rx: rx_for_endpoint,
remote_id: ENDPOINT_A,
is_authority: true,
started: false,
};
endpoint.path = vec![ENDPOINT_A, ENDPOINT_B];
tx_to_endpoint.send(vec![0, 1, 2, 3]).unwrap();
tx_to_endpoint
.send(
echo_packet(vec![ENDPOINT_A, ENDPOINT_B], hook_id)
.serialize()
.unwrap(),
)
.unwrap();
comms.update(&mut endpoint);
let packet = single_inbound_packet(&endpoint, ENDPOINT_B);
assert!(!packet.end_hook);
assert_eq!(packet.hook_id, hook_id);
assert_hook_present(&endpoint, hook_id);
}
#[test]
fn forged_frame_without_required_hook_is_dropped_by_comms_leaf() {
let (tx_to_endpoint, rx_for_endpoint) = crossbeam_channel::unbounded();
let (tx_unused, _rx_unused) = crossbeam_channel::unbounded();
let mut endpoint = Endpoint::new(ENDPOINT_B);
let mut comms = CommsLeaf {
tx: tx_unused,
rx: rx_for_endpoint,
remote_id: ENDPOINT_C,
is_authority: false,
started: false,
};
endpoint.path = vec![ENDPOINT_A, ENDPOINT_B];
endpoint.accept_hook(7, ENDPOINT_C);
endpoint.add_connection(ENDPOINT_A, true);
tx_to_endpoint
.send(
echo_packet_with_end(vec![ENDPOINT_A], 12, true)
.serialize()
.unwrap(),
)
.unwrap();
comms.update(&mut endpoint);
assert_hook_present(&endpoint, 7);
assert!(Endpoint::routes_is_empty(&endpoint.inbound));
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
+5
View File
@@ -0,0 +1,5 @@
mod downward_routing;
mod hook_lifecycle;
mod local_delivery;
mod malformed_or_forged;
mod upward_routing;
@@ -0,0 +1,143 @@
use alloc::vec;
use crate::protocol::{Endpoint, EndpointError, RouteDirection};
use super::super::support::{
assertions::{assert_hook_present, assert_hook_removed},
endpoints::{ENDPOINT_A, ENDPOINT_B, ENDPOINT_C, endpoint_at, single_outbound_packet},
packets::echo_packet_with_end,
};
#[test]
fn inbound_upward_packet_with_hook_routes_to_parent() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.accept_hook(hook_id, ENDPOINT_C);
endpoint.add_connection(ENDPOINT_A, true);
endpoint.add_connection(ENDPOINT_C, false);
endpoint
.add_inbound_from(
ENDPOINT_C,
echo_packet_with_end(vec![ENDPOINT_A], hook_id, true),
)
.unwrap();
let packet = single_outbound_packet(&endpoint, ENDPOINT_A);
assert!(packet.end_hook);
assert_eq!(packet.hook_id, hook_id);
assert_hook_removed(&endpoint, hook_id);
assert!(!Endpoint::route_contains(ENDPOINT_C, &endpoint.outbound));
}
#[test]
fn inbound_upward_packet_without_hook_is_rejected() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.add_connection(ENDPOINT_A, true);
endpoint.add_connection(ENDPOINT_C, false);
let error = endpoint
.add_inbound_from(
ENDPOINT_C,
echo_packet_with_end(vec![ENDPOINT_A], hook_id, true),
)
.unwrap_err();
assert!(matches!(
error,
EndpointError::UnknownHook { hook_id: observed_hook_id } if observed_hook_id == hook_id
));
assert!(Endpoint::routes_is_empty(&endpoint.inbound));
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn forged_upward_packet_with_unknown_hook_is_rejected() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
endpoint.accept_hook(7, ENDPOINT_C);
endpoint.add_connection(ENDPOINT_A, true);
endpoint.add_connection(ENDPOINT_C, false);
let error = endpoint
.add_inbound_from(ENDPOINT_C, echo_packet_with_end(vec![ENDPOINT_A], 99, true))
.unwrap_err();
assert!(matches!(error, EndpointError::UnknownHook { hook_id: 99 }));
assert_hook_present(&endpoint, 7);
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn upward_outbound_without_hook_is_rejected() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
endpoint.accept_hook(7, ENDPOINT_A);
endpoint.add_connection(ENDPOINT_A, true);
let new_hook = endpoint.get_hook_id();
let error = endpoint
.add_outbound(echo_packet_with_end(vec![ENDPOINT_A], new_hook, true))
.unwrap_err();
assert!(matches!(
error,
EndpointError::UnknownHook { hook_id: observed_hook_id } if observed_hook_id == new_hook
));
assert_hook_present(&endpoint, 7);
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn deeper_upward_route_uses_parent_as_next_hop() {
let mut endpoint = endpoint_at(ENDPOINT_C, vec![ENDPOINT_A, ENDPOINT_B, ENDPOINT_C]);
let new_hook = endpoint.get_hook_id();
endpoint.accept_hook(new_hook, ENDPOINT_B);
endpoint.add_connection(ENDPOINT_B, true);
endpoint
.add_outbound(echo_packet_with_end(vec![ENDPOINT_A], new_hook, true))
.unwrap();
assert!(Endpoint::route_contains(ENDPOINT_B, &endpoint.outbound));
assert!(!Endpoint::route_contains(ENDPOINT_A, &endpoint.outbound));
assert_hook_removed(&endpoint, new_hook);
}
#[test]
fn upward_route_without_connection_is_rejected_even_with_hook() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.accept_hook(hook_id, ENDPOINT_A);
let error = endpoint
.add_outbound(echo_packet_with_end(vec![ENDPOINT_A], hook_id, true))
.unwrap_err();
assert!(matches!(
error,
EndpointError::MissingConnection {
next_hop: ENDPOINT_A,
direction: RouteDirection::Upward,
}
));
assert_hook_present(&endpoint, hook_id);
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn trusted_upward_packet_without_peer_metadata_checks_hook_existence_only() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.accept_hook(hook_id, ENDPOINT_A);
endpoint.add_connection(ENDPOINT_A, true);
endpoint
.add_inbound(echo_packet_with_end(vec![ENDPOINT_A], hook_id, true))
.unwrap();
let packet = single_outbound_packet(&endpoint, ENDPOINT_A);
assert_eq!(packet.hook_id, hook_id);
assert_hook_removed(&endpoint, hook_id);
}
+2
View File
@@ -0,0 +1,2 @@
mod oneshot;
mod streams;
+149
View File
@@ -0,0 +1,149 @@
use alloc::{vec, vec::Vec};
use crate::protocol::{Endpoint, Leaf};
#[cfg(feature = "interface")]
use crate::protocol::LeafMeta;
use super::super::support::{
endpoints::{ENDPOINT_A, ENDPOINT_B},
packets::{echo_packet, echo_packet_with_end},
transport::CommsLeaf,
};
const LEAF_CONTROLLER: u32 = 100;
const LEAF_RESPONDER: u32 = 102;
struct ControllerLeaf {
has_run: bool,
}
struct ResponderLeaf;
impl Leaf for ControllerLeaf {
fn get_id(&self) -> u32 {
LEAF_CONTROLLER
}
#[cfg(feature = "interface")]
fn get_meta(&self) -> LeafMeta {
LeafMeta {
name: "Controller Leaf",
identifier: "dev.unshell.test.controller_leaf",
version: "v0",
authors: alloc::vec!["ASTATIN3"],
}
}
fn update(&mut self, endpoint: &mut Endpoint) {
if !self.has_run {
// The controller starts exactly one request so the end-to-end test can
// assert deterministic routing without accumulating retries.
let hook_id = endpoint.get_hook_id();
let packet = echo_packet(vec![ENDPOINT_A, ENDPOINT_B], hook_id);
let _ = endpoint.add_outbound(packet);
self.has_run = true;
}
}
}
impl Leaf for ResponderLeaf {
fn get_id(&self) -> u32 {
LEAF_RESPONDER
}
#[cfg(feature = "interface")]
fn get_meta(&self) -> LeafMeta {
LeafMeta {
name: "Responder Leaf",
identifier: "dev.unshell.test.responder_leaf",
version: "v0",
authors: alloc::vec!["ASTATIN3"],
}
}
fn update(&mut self, endpoint: &mut Endpoint) {
let local_id = endpoint.path.last().cloned().unwrap_or(0);
let mut packets = Vec::new();
endpoint.take_inbound_clear(local_id, |packet| {
let mut response = echo_packet_with_end(vec![ENDPOINT_A], packet.hook_id, true);
response.hook_id = packet.hook_id;
response.data = packet.data.clone();
packets.push(response);
});
for packet in packets {
let _ = endpoint.add_outbound(packet);
}
}
}
#[test]
fn request_response_round_trip_over_mock_transport() {
let (tx_a, rx_a) = crossbeam_channel::unbounded();
let (tx_b, rx_b) = crossbeam_channel::unbounded();
let mut endpoint_a = Endpoint::new(ENDPOINT_A);
let mut controller_a = ControllerLeaf { has_run: false };
let mut comms_a = CommsLeaf {
tx: tx_b,
rx: rx_a,
remote_id: ENDPOINT_B,
is_authority: false,
started: false,
};
endpoint_a.path = vec![ENDPOINT_A];
let mut endpoint_b = Endpoint::new(ENDPOINT_B);
let mut responder_b = ResponderLeaf;
let mut comms_b = CommsLeaf {
tx: tx_a,
rx: rx_b,
remote_id: ENDPOINT_A,
is_authority: true,
started: false,
};
endpoint_b.path = vec![ENDPOINT_A, ENDPOINT_B];
// Connections are registered routing state. The comms leaves also insert them
// during updates, but the first application packet should not depend on leaf order.
endpoint_a.add_connection(ENDPOINT_B, false);
endpoint_b.add_connection(ENDPOINT_A, true);
// Cycle 1: A sends request to B.
controller_a.update(&mut endpoint_a);
comms_a.update(&mut endpoint_a);
responder_b.update(&mut endpoint_b);
comms_b.update(&mut endpoint_b);
// Cycle 2: B receives request and sends response to A.
responder_b.update(&mut endpoint_b);
comms_b.update(&mut endpoint_b);
controller_a.update(&mut endpoint_a);
comms_a.update(&mut endpoint_a);
// Cycle 3: A's transport leaf needs one more update to pull the response bytes
// from the channel and put the packet into the inbound queue.
controller_a.update(&mut endpoint_a);
comms_a.update(&mut endpoint_a);
assert!(
Endpoint::route_contains(ENDPOINT_A, &endpoint_a.inbound),
"Endpoint A should have received response"
);
assert_eq!(
Endpoint::route_get(ENDPOINT_A, &endpoint_a.inbound)
.unwrap()
.len(),
1,
"Endpoint A should have exactly one packet"
);
let response = &Endpoint::route_get(ENDPOINT_A, &endpoint_a.inbound)
.unwrap()
.front()
.unwrap();
assert!(response.end_hook);
assert_eq!(response.data, "ABC123".as_bytes());
assert_eq!(endpoint_b.hook_count(), 0);
}
@@ -5,10 +5,15 @@ use crate::protocol::LeafMeta;
use alloc::{format, vec, vec::Vec}; use alloc::{format, vec, vec::Vec};
use super::support::{CommsLeaf, ENDPOINT_A, ENDPOINT_B, assert_hook_present, assert_hook_removed}; use super::super::support::{
assertions::{assert_hook_present, assert_hook_removed},
endpoints::{ENDPOINT_A, ENDPOINT_B},
transport::CommsLeaf,
};
const LEAF_STREAM_CALLER: u32 = 200; const LEAF_STREAM_CALLER: u32 = 200;
const LEAF_STREAM_RESPONDENT: u32 = 201; const LEAF_STREAM_RESPONDENT: u32 = 201;
const LEAF_COMMS: u32 = 101;
/// Builds the initial downwards packet that opens the stream on the respondent. /// Builds the initial downwards packet that opens the stream on the respondent.
/// ///
@@ -104,7 +109,7 @@ impl Leaf for StreamCallerLeaf {
name: "Stream Caller Leaf", name: "Stream Caller Leaf",
identifier: "dev.unshell.test.stream_caller_leaf", identifier: "dev.unshell.test.stream_caller_leaf",
version: "v0", version: "v0",
authors: vec!["ASTATIN3"], authors: alloc::vec!["ASTATIN3"],
} }
} }
@@ -127,10 +132,10 @@ impl Leaf for StreamRespondentLeaf {
#[cfg(feature = "interface")] #[cfg(feature = "interface")]
fn get_meta(&self) -> LeafMeta { fn get_meta(&self) -> LeafMeta {
LeafMeta { LeafMeta {
name: "Stream Respondant Leaf", name: "Stream Respondent Leaf",
identifier: "dev.unshell.test.stream_respondent_leaf", identifier: "dev.unshell.test.stream_respondent_leaf",
version: "v0", version: "v0",
authors: vec!["ASTATIN3"], authors: alloc::vec!["ASTATIN3"],
} }
} }
@@ -243,9 +248,9 @@ fn stream_endpoints(total_packets: usize) -> StreamHarness {
/// Asserts the requested two-endpoint, four-leaf topology. /// Asserts the requested two-endpoint, four-leaf topology.
fn assert_four_leaf_topology(harness: &StreamHarness) { fn assert_four_leaf_topology(harness: &StreamHarness) {
assert_eq!(harness.caller_a.get_id(), LEAF_STREAM_CALLER); assert_eq!(harness.caller_a.get_id(), LEAF_STREAM_CALLER);
assert_eq!(harness.comms_a.get_id(), 101); assert_eq!(harness.comms_a.get_id(), LEAF_COMMS);
assert_eq!(harness.respondent_b.get_id(), LEAF_STREAM_RESPONDENT); assert_eq!(harness.respondent_b.get_id(), LEAF_STREAM_RESPONDENT);
assert_eq!(harness.comms_b.get_id(), 101); assert_eq!(harness.comms_b.get_id(), LEAF_COMMS);
} }
/// Drives the initial request until it is queued locally on endpoint B. /// Drives the initial request until it is queued locally on endpoint B.
@@ -1,43 +1,25 @@
use alloc::{collections::VecDeque, rc::Rc, vec, vec::Vec}; use alloc::{collections::VecDeque, rc::Rc, vec::Vec};
use core::cell::RefCell; use core::cell::RefCell;
use crossbeam_channel::{Receiver, Sender};
use crate::protocol::{Endpoint, Leaf, Packet}; use crate::protocol::{Endpoint, Leaf, Packet};
#[cfg(feature = "interface")] #[cfg(feature = "interface")]
use crate::protocol::LeafMeta; use crate::protocol::LeafMeta;
use super::{ use super::super::{
codec::{decode_block_chunk, decode_child_summary, decode_u32}, codec::{decode_block_chunk, decode_child_summary, decode_u32},
constants::{ constants::{
ENDPOINT_CALLER, ENDPOINT_RESPONDENT, LEAF_MERKLE_CALLER, LEAF_MERKLE_RESPONDENT, ENDPOINT_CALLER, LEAF_MERKLE_CALLER, PROC_BLOCK_CHUNK, PROC_CHILD_HASH_ENTRY,
LEAF_MOCK_CONNECTION, PROC_BLOCK_CHUNK, PROC_CHILD_HASH_ENTRY, PROC_GET_BLOCK_STREAM, PROC_GET_BLOCK_STREAM, PROC_GET_CHILD_HASHES, PROC_GET_ROOT_HASH, PROC_ROOT_HASH,
PROC_GET_CHILD_HASHES, PROC_GET_ROOT_HASH, PROC_ROOT_HASH, ROOT_NODE, ROOT_NODE,
}, },
rpc::{ rpc::{block_stream_request, child_hashes_request, root_hash_request},
block_chunk_frame, block_stream_request, child_hash_frame, child_hashes_request, state::{CallerPhase, CallerReport},
root_hash_frame, root_hash_request, tree::{ChildKind, MerkleStore},
},
state::{CallerPhase, CallerReport, RespondentReport, ResponseStream},
tree::{BlockChunk, ChildKind, MerkleStore},
}; };
/// Leaf that simulates a serialized transport connection with crossbeam channels.
///
/// This is intentionally tiny and reusable. Both endpoints in the Merkle test have
/// exactly one of these leaves, giving the requested four-leaf topology: caller,
/// respondent, and two mock connections.
pub(super) struct MockConnectionLeaf {
pub(super) tx: Sender<Vec<u8>>,
pub(super) rx: Receiver<Vec<u8>>,
pub(super) remote_id: u32,
pub(super) is_authority: bool,
pub(super) started: bool,
}
/// Caller leaf that drives the Merkle synchronization algorithm. /// Caller leaf that drives the Merkle synchronization algorithm.
pub(super) struct MerkleCallerLeaf { pub(crate) struct MerkleCallerLeaf {
local: MerkleStore, local: MerkleStore,
phase: CallerPhase, phase: CallerPhase,
pending_nodes: VecDeque<u32>, pending_nodes: VecDeque<u32>,
@@ -45,34 +27,9 @@ pub(super) struct MerkleCallerLeaf {
report: Rc<RefCell<CallerReport>>, report: Rc<RefCell<CallerReport>>,
} }
/// Respondent leaf that serves Merkle hash and block streams.
pub(super) struct MerkleRespondentLeaf {
remote: MerkleStore,
active_stream: Option<ResponseStream>,
report: Rc<RefCell<RespondentReport>>,
}
impl MockConnectionLeaf {
/// Creates one side of a mock connection.
pub(super) fn new(
tx: Sender<Vec<u8>>,
rx: Receiver<Vec<u8>>,
remote_id: u32,
is_authority: bool,
) -> Self {
Self {
tx,
rx,
remote_id,
is_authority,
started: false,
}
}
}
impl MerkleCallerLeaf { impl MerkleCallerLeaf {
/// Creates a caller with a local store and externally visible report. /// Creates a caller with a local store and externally visible report.
pub(super) fn new(local: MerkleStore, report: Rc<RefCell<CallerReport>>) -> Self { pub(crate) fn new(local: MerkleStore, report: Rc<RefCell<CallerReport>>) -> Self {
Self { Self {
local, local,
phase: CallerPhase::NeedRoot, phase: CallerPhase::NeedRoot,
@@ -83,55 +40,6 @@ impl MerkleCallerLeaf {
} }
} }
impl MerkleRespondentLeaf {
/// Creates a respondent backed by the authoritative remote store.
pub(super) fn new(remote: MerkleStore, report: Rc<RefCell<RespondentReport>>) -> Self {
Self {
remote,
active_stream: None,
report,
}
}
}
impl Leaf for MockConnectionLeaf {
fn get_id(&self) -> u32 {
LEAF_MOCK_CONNECTION
}
#[cfg(feature = "interface")]
fn get_meta(&self) -> LeafMeta {
LeafMeta {
name: "Merke Connection Leaf",
identifier: "dev.unshell.test.merkle.connection",
version: "v0",
authors: vec!["ASTATIN3"],
}
}
fn update(&mut self, endpoint: &mut Endpoint) {
if !self.started {
endpoint.add_connection(self.remote_id, self.is_authority);
self.started = true;
}
while !self.rx.is_empty() {
let data = self.rx.recv().unwrap();
// Mock transports move untrusted bytes. Malformed frames are dropped so
// the sync state machine is tested only after packet parsing succeeds.
if let Ok(packet) = Packet::deserialize(&data) {
let _ = endpoint.add_inbound_from(self.remote_id, packet);
}
}
endpoint.take_outbound_clear(self.remote_id, |packet| {
let data = packet.serialize().unwrap();
let _ = self.tx.send(data);
});
}
}
impl Leaf for MerkleCallerLeaf { impl Leaf for MerkleCallerLeaf {
fn get_id(&self) -> u32 { fn get_id(&self) -> u32 {
LEAF_MERKLE_CALLER LEAF_MERKLE_CALLER
@@ -143,7 +51,7 @@ impl Leaf for MerkleCallerLeaf {
name: "Merke Caller Leaf", name: "Merke Caller Leaf",
identifier: "dev.unshell.test.merkle.caller", identifier: "dev.unshell.test.merkle.caller",
version: "v0", version: "v0",
authors: vec!["ASTATIN3"], authors: alloc::vec!["ASTATIN3"],
} }
} }
@@ -153,27 +61,6 @@ impl Leaf for MerkleCallerLeaf {
} }
} }
impl Leaf for MerkleRespondentLeaf {
fn get_id(&self) -> u32 {
LEAF_MERKLE_RESPONDENT
}
#[cfg(feature = "interface")]
fn get_meta(&self) -> LeafMeta {
LeafMeta {
name: "Merke Respondent Leaf",
identifier: "dev.unshell.test.merkle.respondent",
version: "v0",
authors: vec!["ASTATIN3"],
}
}
fn update(&mut self, endpoint: &mut Endpoint) {
self.open_stream_from_request(endpoint);
self.send_one_response_frame(endpoint);
}
}
impl MerkleCallerLeaf { impl MerkleCallerLeaf {
/// Consumes all response packets currently delivered to endpoint A. /// Consumes all response packets currently delivered to endpoint A.
fn receive_responses(&mut self, endpoint: &mut Endpoint) { fn receive_responses(&mut self, endpoint: &mut Endpoint) {
@@ -346,89 +233,3 @@ impl MerkleCallerLeaf {
report.final_root_hash = Some(self.local.root_hash()); report.final_root_hash = Some(self.local.root_hash());
} }
} }
impl MerkleRespondentLeaf {
/// Opens one response stream from the first pending local request.
fn open_stream_from_request(&mut self, endpoint: &mut Endpoint) {
if self.active_stream.is_some() {
return;
}
let mut request = None;
endpoint.take_inbound_clear(ENDPOINT_RESPONDENT, |packet| {
if request.is_none() {
request = Some((packet.hook_id, packet.procedure_id, packet.data.clone()));
}
});
let Some((hook_id, procedure_id, data)) = request else {
return;
};
let frames = self.frames_for_request(procedure_id, &data);
self.report.borrow_mut().requests_seen.push(procedure_id);
if !frames.is_empty() {
self.report.borrow_mut().streams_started += 1;
self.active_stream = Some(ResponseStream::new(hook_id, frames));
}
}
/// Builds response frames for one request procedure.
fn frames_for_request(&self, procedure_id: u32, data: &[u8]) -> Vec<super::rpc::OutgoingFrame> {
match procedure_id {
PROC_GET_ROOT_HASH => vec![root_hash_frame(self.remote.root_hash())],
PROC_GET_CHILD_HASHES => {
let node_id = decode_u32(data).expect("child hash request node id");
self.remote
.child_summaries(node_id)
.into_iter()
.map(child_hash_frame)
.collect()
}
PROC_GET_BLOCK_STREAM => {
let block_id = decode_u32(data).expect("block stream request block id");
let chunks = self.remote.block_chunks(block_id);
let total = chunks.len() as u32;
chunks
.into_iter()
.enumerate()
.map(|(index, data)| {
block_chunk_frame(BlockChunk {
block_id,
index: index as u32,
total,
data,
})
})
.collect()
}
_ => Vec::new(),
}
}
/// Sends at most one response frame per update loop.
fn send_one_response_frame(&mut self, endpoint: &mut Endpoint) {
let Some(stream) = self.active_stream.as_mut() else {
return;
};
if stream.is_empty() {
self.active_stream = None;
return;
}
let packet = stream.next_packet().expect("active stream frame");
if endpoint.add_outbound(packet).is_err() {
return;
}
self.report.borrow_mut().frames_sent += 1;
stream.advance();
if stream.is_complete() {
self.report.borrow_mut().streams_completed += 1;
self.active_stream = None;
}
}
}
@@ -0,0 +1,7 @@
mod caller;
mod respondent;
mod transport;
pub(crate) use caller::MerkleCallerLeaf;
pub(crate) use respondent::MerkleRespondentLeaf;
pub(crate) use transport::MockConnectionLeaf;
@@ -0,0 +1,147 @@
use alloc::{rc::Rc, vec::Vec};
use core::cell::RefCell;
use crate::protocol::{Endpoint, Leaf};
#[cfg(feature = "interface")]
use crate::protocol::LeafMeta;
use super::super::{
codec::decode_u32,
constants::{
ENDPOINT_RESPONDENT, LEAF_MERKLE_RESPONDENT, PROC_GET_BLOCK_STREAM, PROC_GET_CHILD_HASHES,
PROC_GET_ROOT_HASH,
},
rpc::{block_chunk_frame, child_hash_frame, root_hash_frame},
state::{RespondentReport, ResponseStream},
tree::{BlockChunk, MerkleStore},
};
/// Respondent leaf that serves Merkle hash and block streams.
pub(crate) struct MerkleRespondentLeaf {
remote: MerkleStore,
active_stream: Option<ResponseStream>,
report: Rc<RefCell<RespondentReport>>,
}
impl MerkleRespondentLeaf {
/// Creates a respondent backed by the authoritative remote store.
pub(crate) fn new(remote: MerkleStore, report: Rc<RefCell<RespondentReport>>) -> Self {
Self {
remote,
active_stream: None,
report,
}
}
}
impl Leaf for MerkleRespondentLeaf {
fn get_id(&self) -> u32 {
LEAF_MERKLE_RESPONDENT
}
#[cfg(feature = "interface")]
fn get_meta(&self) -> LeafMeta {
LeafMeta {
name: "Merke Respondent Leaf",
identifier: "dev.unshell.test.merkle.respondent",
version: "v0",
authors: alloc::vec!["ASTATIN3"],
}
}
fn update(&mut self, endpoint: &mut Endpoint) {
self.open_stream_from_request(endpoint);
self.send_one_response_frame(endpoint);
}
}
impl MerkleRespondentLeaf {
/// Opens one response stream from the first pending local request.
fn open_stream_from_request(&mut self, endpoint: &mut Endpoint) {
if self.active_stream.is_some() {
return;
}
let mut request = None;
endpoint.take_inbound_clear(ENDPOINT_RESPONDENT, |packet| {
if request.is_none() {
request = Some((packet.hook_id, packet.procedure_id, packet.data.clone()));
}
});
let Some((hook_id, procedure_id, data)) = request else {
return;
};
let frames = self.frames_for_request(procedure_id, &data);
self.report.borrow_mut().requests_seen.push(procedure_id);
if !frames.is_empty() {
self.report.borrow_mut().streams_started += 1;
self.active_stream = Some(ResponseStream::new(hook_id, frames));
}
}
/// Builds response frames for one request procedure.
fn frames_for_request(
&self,
procedure_id: u32,
data: &[u8],
) -> Vec<super::super::rpc::OutgoingFrame> {
match procedure_id {
PROC_GET_ROOT_HASH => alloc::vec![root_hash_frame(self.remote.root_hash())],
PROC_GET_CHILD_HASHES => {
let node_id = decode_u32(data).expect("child hash request node id");
self.remote
.child_summaries(node_id)
.into_iter()
.map(child_hash_frame)
.collect()
}
PROC_GET_BLOCK_STREAM => {
let block_id = decode_u32(data).expect("block stream request block id");
let chunks = self.remote.block_chunks(block_id);
let total = chunks.len() as u32;
chunks
.into_iter()
.enumerate()
.map(|(index, data)| {
block_chunk_frame(BlockChunk {
block_id,
index: index as u32,
total,
data,
})
})
.collect()
}
_ => Vec::new(),
}
}
/// Sends at most one response frame per update loop.
fn send_one_response_frame(&mut self, endpoint: &mut Endpoint) {
let Some(stream) = self.active_stream.as_mut() else {
return;
};
if stream.is_empty() {
self.active_stream = None;
return;
}
let packet = stream.next_packet().expect("active stream frame");
if endpoint.add_outbound(packet).is_err() {
return;
}
self.report.borrow_mut().frames_sent += 1;
stream.advance();
if stream.is_complete() {
self.report.borrow_mut().streams_completed += 1;
self.active_stream = None;
}
}
}
@@ -0,0 +1,79 @@
use alloc::vec::Vec;
use crossbeam_channel::{Receiver, Sender};
use crate::protocol::{Endpoint, Leaf, Packet};
#[cfg(feature = "interface")]
use crate::protocol::LeafMeta;
use super::super::constants::LEAF_MOCK_CONNECTION;
/// Leaf that simulates a serialized transport connection with crossbeam channels.
///
/// This is intentionally tiny and reusable. Both endpoints in the Merkle test have
/// exactly one of these leaves, giving the requested four-leaf topology: caller,
/// respondent, and two mock connections.
pub(crate) struct MockConnectionLeaf {
pub(crate) tx: Sender<Vec<u8>>,
pub(crate) rx: Receiver<Vec<u8>>,
pub(crate) remote_id: u32,
pub(crate) is_authority: bool,
pub(crate) started: bool,
}
impl MockConnectionLeaf {
/// Creates one side of a mock connection.
pub(crate) fn new(
tx: Sender<Vec<u8>>,
rx: Receiver<Vec<u8>>,
remote_id: u32,
is_authority: bool,
) -> Self {
Self {
tx,
rx,
remote_id,
is_authority,
started: false,
}
}
}
impl Leaf for MockConnectionLeaf {
fn get_id(&self) -> u32 {
LEAF_MOCK_CONNECTION
}
#[cfg(feature = "interface")]
fn get_meta(&self) -> LeafMeta {
LeafMeta {
name: "Merke Connection Leaf",
identifier: "dev.unshell.test.merkle.connection",
version: "v0",
authors: alloc::vec!["ASTATIN3"],
}
}
fn update(&mut self, endpoint: &mut Endpoint) {
if !self.started {
endpoint.add_connection(self.remote_id, self.is_authority);
self.started = true;
}
while !self.rx.is_empty() {
let data = self.rx.recv().unwrap();
// Mock transports move untrusted bytes. Malformed frames are dropped so
// the sync state machine is tested only after packet parsing succeeds.
if let Ok(packet) = Packet::deserialize(&data) {
let _ = endpoint.add_inbound_from(self.remote_id, packet);
}
}
endpoint.take_outbound_clear(self.remote_id, |packet| {
let data = packet.serialize().unwrap();
let _ = self.tx.send(data);
});
}
}
+5
View File
@@ -0,0 +1,5 @@
mod endpoint;
mod integration;
mod merkle_sync;
mod packet;
mod support;
-491
View File
@@ -1,491 +0,0 @@
mod streams;
mod support;
use crate::protocol::{Endpoint, EndpointError, Leaf, RouteDirection};
use alloc::vec;
use support::{
CommsLeaf, ControllerLeaf, ENDPOINT_A, ENDPOINT_B, ENDPOINT_C, ResponderLeaf,
assert_hook_present, assert_hook_removed, echo_packet, echo_packet_with_end, endpoint_at,
single_inbound_packet, single_outbound_packet,
};
#[test]
fn test_oneshot() {
let (tx_a, rx_a) = crossbeam_channel::unbounded();
let (tx_b, rx_b) = crossbeam_channel::unbounded();
let mut endpoint_a = Endpoint::new(ENDPOINT_A);
let mut controller_a = ControllerLeaf { has_run: false };
let mut comms_a = CommsLeaf {
tx: tx_b,
rx: rx_a,
remote_id: ENDPOINT_B,
is_authority: false,
started: false,
};
endpoint_a.path = vec![ENDPOINT_A];
let mut endpoint_b = Endpoint::new(ENDPOINT_B);
let mut responder_b = ResponderLeaf;
let mut comms_b = CommsLeaf {
tx: tx_a,
rx: rx_b,
remote_id: ENDPOINT_A,
is_authority: true,
started: false,
};
endpoint_b.path = vec![ENDPOINT_A, ENDPOINT_B];
// Connections are registered routing state. The comms leaves also insert them
// during updates, but the first application packet should not depend on leaf order.
endpoint_a.add_connection(ENDPOINT_B, false);
endpoint_b.add_connection(ENDPOINT_A, true);
// Cycle 1: A sends request to B
controller_a.update(&mut endpoint_a);
comms_a.update(&mut endpoint_a);
responder_b.update(&mut endpoint_b);
comms_b.update(&mut endpoint_b);
// Cycle 2: B receives request and sends response to A
responder_b.update(&mut endpoint_b);
comms_b.update(&mut endpoint_b);
controller_a.update(&mut endpoint_a);
comms_a.update(&mut endpoint_a);
// Cycle 3: A's CommsLeaf needs one more update to pull the packet from the channel
// and put it into the inbound queue.
controller_a.update(&mut endpoint_a);
comms_a.update(&mut endpoint_a);
// Assertions on state
assert!(
Endpoint::route_contains(ENDPOINT_A, &endpoint_a.inbound),
"Endpoint A should have received response"
);
assert_eq!(
Endpoint::route_get(ENDPOINT_A, &endpoint_a.inbound)
.unwrap()
.len(),
1,
"Endpoint A should have exactly one packet"
);
let response = &Endpoint::route_get(ENDPOINT_A, &endpoint_a.inbound)
.unwrap()
.front()
.unwrap();
assert!(response.end_hook);
assert_eq!(response.data, "ABC123".as_bytes());
assert!(
endpoint_b.hook_count() == 0,
"responder hook should be cleaned after the upward response"
);
// assert_eq!(response.hook_id, HOOK_ECHO);
}
#[test]
fn inbound_downward_packet_for_local_endpoint_opens_hook() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.add_connection(ENDPOINT_A, true);
endpoint
.add_inbound_from(
ENDPOINT_A,
echo_packet(vec![ENDPOINT_A, ENDPOINT_B], hook_id),
)
.unwrap();
let packet = single_inbound_packet(&endpoint, ENDPOINT_B);
assert!(!packet.end_hook);
assert_eq!(packet.path, vec![ENDPOINT_A, ENDPOINT_B]);
assert_hook_present(&endpoint, hook_id);
assert_eq!(endpoint.hook_peer(hook_id), Some(ENDPOINT_A));
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn outbound_packet_for_local_endpoint_is_delivered_locally() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint
.add_outbound(echo_packet(vec![ENDPOINT_A, ENDPOINT_B], hook_id))
.unwrap();
let packet = single_inbound_packet(&endpoint, ENDPOINT_B);
assert!(!packet.end_hook);
assert_eq!(packet.data, "ABC123".as_bytes());
assert_hook_removed(&endpoint, hook_id);
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn inbound_downward_packet_routes_to_immediate_child() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.add_connection(ENDPOINT_A, true);
endpoint.add_connection(ENDPOINT_C, false);
endpoint
.add_inbound_from(
ENDPOINT_A,
echo_packet(vec![ENDPOINT_A, ENDPOINT_B, ENDPOINT_C], hook_id),
)
.unwrap();
let packet = single_outbound_packet(&endpoint, ENDPOINT_C);
assert!(!packet.end_hook);
assert_eq!(packet.path, vec![ENDPOINT_A, ENDPOINT_B, ENDPOINT_C]);
assert_hook_present(&endpoint, hook_id);
assert_eq!(endpoint.hook_peer(hook_id), Some(ENDPOINT_C));
assert!(!Endpoint::route_contains(ENDPOINT_A, &endpoint.outbound));
}
#[test]
fn outbound_downward_packet_routes_to_immediate_child() {
let mut endpoint = endpoint_at(ENDPOINT_A, vec![ENDPOINT_A]);
let hook_id = endpoint.get_hook_id();
endpoint.accept_hook(hook_id, ENDPOINT_B);
endpoint.add_connection(ENDPOINT_B, false);
endpoint
.add_outbound(echo_packet_with_end(
vec![ENDPOINT_A, ENDPOINT_B, ENDPOINT_C],
hook_id,
true,
))
.unwrap();
let packet = single_outbound_packet(&endpoint, ENDPOINT_B);
assert!(packet.end_hook);
assert_eq!(packet.path, vec![ENDPOINT_A, ENDPOINT_B, ENDPOINT_C]);
assert_hook_removed(&endpoint, hook_id);
assert!(!Endpoint::route_contains(ENDPOINT_C, &endpoint.outbound));
}
#[test]
fn inbound_upward_packet_with_hook_routes_to_parent() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.accept_hook(hook_id, ENDPOINT_C);
endpoint.add_connection(ENDPOINT_A, true);
endpoint.add_connection(ENDPOINT_C, false);
endpoint
.add_inbound_from(
ENDPOINT_C,
echo_packet_with_end(vec![ENDPOINT_A], hook_id, true),
)
.unwrap();
let packet = single_outbound_packet(&endpoint, ENDPOINT_A);
assert!(packet.end_hook);
assert_eq!(packet.hook_id, hook_id);
assert_hook_removed(&endpoint, hook_id);
assert!(!Endpoint::route_contains(ENDPOINT_C, &endpoint.outbound));
}
#[test]
fn inbound_upward_packet_without_hook_is_rejected() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.add_connection(ENDPOINT_A, true);
endpoint.add_connection(ENDPOINT_C, false);
let error = endpoint
.add_inbound_from(
ENDPOINT_C,
echo_packet_with_end(vec![ENDPOINT_A], hook_id, true),
)
.unwrap_err();
assert!(matches!(
error,
EndpointError::UnknownHook { hook_id: observed_hook_id } if observed_hook_id == hook_id
));
assert!(Endpoint::routes_is_empty(&endpoint.inbound));
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn forged_upward_packet_with_unknown_hook_is_rejected() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
endpoint.accept_hook(7, ENDPOINT_C);
endpoint.add_connection(ENDPOINT_A, true);
endpoint.add_connection(ENDPOINT_C, false);
let error = endpoint
.add_inbound_from(ENDPOINT_C, echo_packet_with_end(vec![ENDPOINT_A], 99, true))
.unwrap_err();
assert!(matches!(error, EndpointError::UnknownHook { hook_id: 99 }));
assert_hook_present(&endpoint, 7);
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn forged_sideways_packet_is_rejected_as_incorrect_path() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.accept_hook(hook_id, ENDPOINT_A);
endpoint.add_connection(ENDPOINT_A, true);
let error = endpoint
.add_inbound_from(
ENDPOINT_A,
echo_packet(vec![ENDPOINT_A, ENDPOINT_C], hook_id),
)
.unwrap_err();
assert!(matches!(error, EndpointError::DestinationOutsideLocalTree));
assert_hook_present(&endpoint, hook_id);
assert!(Endpoint::routes_is_empty(&endpoint.inbound));
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn malformed_frame_is_dropped_by_comms_leaf() {
let (tx_to_endpoint, rx_for_endpoint) = crossbeam_channel::unbounded();
let (tx_unused, _rx_unused) = crossbeam_channel::unbounded();
let mut endpoint = Endpoint::new(ENDPOINT_B);
let mut comms = CommsLeaf {
tx: tx_unused,
rx: rx_for_endpoint,
remote_id: ENDPOINT_A,
is_authority: true,
started: false,
};
endpoint.path = vec![ENDPOINT_A, ENDPOINT_B];
tx_to_endpoint.send(vec![0, 1, 2, 3]).unwrap();
comms.update(&mut endpoint);
assert!(Endpoint::routes_is_empty(&endpoint.inbound));
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn malformed_frame_does_not_block_following_valid_packet() {
let (tx_to_endpoint, rx_for_endpoint) = crossbeam_channel::unbounded();
let (tx_unused, _rx_unused) = crossbeam_channel::unbounded();
let hook_id = 42;
let mut endpoint = Endpoint::new(ENDPOINT_B);
let mut comms = CommsLeaf {
tx: tx_unused,
rx: rx_for_endpoint,
remote_id: ENDPOINT_A,
is_authority: true,
started: false,
};
endpoint.path = vec![ENDPOINT_A, ENDPOINT_B];
tx_to_endpoint.send(vec![0, 1, 2, 3]).unwrap();
tx_to_endpoint
.send(
echo_packet(vec![ENDPOINT_A, ENDPOINT_B], hook_id)
.serialize()
.unwrap(),
)
.unwrap();
comms.update(&mut endpoint);
let packet = single_inbound_packet(&endpoint, ENDPOINT_B);
assert!(!packet.end_hook);
assert_eq!(packet.hook_id, hook_id);
assert_hook_present(&endpoint, hook_id);
}
#[test]
fn forged_frame_without_required_hook_is_dropped_by_comms_leaf() {
let (tx_to_endpoint, rx_for_endpoint) = crossbeam_channel::unbounded();
let (tx_unused, _rx_unused) = crossbeam_channel::unbounded();
let mut endpoint = Endpoint::new(ENDPOINT_B);
let mut comms = CommsLeaf {
tx: tx_unused,
rx: rx_for_endpoint,
remote_id: ENDPOINT_C,
is_authority: false,
started: false,
};
endpoint.path = vec![ENDPOINT_A, ENDPOINT_B];
endpoint.accept_hook(7, ENDPOINT_C);
endpoint.add_connection(ENDPOINT_A, true);
tx_to_endpoint
.send(
echo_packet_with_end(vec![ENDPOINT_A], 12, true)
.serialize()
.unwrap(),
)
.unwrap();
comms.update(&mut endpoint);
assert_hook_present(&endpoint, 7);
assert!(Endpoint::routes_is_empty(&endpoint.inbound));
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn upward_outbound_without_hook_is_rejected() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
endpoint.accept_hook(7, ENDPOINT_A);
endpoint.add_connection(ENDPOINT_A, true);
let new_hook = endpoint.get_hook_id();
let error = endpoint
.add_outbound(echo_packet_with_end(vec![ENDPOINT_A], new_hook, true))
.unwrap_err();
assert!(matches!(
error,
EndpointError::UnknownHook { hook_id: observed_hook_id } if observed_hook_id == new_hook
));
assert_hook_present(&endpoint, 7);
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn downward_outbound_without_hook_is_allowed() {
let mut endpoint = endpoint_at(ENDPOINT_A, vec![ENDPOINT_A]);
endpoint.add_connection(ENDPOINT_B, false);
let new_hook = endpoint.get_hook_id();
endpoint
.add_outbound(echo_packet(vec![ENDPOINT_A, ENDPOINT_B], new_hook))
.unwrap();
assert_eq!(
Endpoint::route_get(ENDPOINT_B, &endpoint.outbound)
.unwrap()
.len(),
1
);
assert_hook_present(&endpoint, new_hook);
assert_eq!(endpoint.hook_peer(new_hook), Some(ENDPOINT_B));
}
#[test]
fn deeper_upward_route_uses_parent_as_next_hop() {
let mut endpoint = endpoint_at(ENDPOINT_C, vec![ENDPOINT_A, ENDPOINT_B, ENDPOINT_C]);
let new_hook = endpoint.get_hook_id();
endpoint.accept_hook(new_hook, ENDPOINT_B);
endpoint.add_connection(ENDPOINT_B, true);
endpoint
.add_outbound(echo_packet_with_end(vec![ENDPOINT_A], new_hook, true))
.unwrap();
assert!(Endpoint::route_contains(ENDPOINT_B, &endpoint.outbound));
assert!(!Endpoint::route_contains(ENDPOINT_A, &endpoint.outbound));
assert_hook_removed(&endpoint, new_hook);
}
#[test]
fn downward_route_without_connection_is_rejected() {
let mut endpoint = endpoint_at(ENDPOINT_A, vec![ENDPOINT_A]);
let hook_id = endpoint.get_hook_id();
let error = endpoint
.add_outbound(echo_packet(vec![ENDPOINT_A, ENDPOINT_B], hook_id))
.unwrap_err();
assert!(matches!(
error,
EndpointError::MissingConnection {
next_hop: ENDPOINT_B,
direction: RouteDirection::Downward,
}
));
assert_hook_removed(&endpoint, hook_id);
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn upward_route_without_connection_is_rejected_even_with_hook() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.accept_hook(hook_id, ENDPOINT_A);
let error = endpoint
.add_outbound(echo_packet_with_end(vec![ENDPOINT_A], hook_id, true))
.unwrap_err();
assert!(matches!(
error,
EndpointError::MissingConnection {
next_hop: ENDPOINT_A,
direction: RouteDirection::Upward,
}
));
assert_hook_present(&endpoint, hook_id);
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn end_hook_removes_hook_after_packet_is_queued() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.accept_hook(hook_id, ENDPOINT_A);
endpoint.add_connection(ENDPOINT_A, true);
endpoint
.add_outbound(echo_packet_with_end(vec![ENDPOINT_A], hook_id, true))
.unwrap();
assert_hook_removed(&endpoint, hook_id);
assert_eq!(
single_outbound_packet(&endpoint, ENDPOINT_A).hook_id,
hook_id
);
}
#[test]
fn failed_end_hook_route_keeps_hook_state() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let hook_id = endpoint.get_hook_id();
endpoint.accept_hook(hook_id, ENDPOINT_A);
let error = endpoint
.add_outbound(echo_packet_with_end(vec![ENDPOINT_A], hook_id, true))
.unwrap_err();
assert!(matches!(
error,
EndpointError::MissingConnection {
next_hop: ENDPOINT_A,
direction: RouteDirection::Upward,
}
));
assert_hook_present(&endpoint, hook_id);
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
#[test]
fn inbound_without_absolute_path_is_rejected() {
let mut endpoint = Endpoint::new(ENDPOINT_A);
let error = endpoint
.add_inbound(echo_packet(vec![ENDPOINT_A], 1))
.unwrap_err();
assert!(matches!(error, EndpointError::EndpointPathUnset));
assert!(Endpoint::routes_is_empty(&endpoint.inbound));
}
#[test]
fn outbound_without_absolute_path_is_rejected() {
let mut endpoint = Endpoint::new(ENDPOINT_A);
let error = endpoint
.add_outbound(echo_packet(vec![ENDPOINT_A], 1))
.unwrap_err();
assert!(matches!(error, EndpointError::EndpointPathUnset));
assert!(Endpoint::routes_is_empty(&endpoint.outbound));
}
-205
View File
@@ -1,205 +0,0 @@
use crate::protocol::{Endpoint, Leaf, Packet};
#[cfg(feature = "interface")]
use crate::protocol::LeafMeta;
use alloc::{vec, vec::Vec};
use crossbeam_channel::{Receiver, Sender};
pub(super) const ENDPOINT_A: u32 = 0;
pub(super) const ENDPOINT_B: u32 = 1;
pub(super) const ENDPOINT_C: u32 = 2;
const LEAF_CONTROLLER: u32 = 100;
const LEAF_COMMS: u32 = 101;
const LEAF_RESPONDER: u32 = 102;
/// Builds a test packet whose route is the only field varied by routing tests.
///
/// Keeping the payload stable makes each assertion about endpoint behavior rather
/// than packet construction, which is important because forged and malformed cases
/// should fail before any leaf-level procedure handling would matter.
pub(super) fn echo_packet(path: Vec<u32>, hook_id: u16) -> Packet {
echo_packet_with_end(path, hook_id, false)
}
/// Builds a test packet with an explicit hook-lifetime marker.
pub(super) fn echo_packet_with_end(path: Vec<u32>, hook_id: u16, end_hook: bool) -> Packet {
Packet {
hook_id,
end_hook,
path,
procedure_id: 1,
data: "ABC123".as_bytes().to_vec(),
}
}
/// Creates a bare endpoint at a known absolute path.
///
/// Most routing tests do not need leaves; they only need the endpoint's local path,
/// connection table, and hook table. This helper keeps that setup explicit without
/// hiding the routing state that each test is validating.
pub(super) fn endpoint_at(id: u32, path: Vec<u32>) -> Endpoint {
let mut endpoint = Endpoint::new(id);
endpoint.path = path;
endpoint
}
/// Returns the only outbound packet queued for `next_hop`.
///
/// Routing bugs often show up as packets being sent to the final destination rather
/// than the immediate neighbor. Tests use this helper to assert both that exactly one
/// packet exists and that it was queued for the expected adjacent endpoint.
pub(super) fn single_outbound_packet(endpoint: &Endpoint, next_hop: u32) -> &Packet {
let queue = Endpoint::route_get(next_hop, &endpoint.outbound)
.unwrap_or_else(|| panic!("expected one outbound queue for {next_hop}"));
assert_eq!(queue.len(), 1, "expected exactly one outbound packet");
queue.front().unwrap()
}
/// Returns the only inbound packet delivered to `local_id`.
///
/// Local delivery is intentionally separate from transit forwarding, so the tests
/// assert against the local inbound queue instead of only checking that routing did
/// not produce an error.
pub(super) fn single_inbound_packet(endpoint: &Endpoint, local_id: u32) -> &Packet {
let queue = Endpoint::route_get(local_id, &endpoint.inbound)
.unwrap_or_else(|| panic!("expected one inbound queue for {local_id}"));
assert_eq!(queue.len(), 1, "expected exactly one inbound packet");
queue.front().unwrap()
}
/// Asserts that local hook state still contains `hook_id`.
///
/// Tests use this instead of open-coded map checks so every lifecycle assertion
/// explains the intended routing invariant when it fails.
pub(super) fn assert_hook_present(endpoint: &Endpoint, hook_id: u16) {
assert!(
endpoint.has_hook(hook_id),
"expected hook {hook_id} to remain registered"
);
}
/// Asserts that local hook state no longer contains `hook_id`.
///
/// Upward `end_hook` packets are the only cases that should remove hook state;
/// downward and local packets with the same flag must leave hooks alone.
pub(super) fn assert_hook_removed(endpoint: &Endpoint, hook_id: u16) {
assert!(
!endpoint.has_hook(hook_id),
"expected hook {hook_id} to be cleaned up"
);
}
pub(super) struct ControllerLeaf {
pub(super) has_run: bool,
}
pub(super) struct CommsLeaf {
pub(super) tx: Sender<Vec<u8>>,
pub(super) rx: Receiver<Vec<u8>>,
pub(super) remote_id: u32,
pub(super) is_authority: bool,
pub(super) started: bool,
}
pub(super) struct ResponderLeaf;
impl Leaf for ControllerLeaf {
fn get_id(&self) -> u32 {
LEAF_CONTROLLER
}
#[cfg(feature = "interface")]
fn get_meta(&self) -> LeafMeta {
LeafMeta {
name: "Controller Leaf",
identifier: "dev.unshell.test.controller_leaf",
version: "v0",
authors: vec!["ASTATIN3"],
}
}
fn update(&mut self, endpoint: &mut Endpoint) {
if !self.has_run {
// The controller starts exactly one request so the end-to-end test can
// assert deterministic routing without accumulating retries.
let hook_id = endpoint.get_hook_id();
let packet = echo_packet(vec![ENDPOINT_A, ENDPOINT_B], hook_id);
let _ = endpoint.add_outbound(packet);
self.has_run = true;
}
}
}
impl Leaf for CommsLeaf {
fn get_id(&self) -> u32 {
LEAF_COMMS
}
#[cfg(feature = "interface")]
fn get_meta(&self) -> LeafMeta {
LeafMeta {
name: "Comms Leaf",
identifier: "dev.unshell.test.comms_leaf",
version: "v0",
authors: vec!["ASTATIN3"],
}
}
fn update(&mut self, endpoint: &mut Endpoint) {
if !self.started {
endpoint.add_connection(self.remote_id, self.is_authority);
self.started = true;
}
while !self.rx.is_empty() {
let data = self.rx.recv().unwrap();
// Transport bytes are untrusted. Dropping malformed frames here keeps
// the oneshot harness faithful to a router boundary: invalid wire data
// must not panic or poison later valid packets on the same connection.
if let Ok(packet) = Packet::deserialize(&data) {
let _ = endpoint.add_inbound_from(self.remote_id, packet);
}
}
endpoint.take_outbound_clear(self.remote_id, |packet| {
let data = packet.serialize().unwrap();
let _ = self.tx.send(data);
});
}
}
impl Leaf for ResponderLeaf {
fn get_id(&self) -> u32 {
LEAF_RESPONDER
}
#[cfg(feature = "interface")]
fn get_meta(&self) -> LeafMeta {
LeafMeta {
name: "Responder Leaf",
identifier: "dev.unshell.test.responder_leaf",
version: "v0",
authors: vec!["ASTATIN3"],
}
}
fn update(&mut self, endpoint: &mut Endpoint) {
let local_id = endpoint.path.last().cloned().unwrap_or(0);
let mut packets = Vec::new();
endpoint.take_inbound_clear(local_id, |packet| {
let mut response = echo_packet_with_end(vec![ENDPOINT_A], packet.hook_id, true);
response.hook_id = packet.hook_id;
response.data = packet.data.clone();
packets.push(response);
});
for packet in packets {
let _ = endpoint.add_outbound(packet);
}
}
}
+23
View File
@@ -0,0 +1,23 @@
use crate::protocol::Endpoint;
/// Asserts that local hook state still contains `hook_id`.
///
/// Tests use this instead of open-coded map checks so every lifecycle assertion
/// explains the intended routing invariant when it fails.
pub(crate) fn assert_hook_present(endpoint: &Endpoint, hook_id: u16) {
assert!(
endpoint.has_hook(hook_id),
"expected hook {hook_id} to remain registered"
);
}
/// Asserts that local hook state no longer contains `hook_id`.
///
/// Upward `end_hook` packets are the only cases that should remove hook state;
/// downward and local packets with the same flag must leave hooks alone.
pub(crate) fn assert_hook_removed(endpoint: &Endpoint, hook_id: u16) {
assert!(
!endpoint.has_hook(hook_id),
"expected hook {hook_id} to be cleaned up"
);
}
+42
View File
@@ -0,0 +1,42 @@
use alloc::vec::Vec;
use crate::protocol::{Endpoint, Packet};
pub(crate) const ENDPOINT_A: u32 = 0;
pub(crate) const ENDPOINT_B: u32 = 1;
pub(crate) const ENDPOINT_C: u32 = 2;
/// Creates a bare endpoint at a known absolute path.
///
/// Most routing tests do not need leaves; they only need the endpoint's local path,
/// connection table, and hook table. This helper keeps that setup explicit without
/// hiding the routing state that each test is validating.
pub(crate) fn endpoint_at(id: u32, path: Vec<u32>) -> Endpoint {
let mut endpoint = Endpoint::new(id);
endpoint.path = path;
endpoint
}
/// Returns the only outbound packet queued for `next_hop`.
///
/// Routing bugs often show up as packets being sent to the final destination rather
/// than the immediate neighbor. Tests use this helper to assert both that exactly one
/// packet exists and that it was queued for the expected adjacent endpoint.
pub(crate) fn single_outbound_packet(endpoint: &Endpoint, next_hop: u32) -> &Packet {
let queue = Endpoint::route_get(next_hop, &endpoint.outbound)
.unwrap_or_else(|| panic!("expected one outbound queue for {next_hop}"));
assert_eq!(queue.len(), 1, "expected exactly one outbound packet");
queue.front().unwrap()
}
/// Returns the only inbound packet delivered to `local_id`.
///
/// Local delivery is intentionally separate from transit forwarding, so the tests
/// assert against the local inbound queue instead of only checking that routing did
/// not produce an error.
pub(crate) fn single_inbound_packet(endpoint: &Endpoint, local_id: u32) -> &Packet {
let queue = Endpoint::route_get(local_id, &endpoint.inbound)
.unwrap_or_else(|| panic!("expected one inbound queue for {local_id}"));
assert_eq!(queue.len(), 1, "expected exactly one inbound packet");
queue.front().unwrap()
}
+4
View File
@@ -0,0 +1,4 @@
pub(crate) mod assertions;
pub(crate) mod endpoints;
pub(crate) mod packets;
pub(crate) mod transport;
+23
View File
@@ -0,0 +1,23 @@
use alloc::vec::Vec;
use crate::protocol::Packet;
/// Builds a test packet whose route is the only field varied by routing tests.
///
/// Keeping the payload stable makes each assertion about endpoint behavior rather
/// than packet construction, which is important because forged and malformed cases
/// should fail before any leaf-level procedure handling would matter.
pub(crate) fn echo_packet(path: Vec<u32>, hook_id: u16) -> Packet {
echo_packet_with_end(path, hook_id, false)
}
/// Builds a test packet with an explicit hook-lifetime marker.
pub(crate) fn echo_packet_with_end(path: Vec<u32>, hook_id: u16, end_hook: bool) -> Packet {
Packet {
hook_id,
end_hook,
path,
procedure_id: 1,
data: "ABC123".as_bytes().to_vec(),
}
}
+63
View File
@@ -0,0 +1,63 @@
use alloc::vec::Vec;
use crossbeam_channel::{Receiver, Sender};
use crate::protocol::{Endpoint, Leaf, Packet};
#[cfg(feature = "interface")]
use crate::protocol::LeafMeta;
const LEAF_COMMS: u32 = 101;
/// Mock transport leaf that serializes outbound packets through a channel pair.
///
/// This is intentionally shared by protocol integration tests: it is the boundary
/// where structured packets become untrusted bytes and malformed frames get dropped
/// before reaching endpoint routing.
pub(crate) struct CommsLeaf {
pub(crate) tx: Sender<Vec<u8>>,
pub(crate) rx: Receiver<Vec<u8>>,
pub(crate) remote_id: u32,
pub(crate) is_authority: bool,
pub(crate) started: bool,
}
impl Leaf for CommsLeaf {
fn get_id(&self) -> u32 {
LEAF_COMMS
}
#[cfg(feature = "interface")]
fn get_meta(&self) -> LeafMeta {
LeafMeta {
name: "Comms Leaf",
identifier: "dev.unshell.test.comms_leaf",
version: "v0",
authors: alloc::vec!["ASTATIN3"],
}
}
fn update(&mut self, endpoint: &mut Endpoint) {
if !self.started {
endpoint.add_connection(self.remote_id, self.is_authority);
self.started = true;
}
while !self.rx.is_empty() {
let data = self.rx.recv().unwrap();
// Transport bytes are untrusted. Dropping malformed frames here keeps
// integration harnesses faithful to a router boundary: invalid wire data
// must not panic or poison later valid packets on the same connection.
if let Ok(packet) = Packet::deserialize(&data) {
let _ = endpoint.add_inbound_from(self.remote_id, packet);
}
}
endpoint.take_outbound_clear(self.remote_id, |packet| {
let data = packet.serialize().unwrap();
let _ = self.tx.send(data);
});
}
}
@@ -1,282 +0,0 @@
use alloc::{vec, vec::Vec};
use unshell::protocol::{Leaf, Packet};
use crate::{
FakePtyLeaf, FakePtyState, OP_ABORT, OP_ERROR, OP_EXIT, OP_INPUT, OP_OUTPUT, OP_STDIN_EOF,
OP_TERMINATE, pty_open_packet,
};
use super::support::{
ENDPOINT_A, ENDPOINT_B, PROC_OTHER, assert_frame, assert_hook_present, assert_hook_removed,
assert_opened, drain_parent_pty_packets, endpoint_at, has_frame, open_pty_session,
pty_endpoints, send_downward_frame, transfer_packets,
};
#[test]
fn open_pty_paves_hook_and_creates_session() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
let packets = drain_parent_pty_packets(&mut endpoint_a);
assert_eq!(leaf.active_session_count(), 1);
assert_eq!(leaf.state().active_count, 1);
assert_eq!(leaf.state().total_opened, 1);
assert_hook_present(&endpoint_a, hook_id);
assert_hook_present(&endpoint_b, hook_id);
assert_eq!(packets.len(), 1);
assert_opened(&packets[0], hook_id);
}
#[test]
fn input_and_output_share_one_hook() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
drain_parent_pty_packets(&mut endpoint_a);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
hook_id,
OP_INPUT,
b"hello",
false,
);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
let packets = drain_parent_pty_packets(&mut endpoint_a);
assert_eq!(packets.len(), 1);
assert_frame(&packets[0], hook_id, OP_OUTPUT, false, b"hello");
assert_hook_present(&endpoint_a, hook_id);
assert_hook_present(&endpoint_b, hook_id);
}
#[test]
fn stdin_eof_keeps_hook_until_exit() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
drain_parent_pty_packets(&mut endpoint_a);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
hook_id,
OP_STDIN_EOF,
&[],
false,
);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
assert_eq!(leaf.state().last_stdin_eof_hook, Some(hook_id));
assert!(drain_parent_pty_packets(&mut endpoint_a).is_empty());
assert_hook_present(&endpoint_a, hook_id);
assert_hook_present(&endpoint_b, hook_id);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
hook_id,
OP_TERMINATE,
&[],
false,
);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
let packets = drain_parent_pty_packets(&mut endpoint_a);
assert_eq!(packets.len(), 1);
assert_frame(&packets[0], hook_id, OP_EXIT, true, &[0]);
assert_eq!(leaf.active_session_count(), 0);
assert_hook_removed(&endpoint_a, hook_id);
assert_hook_removed(&endpoint_b, hook_id);
}
#[test]
fn exit_end_hook_cleans_route_and_session() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
drain_parent_pty_packets(&mut endpoint_a);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
hook_id,
OP_TERMINATE,
&[],
false,
);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
let packets = drain_parent_pty_packets(&mut endpoint_a);
assert_eq!(packets.len(), 1);
assert_frame(&packets[0], hook_id, OP_EXIT, true, &[0]);
assert_eq!(leaf.active_session_count(), 0);
assert_hook_removed(&endpoint_a, hook_id);
assert_hook_removed(&endpoint_b, hook_id);
}
#[test]
fn failed_final_exit_route_closes_session_without_retry() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
drain_parent_pty_packets(&mut endpoint_a);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
hook_id,
OP_TERMINATE,
&[],
false,
);
endpoint_b.remove_connection(ENDPOINT_A, true);
leaf.update(&mut endpoint_b);
assert_eq!(leaf.active_session_count(), 0);
assert_eq!(leaf.pending_packet_count(), 0);
assert_hook_removed(&endpoint_b, hook_id);
endpoint_b.add_connection(ENDPOINT_A, true);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
let packets = drain_parent_pty_packets(&mut endpoint_a);
assert!(packets.is_empty());
assert_eq!(leaf.active_session_count(), 0);
assert_hook_present(&endpoint_a, hook_id);
assert_hook_removed(&endpoint_b, hook_id);
}
#[test]
fn abort_downward_end_hook_closes_without_ack() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
drain_parent_pty_packets(&mut endpoint_a);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
hook_id,
OP_ABORT,
&[],
true,
);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
assert_eq!(leaf.active_session_count(), 0);
assert!(drain_parent_pty_packets(&mut endpoint_a).is_empty());
assert_hook_removed(&endpoint_a, hook_id);
assert_hook_removed(&endpoint_b, hook_id);
}
#[test]
fn unknown_session_input_returns_error_end_hook() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let hook_id = endpoint_a.get_hook_id();
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
hook_id,
OP_INPUT,
b"orphan",
false,
);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
let packets = drain_parent_pty_packets(&mut endpoint_a);
assert_eq!(packets.len(), 1);
assert_frame(&packets[0], hook_id, OP_ERROR, true, b"unknown-session");
assert_eq!(leaf.active_session_count(), 0);
assert_hook_removed(&endpoint_a, hook_id);
assert_hook_removed(&endpoint_b, hook_id);
}
#[test]
fn two_pty_sessions_interleave_without_crossing_hooks() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let first_hook = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
let second_hook = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
drain_parent_pty_packets(&mut endpoint_a);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
second_hook,
OP_INPUT,
b"second",
false,
);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
first_hook,
OP_INPUT,
b"first",
false,
);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
let packets = drain_parent_pty_packets(&mut endpoint_a);
assert_eq!(leaf.active_session_count(), 2);
assert_eq!(packets.len(), 2);
assert!(has_frame(&packets, first_hook, OP_OUTPUT, b"first"));
assert!(has_frame(&packets, second_hook, OP_OUTPUT, b"second"));
assert_hook_present(&endpoint_a, first_hook);
assert_hook_present(&endpoint_a, second_hook);
assert_hook_present(&endpoint_b, first_hook);
assert_hook_present(&endpoint_b, second_hook);
}
#[test]
fn pty_leaf_does_not_consume_other_leaf_packets() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
endpoint.add_connection(ENDPOINT_A, true);
endpoint
.add_inbound_from(ENDPOINT_A, pty_open_packet(vec![ENDPOINT_A, ENDPOINT_B], 7))
.unwrap();
endpoint
.add_inbound_from(
ENDPOINT_A,
Packet {
hook_id: 8,
end_hook: false,
path: vec![ENDPOINT_A, ENDPOINT_B],
procedure_id: PROC_OTHER,
data: b"leave-me".to_vec(),
},
)
.unwrap();
leaf.update(&mut endpoint);
let mut other_packets = Vec::new();
endpoint.take_inbound_matching(
ENDPOINT_B,
|packet| packet.procedure_id == PROC_OTHER,
|packet| other_packets.push(packet),
);
assert_eq!(leaf.active_session_count(), 1);
assert_eq!(other_packets.len(), 1);
assert_eq!(other_packets[0].procedure_id, PROC_OTHER);
assert_eq!(other_packets[0].data, b"leave-me".to_vec());
}
@@ -0,0 +1,47 @@
use unshell::protocol::Leaf;
use crate::{FakePtyLeaf, FakePtyState, OP_INPUT, OP_OUTPUT};
use super::super::support::{
ENDPOINT_A, ENDPOINT_B, assert_hook_present, drain_parent_pty_packets, has_frame,
open_pty_session, pty_endpoints, send_downward_frame, transfer_packets,
};
#[test]
fn two_pty_sessions_interleave_without_crossing_hooks() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let first_hook = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
let second_hook = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
drain_parent_pty_packets(&mut endpoint_a);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
second_hook,
OP_INPUT,
b"second",
false,
);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
first_hook,
OP_INPUT,
b"first",
false,
);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
let packets = drain_parent_pty_packets(&mut endpoint_a);
assert_eq!(leaf.active_session_count(), 2);
assert_eq!(packets.len(), 2);
assert!(has_frame(&packets, first_hook, OP_OUTPUT, b"first"));
assert!(has_frame(&packets, second_hook, OP_OUTPUT, b"second"));
assert_hook_present(&endpoint_a, first_hook);
assert_hook_present(&endpoint_a, second_hook);
assert_hook_present(&endpoint_b, first_hook);
assert_hook_present(&endpoint_b, second_hook);
}
@@ -0,0 +1,41 @@
use unshell::protocol::Leaf;
use crate::{FakePtyLeaf, FakePtyState, OP_TERMINATE};
use super::super::support::{
ENDPOINT_A, ENDPOINT_B, assert_hook_present, assert_hook_removed, drain_parent_pty_packets,
open_pty_session, pty_endpoints, send_downward_frame, transfer_packets,
};
#[test]
fn failed_final_exit_route_closes_session_without_retry() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
drain_parent_pty_packets(&mut endpoint_a);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
hook_id,
OP_TERMINATE,
&[],
false,
);
endpoint_b.remove_connection(ENDPOINT_A, true);
leaf.update(&mut endpoint_b);
assert_eq!(leaf.active_session_count(), 0);
assert_eq!(leaf.pending_packet_count(), 0);
assert_hook_removed(&endpoint_b, hook_id);
endpoint_b.add_connection(ENDPOINT_A, true);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
let packets = drain_parent_pty_packets(&mut endpoint_a);
assert!(packets.is_empty());
assert_eq!(leaf.active_session_count(), 0);
assert_hook_present(&endpoint_a, hook_id);
assert_hook_removed(&endpoint_b, hook_id);
}
@@ -0,0 +1,44 @@
use alloc::{vec, vec::Vec};
use unshell::protocol::{Leaf, Packet};
use crate::{FakePtyLeaf, FakePtyState, pty_open_packet};
use super::super::support::{ENDPOINT_A, ENDPOINT_B, PROC_OTHER, endpoint_at};
#[test]
fn pty_leaf_does_not_consume_other_leaf_packets() {
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
endpoint.add_connection(ENDPOINT_A, true);
endpoint
.add_inbound_from(ENDPOINT_A, pty_open_packet(vec![ENDPOINT_A, ENDPOINT_B], 7))
.unwrap();
endpoint
.add_inbound_from(
ENDPOINT_A,
Packet {
hook_id: 8,
end_hook: false,
path: vec![ENDPOINT_A, ENDPOINT_B],
procedure_id: PROC_OTHER,
data: b"leave-me".to_vec(),
},
)
.unwrap();
leaf.update(&mut endpoint);
let mut other_packets = Vec::new();
endpoint.take_inbound_matching(
ENDPOINT_B,
|packet| packet.procedure_id == PROC_OTHER,
|packet| other_packets.push(packet),
);
assert_eq!(leaf.active_session_count(), 1);
assert_eq!(other_packets.len(), 1);
assert_eq!(other_packets[0].procedure_id, PROC_OTHER);
assert_eq!(other_packets[0].data, b"leave-me".to_vec());
}
@@ -0,0 +1,33 @@
use unshell::protocol::Leaf;
use crate::{FakePtyLeaf, FakePtyState, OP_INPUT, OP_OUTPUT};
use super::super::support::{
ENDPOINT_A, ENDPOINT_B, assert_frame, assert_hook_present, drain_parent_pty_packets,
open_pty_session, pty_endpoints, send_downward_frame, transfer_packets,
};
#[test]
fn input_and_output_share_one_hook() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
drain_parent_pty_packets(&mut endpoint_a);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
hook_id,
OP_INPUT,
b"hello",
false,
);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
let packets = drain_parent_pty_packets(&mut endpoint_a);
assert_eq!(packets.len(), 1);
assert_frame(&packets[0], hook_id, OP_OUTPUT, false, b"hello");
assert_hook_present(&endpoint_a, hook_id);
assert_hook_present(&endpoint_b, hook_id);
}
@@ -0,0 +1,145 @@
use unshell::protocol::Leaf;
use crate::{
FakePtyLeaf, FakePtyState, OP_ABORT, OP_ERROR, OP_EXIT, OP_INPUT, OP_STDIN_EOF, OP_TERMINATE,
};
use super::super::support::{
ENDPOINT_A, ENDPOINT_B, assert_frame, assert_hook_present, assert_hook_removed, assert_opened,
drain_parent_pty_packets, open_pty_session, pty_endpoints, send_downward_frame,
transfer_packets,
};
#[test]
fn open_pty_paves_hook_and_creates_session() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
let packets = drain_parent_pty_packets(&mut endpoint_a);
assert_eq!(leaf.active_session_count(), 1);
assert_eq!(leaf.state().active_count, 1);
assert_eq!(leaf.state().total_opened, 1);
assert_hook_present(&endpoint_a, hook_id);
assert_hook_present(&endpoint_b, hook_id);
assert_eq!(packets.len(), 1);
assert_opened(&packets[0], hook_id);
}
#[test]
fn stdin_eof_keeps_hook_until_exit() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
drain_parent_pty_packets(&mut endpoint_a);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
hook_id,
OP_STDIN_EOF,
&[],
false,
);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
assert_eq!(leaf.state().last_stdin_eof_hook, Some(hook_id));
assert!(drain_parent_pty_packets(&mut endpoint_a).is_empty());
assert_hook_present(&endpoint_a, hook_id);
assert_hook_present(&endpoint_b, hook_id);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
hook_id,
OP_TERMINATE,
&[],
false,
);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
let packets = drain_parent_pty_packets(&mut endpoint_a);
assert_eq!(packets.len(), 1);
assert_frame(&packets[0], hook_id, OP_EXIT, true, &[0]);
assert_eq!(leaf.active_session_count(), 0);
assert_hook_removed(&endpoint_a, hook_id);
assert_hook_removed(&endpoint_b, hook_id);
}
#[test]
fn exit_end_hook_cleans_route_and_session() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
drain_parent_pty_packets(&mut endpoint_a);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
hook_id,
OP_TERMINATE,
&[],
false,
);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
let packets = drain_parent_pty_packets(&mut endpoint_a);
assert_eq!(packets.len(), 1);
assert_frame(&packets[0], hook_id, OP_EXIT, true, &[0]);
assert_eq!(leaf.active_session_count(), 0);
assert_hook_removed(&endpoint_a, hook_id);
assert_hook_removed(&endpoint_b, hook_id);
}
#[test]
fn abort_downward_end_hook_closes_without_ack() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
drain_parent_pty_packets(&mut endpoint_a);
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
hook_id,
OP_ABORT,
&[],
true,
);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
assert_eq!(leaf.active_session_count(), 0);
assert!(drain_parent_pty_packets(&mut endpoint_a).is_empty());
assert_hook_removed(&endpoint_a, hook_id);
assert_hook_removed(&endpoint_b, hook_id);
}
#[test]
fn unknown_session_input_returns_error_end_hook() {
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
let hook_id = endpoint_a.get_hook_id();
send_downward_frame(
&mut endpoint_a,
&mut endpoint_b,
hook_id,
OP_INPUT,
b"orphan",
false,
);
leaf.update(&mut endpoint_b);
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
let packets = drain_parent_pty_packets(&mut endpoint_a);
assert_eq!(packets.len(), 1);
assert_frame(&packets[0], hook_id, OP_ERROR, true, b"unknown-session");
assert_eq!(leaf.active_session_count(), 0);
assert_hook_removed(&endpoint_a, hook_id);
assert_hook_removed(&endpoint_b, hook_id);
}
@@ -0,0 +1,5 @@
mod concurrency;
mod failure;
mod filtering;
mod input_output;
mod lifecycle;
@@ -1,137 +0,0 @@
use alloc::{vec, vec::Vec};
use unshell::protocol::{Endpoint, Leaf, Packet};
use crate::{
FakePtyLeaf, OP_OPENED, PROC_PTY, frame_opcode, frame_payload, pty_open_packet, pty_packet,
};
pub(super) const ENDPOINT_A: u32 = 0;
pub(super) const ENDPOINT_B: u32 = 1;
pub(super) const PROC_OTHER: u32 = 31;
/// Creates a bare endpoint at a known absolute path.
pub(super) fn endpoint_at(id: u32, path: Vec<u32>) -> Endpoint {
let mut endpoint = Endpoint::new(id);
endpoint.path = path;
endpoint
}
/// Creates the parent/child endpoint pair used by PTY session tests.
pub(super) fn pty_endpoints() -> (Endpoint, Endpoint) {
let mut endpoint_a = endpoint_at(ENDPOINT_A, vec![ENDPOINT_A]);
let mut endpoint_b = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
endpoint_a.add_connection(ENDPOINT_B, false);
endpoint_b.add_connection(ENDPOINT_A, true);
(endpoint_a, endpoint_b)
}
/// Transfers every queued packet for `next_hop` into `receiver` as `remote_id` traffic.
pub(super) fn transfer_packets(
sender: &mut Endpoint,
receiver: &mut Endpoint,
next_hop: u32,
remote_id: u32,
) {
let mut packets = Vec::new();
sender.take_outbound_clear(next_hop, |packet| packets.push(packet.clone()));
for packet in packets {
receiver.add_inbound_from(remote_id, packet).unwrap();
}
}
/// Sends one downward PTY frame from endpoint A to endpoint B.
pub(super) fn send_downward_frame(
endpoint_a: &mut Endpoint,
endpoint_b: &mut Endpoint,
hook_id: u16,
opcode: u8,
payload: &[u8],
end_hook: bool,
) {
endpoint_a
.add_outbound(pty_packet(
vec![ENDPOINT_A, ENDPOINT_B],
hook_id,
end_hook,
opcode,
payload,
))
.unwrap();
transfer_packets(endpoint_a, endpoint_b, ENDPOINT_B, ENDPOINT_A);
}
/// Opens a fake PTY session and delivers the `Opened` response to endpoint A.
pub(super) fn open_pty_session(
endpoint_a: &mut Endpoint,
endpoint_b: &mut Endpoint,
leaf: &mut FakePtyLeaf,
) -> u16 {
let hook_id = endpoint_a.get_hook_id();
endpoint_a
.add_outbound(pty_open_packet(vec![ENDPOINT_A, ENDPOINT_B], hook_id))
.unwrap();
transfer_packets(endpoint_a, endpoint_b, ENDPOINT_B, ENDPOINT_A);
leaf.update(endpoint_b);
transfer_packets(endpoint_b, endpoint_a, ENDPOINT_A, ENDPOINT_B);
hook_id
}
/// Drains packets for `procedure_id` delivered to endpoint A.
pub(super) fn drain_parent_packets(endpoint: &mut Endpoint, procedure_id: u32) -> Vec<Packet> {
let mut packets = Vec::new();
endpoint.take_inbound_matching(
ENDPOINT_A,
|packet| packet.procedure_id == procedure_id,
|packet| packets.push(packet),
);
packets
}
/// Drains PTY packets delivered to endpoint A.
pub(super) fn drain_parent_pty_packets(endpoint: &mut Endpoint) -> Vec<Packet> {
drain_parent_packets(endpoint, PROC_PTY)
}
/// Asserts that local hook state still contains `hook_id`.
pub(super) fn assert_hook_present(endpoint: &Endpoint, hook_id: u16) {
assert!(endpoint.has_hook(hook_id));
}
/// Asserts that local hook state no longer contains `hook_id`.
pub(super) fn assert_hook_removed(endpoint: &Endpoint, hook_id: u16) {
assert!(!endpoint.has_hook(hook_id));
}
/// Asserts that `packet` carries the expected PTY frame.
pub(super) fn assert_frame(
packet: &Packet,
hook_id: u16,
opcode: u8,
end_hook: bool,
payload: &[u8],
) {
assert_eq!(packet.hook_id, hook_id);
assert_eq!(packet.end_hook, end_hook);
assert_eq!(frame_opcode(packet), Some(opcode));
assert_eq!(frame_payload(packet), payload);
}
/// Returns true when `packets` contains the requested frame.
pub(super) fn has_frame(packets: &[Packet], hook_id: u16, opcode: u8, payload: &[u8]) -> bool {
packets.iter().any(|packet| {
packet.hook_id == hook_id
&& frame_opcode(packet) == Some(opcode)
&& frame_payload(packet) == payload
})
}
/// Asserts that a packet is the fake PTY open acknowledgement.
pub(super) fn assert_opened(packet: &Packet, hook_id: u16) {
assert_frame(packet, hook_id, OP_OPENED, false, &[]);
}
@@ -0,0 +1,47 @@
use unshell::protocol::{Endpoint, Packet};
use crate::{OP_OPENED, frame_opcode, frame_payload};
/// Asserts that local hook state still contains `hook_id`.
pub(crate) fn assert_hook_present(endpoint: &Endpoint, hook_id: u16) {
assert!(
endpoint.has_hook(hook_id),
"expected hook {hook_id} to remain registered"
);
}
/// Asserts that local hook state no longer contains `hook_id`.
pub(crate) fn assert_hook_removed(endpoint: &Endpoint, hook_id: u16) {
assert!(
!endpoint.has_hook(hook_id),
"expected hook {hook_id} to be cleaned up"
);
}
/// Asserts that `packet` carries the expected PTY frame.
pub(crate) fn assert_frame(
packet: &Packet,
hook_id: u16,
opcode: u8,
end_hook: bool,
payload: &[u8],
) {
assert_eq!(packet.hook_id, hook_id);
assert_eq!(packet.end_hook, end_hook);
assert_eq!(frame_opcode(packet), Some(opcode));
assert_eq!(frame_payload(packet), payload);
}
/// Returns true when `packets` contains the requested frame.
pub(crate) fn has_frame(packets: &[Packet], hook_id: u16, opcode: u8, payload: &[u8]) -> bool {
packets.iter().any(|packet| {
packet.hook_id == hook_id
&& frame_opcode(packet) == Some(opcode)
&& frame_payload(packet) == payload
})
}
/// Asserts that a packet is the fake PTY open acknowledgement.
pub(crate) fn assert_opened(packet: &Packet, hook_id: u16) {
assert_frame(packet, hook_id, OP_OPENED, false, &[]);
}
@@ -0,0 +1,23 @@
use alloc::vec::Vec;
use unshell::protocol::{Endpoint, Packet};
use crate::PROC_PTY;
use super::ENDPOINT_A;
/// Drains packets for `procedure_id` delivered to endpoint A.
pub(crate) fn drain_parent_packets(endpoint: &mut Endpoint, procedure_id: u32) -> Vec<Packet> {
let mut packets = Vec::new();
endpoint.take_inbound_matching(
ENDPOINT_A,
|packet| packet.procedure_id == procedure_id,
|packet| packets.push(packet),
);
packets
}
/// Drains PTY packets delivered to endpoint A.
pub(crate) fn drain_parent_pty_packets(endpoint: &mut Endpoint) -> Vec<Packet> {
drain_parent_packets(endpoint, PROC_PTY)
}
@@ -0,0 +1,40 @@
use alloc::{vec, vec::Vec};
use unshell::protocol::{Endpoint, Packet};
pub(crate) const ENDPOINT_A: u32 = 0;
pub(crate) const ENDPOINT_B: u32 = 1;
pub(crate) const PROC_OTHER: u32 = 31;
/// Creates a bare endpoint at a known absolute path.
pub(crate) fn endpoint_at(id: u32, path: Vec<u32>) -> Endpoint {
let mut endpoint = Endpoint::new(id);
endpoint.path = path;
endpoint
}
/// Creates the parent/child endpoint pair used by PTY session tests.
pub(crate) fn pty_endpoints() -> (Endpoint, Endpoint) {
let mut endpoint_a = endpoint_at(ENDPOINT_A, vec![ENDPOINT_A]);
let mut endpoint_b = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
endpoint_a.add_connection(ENDPOINT_B, false);
endpoint_b.add_connection(ENDPOINT_A, true);
(endpoint_a, endpoint_b)
}
/// Transfers every queued packet for `next_hop` into `receiver` as `remote_id` traffic.
pub(crate) fn transfer_packets(
sender: &mut Endpoint,
receiver: &mut Endpoint,
next_hop: u32,
remote_id: u32,
) {
let mut packets = Vec::<Packet>::new();
sender.take_outbound_clear(next_hop, |packet| packets.push(packet.clone()));
for packet in packets {
receiver.add_inbound_from(remote_id, packet).unwrap();
}
}
@@ -0,0 +1,9 @@
mod assertions;
mod drains;
mod endpoints;
mod packets;
pub(crate) use assertions::*;
pub(crate) use drains::*;
pub(crate) use endpoints::*;
pub(crate) use packets::*;
@@ -0,0 +1,46 @@
use alloc::vec;
use unshell::protocol::{Endpoint, Leaf};
use crate::{FakePtyLeaf, pty_open_packet, pty_packet};
use super::{ENDPOINT_A, ENDPOINT_B, transfer_packets};
/// Sends one downward PTY frame from endpoint A to endpoint B.
pub(crate) fn send_downward_frame(
endpoint_a: &mut Endpoint,
endpoint_b: &mut Endpoint,
hook_id: u16,
opcode: u8,
payload: &[u8],
end_hook: bool,
) {
endpoint_a
.add_outbound(pty_packet(
vec![ENDPOINT_A, ENDPOINT_B],
hook_id,
end_hook,
opcode,
payload,
))
.unwrap();
transfer_packets(endpoint_a, endpoint_b, ENDPOINT_B, ENDPOINT_A);
}
/// Opens a fake PTY session and delivers the `Opened` response to endpoint A.
pub(crate) fn open_pty_session(
endpoint_a: &mut Endpoint,
endpoint_b: &mut Endpoint,
leaf: &mut FakePtyLeaf,
) -> u16 {
let hook_id = endpoint_a.get_hook_id();
endpoint_a
.add_outbound(pty_open_packet(vec![ENDPOINT_A, ENDPOINT_B], hook_id))
.unwrap();
transfer_packets(endpoint_a, endpoint_b, ENDPOINT_B, ENDPOINT_A);
leaf.update(endpoint_b);
transfer_packets(endpoint_b, endpoint_a, ENDPOINT_A, ENDPOINT_B);
hook_id
}