mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Add stateful call leaf runtime
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
use alloc::{borrow::ToOwned, format, string::String, vec, vec::Vec};
|
||||
use core::convert::Infallible;
|
||||
|
||||
use rkyv::{Archive, Deserialize, Serialize};
|
||||
|
||||
use crate::protocol::tree::{
|
||||
Call, CallLeaf, ChildRoute, ConnectionState, Ingress, LeafRuntime, ProtocolEndpoint,
|
||||
decode_call_input, encode_call_reply,
|
||||
};
|
||||
use crate::protocol::{PacketType, decode_frame};
|
||||
use crate::{Leaf, procedures};
|
||||
|
||||
fn path(parts: &[&str]) -> Vec<String> {
|
||||
parts.iter().map(|part| (*part).to_owned()).collect()
|
||||
}
|
||||
|
||||
#[derive(Leaf)]
|
||||
#[leaf(id = "org.example.v1.echo")]
|
||||
struct EchoLeaf {
|
||||
prefix: String,
|
||||
}
|
||||
|
||||
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
struct EchoRequest {
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
struct EchoResponse {
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[procedures(error = Infallible)]
|
||||
impl EchoLeaf {
|
||||
#[call]
|
||||
fn echo(&mut self, request: Call<EchoRequest>) -> EchoResponse {
|
||||
EchoResponse {
|
||||
text: format!("{}{}", self.prefix, request.input.text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CallLeaf for EchoLeaf {
|
||||
type Error = Infallible;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaf_runtime_dispatches_generated_call_procedure() {
|
||||
let endpoint = ProtocolEndpoint::new(
|
||||
path(&["agent"]),
|
||||
Some(Vec::new()),
|
||||
Vec::new(),
|
||||
vec![EchoLeaf::protocol_leaf_spec()],
|
||||
);
|
||||
let mut runtime = LeafRuntime::new(
|
||||
endpoint,
|
||||
EchoLeaf {
|
||||
prefix: String::from("echo: "),
|
||||
},
|
||||
);
|
||||
|
||||
let mut controller = ProtocolEndpoint::new(
|
||||
Vec::new(),
|
||||
None,
|
||||
vec![ChildRoute {
|
||||
path: path(&["agent"]),
|
||||
state: ConnectionState::Registered,
|
||||
}],
|
||||
Vec::new(),
|
||||
);
|
||||
let hook_id = controller.allocate_hook_id();
|
||||
let controller_outcome = controller
|
||||
.send_call(
|
||||
path(&["agent"]),
|
||||
Some(EchoLeaf::protocol_leaf_name()),
|
||||
EchoLeaf::protocol_procedure_id("echo").expect("generated suffix should resolve"),
|
||||
Some(hook_id),
|
||||
encode_call_reply(&EchoRequest {
|
||||
text: String::from("hello"),
|
||||
})
|
||||
.expect("request should encode"),
|
||||
)
|
||||
.expect("call should encode");
|
||||
let Some((_, frame)) = controller_outcome.forward else {
|
||||
panic!("controller should forward call to child");
|
||||
};
|
||||
|
||||
let outcome = runtime
|
||||
.receive(&Ingress::Parent, frame)
|
||||
.expect("runtime should handle call");
|
||||
let [response_frame] = outcome.frames.as_slice() else {
|
||||
panic!("expected one response frame");
|
||||
};
|
||||
|
||||
let parsed = decode_frame(response_frame.as_slice()).expect("response frame should decode");
|
||||
assert_eq!(parsed.packet_type(), PacketType::Data);
|
||||
let response = decode_call_input::<EchoResponse>(
|
||||
parsed
|
||||
.deserialize_data()
|
||||
.expect("data payload should deserialize")
|
||||
.data
|
||||
.as_slice(),
|
||||
)
|
||||
.expect("typed response should decode");
|
||||
assert_eq!(response.text, "echo: hello");
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
mod call;
|
||||
mod protocol;
|
||||
mod tree;
|
||||
|
||||
@@ -81,6 +81,7 @@ fn protocol_endpoint_introspection_returns_leaf_summary() {
|
||||
let LocalEvent::Data {
|
||||
header,
|
||||
message: response,
|
||||
..
|
||||
} = outcome.event.as_ref().expect("expected local data event")
|
||||
else {
|
||||
panic!("expected local data event");
|
||||
@@ -142,7 +143,9 @@ fn invalid_hook_peer_emits_local_fault_event() {
|
||||
assert!(!outcome.dropped);
|
||||
|
||||
match outcome.event.as_ref().expect("expected event") {
|
||||
LocalEvent::Fault { header, message } => {
|
||||
LocalEvent::Fault {
|
||||
header, message, ..
|
||||
} => {
|
||||
assert_eq!(header.packet_type, PacketType::Fault);
|
||||
assert_eq!(header.hook_id, Some(hook_id));
|
||||
assert_eq!(
|
||||
@@ -251,3 +254,68 @@ fn pending_hook_fault_is_delivered_before_activation() {
|
||||
|
||||
assert!(outcome.forward.is_some() || outcome.event.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callee_side_end_hook_marks_local_end_before_peer_close() {
|
||||
let mut endpoint = ProtocolEndpoint::new(path(&["server"]), None, Vec::new(), Vec::new());
|
||||
endpoint
|
||||
.add_endpoint_procedure("example.service.v1.invoke")
|
||||
.expect("procedure registration should succeed");
|
||||
let frame = encode_packet(
|
||||
&PacketHeader {
|
||||
packet_type: PacketType::Call,
|
||||
src_path: Vec::new(),
|
||||
dst_path: path(&["server"]),
|
||||
dst_leaf: None,
|
||||
hook_id: None,
|
||||
},
|
||||
&crate::protocol::CallMessage {
|
||||
procedure_id: "example.service.v1.invoke".to_owned(),
|
||||
data: vec![1],
|
||||
response_hook: Some(crate::protocol::HookTarget {
|
||||
hook_id: 21,
|
||||
return_path: Vec::new(),
|
||||
}),
|
||||
},
|
||||
)
|
||||
.expect("call should encode");
|
||||
|
||||
endpoint
|
||||
.receive(&Ingress::Parent, frame)
|
||||
.expect("callee should accept call");
|
||||
|
||||
let key = crate::protocol::tree::HookKey::new(Vec::new(), 21);
|
||||
assert!(endpoint.hooks.active(&key).is_some());
|
||||
|
||||
endpoint
|
||||
.send_data(
|
||||
Vec::new(),
|
||||
21,
|
||||
"example.service.v1.invoke",
|
||||
Vec::new(),
|
||||
true,
|
||||
)
|
||||
.expect("callee local end should succeed");
|
||||
assert!(endpoint.hooks.active(&key).is_some());
|
||||
|
||||
let peer_final = encode_packet(
|
||||
&PacketHeader {
|
||||
packet_type: PacketType::Data,
|
||||
src_path: Vec::new(),
|
||||
dst_path: path(&["server"]),
|
||||
dst_leaf: None,
|
||||
hook_id: Some(21),
|
||||
},
|
||||
&DataMessage {
|
||||
procedure_id: "example.service.v1.invoke".to_owned(),
|
||||
data: Vec::new(),
|
||||
end_hook: true,
|
||||
},
|
||||
)
|
||||
.expect("peer final data should encode");
|
||||
|
||||
endpoint
|
||||
.receive(&Ingress::Parent, peer_final)
|
||||
.expect("callee should accept peer close");
|
||||
assert!(endpoint.hooks.active(&key).is_none());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user