use async_trait::async_trait;
use futures::{SinkExt, StreamExt};
use simple_doip::{
Error, LogicalAddress,
client::{AddressType, Client, ClientOptions, RoutingActivationOptions},
connection::Connector,
message_codec::MessageCodec,
messages::{
ActivationTypeCode, DiagnosticAckCode, DiagnosticMessage, Encode, OwnedMessage,
OwnedPayload, ProtocolVersion, RoutingActivationRequest, RoutingActivationResponseCode,
},
server::{ResponseWriter, Server, ServerConnectionHandler},
};
use std::{
net::{IpAddr, SocketAddr},
sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
},
time::Duration,
};
use tokio::{
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
net::{
TcpListener, TcpStream,
tcp::{OwnedReadHalf, OwnedWriteHalf},
},
task::JoinHandle,
};
use tokio_util::codec::{FramedRead, FramedWrite};
const TEST_TIMEOUT: Duration = Duration::from_secs(5);
const SERVER_LOGICAL_ADDRESS: LogicalAddress = LogicalAddress(0x0001);
const CLIENT_LOGICAL_ADDRESS: LogicalAddress = LogicalAddress(0x0E01);
async fn with_timeout<F: std::future::Future>(context: &str, fut: F) -> F::Output {
tokio::time::timeout(TEST_TIMEOUT, fut)
.await
.unwrap_or_else(|_| panic!("timed out waiting for: {context}"))
}
async fn send_positive_ack(
handler: &impl ServerConnectionHandler,
message: &DiagnosticMessage<'_>,
responses: &mut dyn ResponseWriter,
) -> Result<(), Error> {
responses
.send(OwnedMessage::diagnostic_message_ack(
handler.protocol_version(),
handler.get_logical_address(),
message.source_address,
DiagnosticAckCode::RoutingConfirmationAck,
message.user_data.to_vec(),
))
.await
}
struct TestHandler {
last_diagnostic_payload: Arc<Mutex<Option<Vec<u8>>>>,
routing_activation_requests: Arc<AtomicUsize>,
}
#[async_trait]
impl ServerConnectionHandler for TestHandler {
fn get_vin(&self) -> [u8; 17] {
[0x00; 17]
}
fn get_logical_address(&self) -> LogicalAddress {
SERVER_LOGICAL_ADDRESS
}
fn get_entity_id(&self) -> [u8; 6] {
[0x00; 6]
}
fn get_group_id(&self) -> Option<[u8; 6]> {
None
}
async fn routing_activation(
&self,
request: &RoutingActivationRequest,
) -> Result<OwnedMessage, Error> {
self.routing_activation_requests
.fetch_add(1, Ordering::SeqCst);
Ok(OwnedMessage::routing_activation_response(
self.protocol_version(),
request.source_address,
self.get_logical_address(),
RoutingActivationResponseCode::RoutingSuccessfullyActivated,
[0; 4],
None,
))
}
async fn diagnostic_message(
&self,
message: &DiagnosticMessage<'_>,
responses: &mut dyn ResponseWriter,
) -> Result<(), Error> {
*self.last_diagnostic_payload.lock().unwrap() = Some(message.user_data.to_vec());
send_positive_ack(self, message, responses).await
}
}
struct TestServer {
addr: SocketAddr,
last_diagnostic_payload: Arc<Mutex<Option<Vec<u8>>>>,
routing_activation_requests: Arc<AtomicUsize>,
accept_loop: JoinHandle<()>,
}
impl TestServer {
async fn shutdown(self) {
assert!(
!self.accept_loop.is_finished(),
"server accept loop died during the test (a connection handler panicked?)"
);
self.accept_loop.abort();
let _ = self.accept_loop.await;
}
}
async fn start_server() -> TestServer {
let last_diagnostic_payload = Arc::new(Mutex::new(None));
let routing_activation_requests = Arc::new(AtomicUsize::new(0));
let handler = TestHandler {
last_diagnostic_payload: Arc::clone(&last_diagnostic_payload),
routing_activation_requests: Arc::clone(&routing_activation_requests),
};
let (addr, accept_loop) = start_server_with(handler).await;
TestServer {
addr,
last_diagnostic_payload,
routing_activation_requests,
accept_loop,
}
}
async fn start_server_with<H>(handler: H) -> (SocketAddr, JoinHandle<()>)
where
H: ServerConnectionHandler + Send + Sync + 'static,
{
let server = Server::new(handler).expect("server should construct");
let listener = TcpListener::bind(("127.0.0.1", 0))
.await
.expect("failed to bind test server to an ephemeral port");
let addr = listener
.local_addr()
.expect("bound listener has a local address");
let accept_loop = tokio::spawn(async move {
loop {
let Ok((stream, peer_addr)) = listener.accept().await else {
break;
};
let _ = server.handle_client_connection(peer_addr, stream).await;
}
});
(addr, accept_loop)
}
async fn read_message<R>(framed: &mut FramedRead<R, MessageCodec>) -> OwnedMessage
where
R: AsyncRead + Unpin,
{
with_timeout("read message", framed.next())
.await
.expect("stream closed before a message arrived")
.expect("decode message")
}
async fn send_routing_activation<W>(
framed: &mut FramedWrite<W, MessageCodec>,
source_address: LogicalAddress,
) where
W: AsyncWrite + Unpin,
{
let request = OwnedMessage::routing_activation_request(
ProtocolVersion::V2012,
source_address,
ActivationTypeCode::Default,
None,
);
with_timeout("send routing activation", framed.send(&request))
.await
.expect("send routing activation");
}
async fn send_diagnostic_message<W>(
framed: &mut FramedWrite<W, MessageCodec>,
source_address: LogicalAddress,
user_data: &[u8],
) where
W: AsyncWrite + Unpin,
{
let request = OwnedMessage::diagnostic_message(
ProtocolVersion::V2012,
source_address,
SERVER_LOGICAL_ADDRESS,
user_data.to_vec(),
);
with_timeout("send diagnostic message", framed.send(&request))
.await
.expect("send diagnostic message");
}
#[derive(Clone, Copy, Debug)]
struct TestConnector;
#[async_trait]
impl Connector for TestConnector {
async fn establish_connection(
gateway_address: SocketAddr,
) -> Result<(OwnedReadHalf, OwnedWriteHalf), Error> {
let stream =
tokio::time::timeout(TEST_TIMEOUT, TcpStream::connect(gateway_address)).await??;
stream.set_nodelay(true)?;
Ok(stream.into_split())
}
}
fn client_options(server_addr: SocketAddr) -> ClientOptions {
ClientOptions {
server_address: server_addr,
server_logical_address: SERVER_LOGICAL_ADDRESS,
server_physical_address: SERVER_LOGICAL_ADDRESS,
client_address: IpAddr::from([0, 0, 0, 0]),
client_logical_address: CLIENT_LOGICAL_ADDRESS,
protocol_version: ProtocolVersion::V2012,
routing_activation_options: Some(RoutingActivationOptions {
activation_type: ActivationTypeCode::Default,
oem_specific: None,
}),
diagnostic_message_timeout: simple_doip::TIMEOUT_DIAGNOSTIC_MESSAGE_RESPONSE,
}
}
async fn connect_and_activate(
server_addr: SocketAddr,
server: &TestServer,
) -> Client<TestConnector> {
let requests_before = server.routing_activation_requests.load(Ordering::SeqCst);
let client = with_timeout(
"client connect + routing activation",
Client::<TestConnector>::connect(client_options(server_addr)),
)
.await
.expect("client should connect and activate routing successfully");
assert_eq!(
server.routing_activation_requests.load(Ordering::SeqCst),
requests_before + 1,
"server should have processed exactly one routing activation request"
);
client
}
#[tokio::test]
async fn routing_activation_succeeds() {
let server = start_server().await;
let client = connect_and_activate(server.addr, &server).await;
with_timeout("client shutdown", client.shut_down()).await;
server.shutdown().await;
}
#[tokio::test]
async fn diagnostic_message_round_trip() {
let server = start_server().await;
let mut client = connect_and_activate(server.addr, &server).await;
let request_bytes = vec![0x10, 0x03];
let send_result = with_timeout(
"send_diagnostic_message",
client.send_diagnostic_message(AddressType::Physical, request_bytes.clone()),
)
.await;
assert!(
send_result.is_ok(),
"expected a positive ACK for the diagnostic message, got {send_result:?}"
);
let received = server
.last_diagnostic_payload
.lock()
.unwrap()
.clone()
.expect("server handler should have recorded the diagnostic payload");
assert_eq!(
received, request_bytes,
"server should receive exactly the bytes the client sent"
);
with_timeout("client shutdown", client.shut_down()).await;
server.shutdown().await;
}
async fn wait_for_connection_close(stream: &mut TcpStream) {
let mut buf = [0u8; 64];
with_timeout("peer closing the raw connection", async {
loop {
match stream.read(&mut buf).await {
Ok(0) | Err(_) => return, Ok(_) => {} }
}
})
.await;
}
#[tokio::test]
async fn unsupported_payload_type_does_not_kill_server() {
let server = start_server().await;
let mut raw_stream = with_timeout("raw connect", TcpStream::connect(server.addr))
.await
.expect("raw TCP connection should succeed");
let unsupported_frame = [0x02, 0xFD, 0x40, 0x03, 0x00, 0x00, 0x00, 0x00];
let routing_activation_request = OwnedMessage::routing_activation_request(
ProtocolVersion::V2012,
CLIENT_LOGICAL_ADDRESS,
ActivationTypeCode::Default,
None,
);
let mut activation_bytes = vec![0u8; routing_activation_request.encoded_size().unwrap()];
let written = {
let mut writer: &mut [u8] = &mut activation_bytes;
routing_activation_request.encode(&mut writer).unwrap()
};
activation_bytes.truncate(written);
with_timeout(
"write unsupported-payload frame followed by a valid routing activation request",
async {
raw_stream.write_all(&unsupported_frame).await?;
raw_stream.write_all(&activation_bytes).await
},
)
.await
.expect("writes should succeed");
let mut response_buf = [0u8; 64];
let read = with_timeout(
"read routing activation response",
raw_stream.read(&mut response_buf),
)
.await
.expect("read should succeed");
assert!(
read > 0,
"server should have sent a routing activation response"
);
assert_eq!(
server.routing_activation_requests.load(Ordering::SeqCst),
1,
"server should have processed the routing activation request that followed the \
skipped unsupported frame, on the same connection"
);
drop(raw_stream);
server.shutdown().await;
}
#[tokio::test]
async fn malformed_header_does_not_kill_server() {
let server = start_server().await;
let mut raw_stream = with_timeout("raw connect", TcpStream::connect(server.addr))
.await
.expect("raw TCP connection should succeed");
let corrupt_header = [0x02, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
with_timeout(
"write malformed header",
raw_stream.write_all(&corrupt_header),
)
.await
.expect("write should succeed");
wait_for_connection_close(&mut raw_stream).await;
let fresh_client = connect_and_activate(server.addr, &server).await;
with_timeout("client shutdown", fresh_client.shut_down()).await;
server.shutdown().await;
}
struct MisbehavingHandler;
#[async_trait]
impl ServerConnectionHandler for MisbehavingHandler {
fn get_vin(&self) -> [u8; 17] {
[0x00; 17]
}
fn get_logical_address(&self) -> LogicalAddress {
SERVER_LOGICAL_ADDRESS
}
fn get_entity_id(&self) -> [u8; 6] {
[0x00; 6]
}
fn get_group_id(&self) -> Option<[u8; 6]> {
None
}
async fn routing_activation(
&self,
request: &RoutingActivationRequest,
) -> Result<OwnedMessage, Error> {
let _ = request;
Ok(OwnedMessage::alive_check_response(
self.protocol_version(),
self.get_logical_address(),
))
}
async fn diagnostic_message(
&self,
message: &DiagnosticMessage<'_>,
responses: &mut dyn ResponseWriter,
) -> Result<(), Error> {
send_positive_ack(self, message, responses).await
}
}
#[tokio::test]
async fn wrong_routing_activation_response_type_errors_without_panicking() {
let server = Server::new(MisbehavingHandler).expect("server should construct");
let listener = TcpListener::bind(("127.0.0.1", 0))
.await
.expect("failed to bind test server to an ephemeral port");
let addr = listener
.local_addr()
.expect("bound listener has a local address");
let accept_loop = tokio::spawn(async move {
loop {
let Ok((stream, peer_addr)) = listener.accept().await else {
break;
};
let _ = server.handle_client_connection(peer_addr, stream).await;
}
});
let result = with_timeout(
"client connect against a misbehaving server",
Client::<TestConnector>::connect(client_options(addr)),
)
.await;
assert!(
matches!(result, Err(Error::UnexpectedMessageType(_))),
"a wrongly typed routing activation response must surface as UnexpectedMessageType \
without panicking or hanging; got: {result:?}"
);
accept_loop.abort();
let _ = accept_loop.await;
}
struct DenyingRoutingHandler;
#[async_trait]
impl ServerConnectionHandler for DenyingRoutingHandler {
fn get_vin(&self) -> [u8; 17] {
[0x00; 17]
}
fn get_logical_address(&self) -> LogicalAddress {
SERVER_LOGICAL_ADDRESS
}
fn get_entity_id(&self) -> [u8; 6] {
[0x00; 6]
}
fn get_group_id(&self) -> Option<[u8; 6]> {
None
}
async fn routing_activation(
&self,
request: &RoutingActivationRequest,
) -> Result<OwnedMessage, Error> {
Ok(OwnedMessage::routing_activation_response(
self.protocol_version(),
request.source_address,
self.get_logical_address(),
RoutingActivationResponseCode::DeniedSourceAddressAlreadyRegistered,
[0; 4],
None,
))
}
async fn diagnostic_message(
&self,
message: &DiagnosticMessage<'_>,
responses: &mut dyn ResponseWriter,
) -> Result<(), Error> {
send_positive_ack(self, message, responses).await
}
}
#[tokio::test]
async fn routing_activation_denial_surfaces_as_error() {
let server = Server::new(DenyingRoutingHandler).expect("server should construct");
let listener = TcpListener::bind(("127.0.0.1", 0))
.await
.expect("failed to bind test server to an ephemeral port");
let addr = listener
.local_addr()
.expect("bound listener has a local address");
let accept_loop = tokio::spawn(async move {
loop {
let Ok((stream, peer_addr)) = listener.accept().await else {
break;
};
let _ = server.handle_client_connection(peer_addr, stream).await;
}
});
let result = with_timeout(
"client connect against a denying server",
Client::<TestConnector>::connect(client_options(addr)),
)
.await;
assert!(
matches!(
result,
Err(Error::RoutingActivationDenied(
RoutingActivationResponseCode::DeniedSourceAddressAlreadyRegistered
))
),
"a routing activation denial must surface as RoutingActivationDenied with the \
reported code; got: {result:?}"
);
accept_loop.abort();
let _ = accept_loop.await;
}
struct NackingRoutingHandler;
#[async_trait]
impl ServerConnectionHandler for NackingRoutingHandler {
fn get_vin(&self) -> [u8; 17] {
[0x00; 17]
}
fn get_logical_address(&self) -> LogicalAddress {
SERVER_LOGICAL_ADDRESS
}
fn get_entity_id(&self) -> [u8; 6] {
[0x00; 6]
}
fn get_group_id(&self) -> Option<[u8; 6]> {
None
}
async fn routing_activation(
&self,
request: &RoutingActivationRequest,
) -> Result<OwnedMessage, Error> {
assert!(
DiagnosticAckCode::UnknownTargetAddress.is_negative_ack(),
"this test is only meaningful with a genuinely negative ack code"
);
Ok(OwnedMessage::diagnostic_message_ack(
self.protocol_version(),
self.get_logical_address(),
request.source_address,
DiagnosticAckCode::UnknownTargetAddress,
Vec::new(),
))
}
async fn diagnostic_message(
&self,
message: &DiagnosticMessage<'_>,
responses: &mut dyn ResponseWriter,
) -> Result<(), Error> {
send_positive_ack(self, message, responses).await
}
}
#[tokio::test]
async fn negative_ack_during_routing_activation_does_not_drop_pending_request() {
let server = Server::new(NackingRoutingHandler).expect("server should construct");
let listener = TcpListener::bind(("127.0.0.1", 0))
.await
.expect("failed to bind test server to an ephemeral port");
let addr = listener
.local_addr()
.expect("bound listener has a local address");
let accept_loop = tokio::spawn(async move {
loop {
let Ok((stream, peer_addr)) = listener.accept().await else {
break;
};
let _ = server.handle_client_connection(peer_addr, stream).await;
}
});
let result = with_timeout(
"client connect against a nacking server",
Client::<TestConnector>::connect(client_options(addr)),
)
.await;
assert!(
matches!(result, Err(Error::UnexpectedMessageType(_))),
"a negative DiagnosticMessageAck answering a routing activation must surface as \
UnexpectedMessageType, not be silently swallowed; got: {result:?}"
);
accept_loop.abort();
let _ = accept_loop.await;
}
#[tokio::test]
async fn cancelled_receive_diagnostic_response_does_not_brick_client() {
let server = start_server().await;
let mut client = connect_and_activate(server.addr, &server).await;
let cancelled = tokio::time::timeout(
Duration::from_millis(50),
client.receive_diagnostic_response(Duration::from_secs(30)),
)
.await;
assert!(
cancelled.is_err(),
"the receive future was supposed to be cancelled by the outer timeout, but it \
completed: {cancelled:?}"
);
let send_result = with_timeout(
"send_diagnostic_message after a cancelled receive",
client.send_diagnostic_message(AddressType::Physical, vec![0x10, 0x03]),
)
.await;
assert!(
send_result.is_ok(),
"a request issued after a cancelled receive_diagnostic_response must still be \
served; got: {send_result:?} (Err(ConnectionClosed) means the inner task died)"
);
with_timeout("client shutdown", client.shut_down()).await;
server.shutdown().await;
}
struct SilentOnDiagnosticHandler;
#[async_trait]
impl ServerConnectionHandler for SilentOnDiagnosticHandler {
fn get_vin(&self) -> [u8; 17] {
[0x00; 17]
}
fn get_logical_address(&self) -> LogicalAddress {
SERVER_LOGICAL_ADDRESS
}
fn get_entity_id(&self) -> [u8; 6] {
[0x00; 6]
}
fn get_group_id(&self) -> Option<[u8; 6]> {
None
}
async fn routing_activation(
&self,
request: &RoutingActivationRequest,
) -> Result<OwnedMessage, Error> {
Ok(OwnedMessage::routing_activation_response(
self.protocol_version(),
request.source_address,
self.get_logical_address(),
RoutingActivationResponseCode::RoutingSuccessfullyActivated,
[0; 4],
None,
))
}
async fn diagnostic_message(
&self,
_message: &DiagnosticMessage<'_>,
_responses: &mut dyn ResponseWriter,
) -> Result<(), Error> {
std::future::pending().await
}
}
#[tokio::test]
async fn timed_out_diagnostic_message_ack_surfaces_timeout_not_connection_closed() {
let server = Server::new(SilentOnDiagnosticHandler).expect("server should construct");
let listener = TcpListener::bind(("127.0.0.1", 0))
.await
.expect("failed to bind test server to an ephemeral port");
let addr = listener
.local_addr()
.expect("bound listener has a local address");
let accept_loop = tokio::spawn(async move {
loop {
let Ok((stream, peer_addr)) = listener.accept().await else {
break;
};
let _ = server.handle_client_connection(peer_addr, stream).await;
}
});
let mut client = with_timeout(
"client connect + routing activation",
Client::<TestConnector>::connect(client_options(addr)),
)
.await
.expect(
"client should connect and activate routing successfully against a server \
that behaves normally until the diagnostic message",
);
let result = with_timeout(
"send_diagnostic_message against a server that never acks",
client.send_diagnostic_message(AddressType::Physical, vec![0x10, 0x03]),
)
.await;
assert!(
matches!(result, Err(Error::ResponseTimeoutExceeded)),
"a diagnostic message that the server never acks must surface \
ResponseTimeoutExceeded, not ConnectionClosed or any other error; got: {result:?}"
);
accept_loop.abort();
let _ = accept_loop.await;
}
struct AckThenRespondHandler;
#[async_trait]
impl ServerConnectionHandler for AckThenRespondHandler {
fn get_vin(&self) -> [u8; 17] {
[0x00; 17]
}
fn get_logical_address(&self) -> LogicalAddress {
SERVER_LOGICAL_ADDRESS
}
fn get_entity_id(&self) -> [u8; 6] {
[0x00; 6]
}
fn get_group_id(&self) -> Option<[u8; 6]> {
None
}
async fn routing_activation(
&self,
request: &RoutingActivationRequest,
) -> Result<OwnedMessage, Error> {
Ok(OwnedMessage::routing_activation_response(
self.protocol_version(),
request.source_address,
self.get_logical_address(),
RoutingActivationResponseCode::RoutingSuccessfullyActivated,
[0; 4],
None,
))
}
async fn diagnostic_message(
&self,
message: &DiagnosticMessage<'_>,
responses: &mut dyn ResponseWriter,
) -> Result<(), Error> {
send_positive_ack(self, message, responses).await?;
responses
.send(OwnedMessage::diagnostic_message(
self.protocol_version(),
self.get_logical_address(),
message.source_address,
vec![0x62, 0xFD, 0x69, 0xAA],
))
.await?;
Ok(())
}
}
struct SlowAckThenRespondHandler;
#[async_trait]
impl ServerConnectionHandler for SlowAckThenRespondHandler {
fn get_vin(&self) -> [u8; 17] {
[0x00; 17]
}
fn get_logical_address(&self) -> LogicalAddress {
SERVER_LOGICAL_ADDRESS
}
fn get_entity_id(&self) -> [u8; 6] {
[0x00; 6]
}
fn get_group_id(&self) -> Option<[u8; 6]> {
None
}
async fn routing_activation(
&self,
request: &RoutingActivationRequest,
) -> Result<OwnedMessage, Error> {
Ok(OwnedMessage::routing_activation_response(
self.protocol_version(),
request.source_address,
self.get_logical_address(),
RoutingActivationResponseCode::RoutingSuccessfullyActivated,
[0; 4],
None,
))
}
async fn diagnostic_message(
&self,
message: &DiagnosticMessage<'_>,
responses: &mut dyn ResponseWriter,
) -> Result<(), Error> {
tokio::time::sleep(Duration::from_millis(200)).await;
send_positive_ack(self, message, responses).await?;
responses
.send(OwnedMessage::diagnostic_message(
self.protocol_version(),
self.get_logical_address(),
message.source_address,
vec![0x62, 0xFD, 0x69, 0xAA],
))
.await?;
Ok(())
}
}
#[tokio::test]
async fn ack_later_than_the_entity_requirement_but_inside_the_loss_timeout_succeeds() {
let (server_addr, accept_loop) = start_server_with(SlowAckThenRespondHandler).await;
let mut client = with_timeout(
"client connect + routing activation",
Client::<TestConnector>::connect(client_options(server_addr)),
)
.await
.expect("client should connect and activate routing");
let result = with_timeout(
"send_diagnostic_message against a server that acks after 200ms",
client.send_diagnostic_message(AddressType::Physical, vec![0x22, 0xFD, 0x69]),
)
.await;
assert!(
result.is_ok(),
"an ack arriving 200ms after the request is late by the entity's own 50ms \
requirement but far inside A_DoIP_Diagnostic_Message (2s), so the send must \
succeed rather than surfacing a timeout; got: {result:?}"
);
accept_loop.abort();
let _ = accept_loop.await;
}
#[tokio::test]
async fn diagnostic_message_timeout_below_the_ack_delay_expires() {
let (server_addr, accept_loop) = start_server_with(SlowAckThenRespondHandler).await;
let options =
client_options(server_addr).with_diagnostic_message_timeout(Duration::from_millis(20));
let mut client = with_timeout(
"connect with a 20ms diagnostic-message timeout",
Client::<TestConnector>::connect(options),
)
.await
.expect("client should connect and activate routing");
let result = with_timeout(
"send with a 20ms timeout against a 200ms ack",
client.send_diagnostic_message(AddressType::Physical, vec![0x22, 0xFD, 0x69]),
)
.await;
assert!(
matches!(result, Err(Error::ResponseTimeoutExceeded)),
"a 20ms configured timeout must expire against a 200ms ack — otherwise the \
configured value is being ignored; got: {result:?}"
);
accept_loop.abort();
let _ = accept_loop.await;
}
#[tokio::test]
async fn diagnostic_message_timeout_above_the_ack_delay_succeeds() {
let (server_addr, accept_loop) = start_server_with(SlowAckThenRespondHandler).await;
let options =
client_options(server_addr).with_diagnostic_message_timeout(Duration::from_millis(1500));
let mut client = with_timeout(
"connect with a 1500ms diagnostic-message timeout",
Client::<TestConnector>::connect(options),
)
.await
.expect("client should connect and activate routing");
let result = with_timeout(
"send with a 1500ms timeout against a 200ms ack",
client.send_diagnostic_message(AddressType::Physical, vec![0x22, 0xFD, 0x69]),
)
.await;
assert!(
result.is_ok(),
"a 200ms ack must succeed under a 1500ms configured timeout; got: {result:?}"
);
accept_loop.abort();
let _ = accept_loop.await;
}
#[tokio::test]
async fn handler_can_emit_ack_then_response() {
let (server_addr, accept_loop) = start_server_with(AckThenRespondHandler).await;
let mut stream = with_timeout("connect", TcpStream::connect(server_addr))
.await
.expect("connect to test server");
let (rx, tx) = stream.split();
let mut reader = FramedRead::new(rx, MessageCodec::new());
let mut writer = FramedWrite::new(tx, MessageCodec::new());
send_routing_activation(&mut writer, CLIENT_LOGICAL_ADDRESS).await;
let _activation = read_message(&mut reader).await;
send_diagnostic_message(&mut writer, CLIENT_LOGICAL_ADDRESS, &[0x22, 0xFD, 0x69]).await;
let first = read_message(&mut reader).await;
match first.payload {
OwnedPayload::DiagnosticMessageAck(ref ack) => {
assert_eq!(ack.ack_code, DiagnosticAckCode::RoutingConfirmationAck);
}
other => panic!("expected DiagnosticMessageAck first, got {other:?}"),
}
let second = read_message(&mut reader).await;
match second.payload {
OwnedPayload::DiagnosticMessage(ref diag) => {
assert_eq!(diag.user_data, vec![0x62, 0xFD, 0x69, 0xAA]);
}
other => panic!("expected DiagnosticMessage second, got {other:?}"),
}
accept_loop.abort();
let _ = accept_loop.await;
}
#[tokio::test]
async fn run_server_with_listener_serves_a_caller_bound_socket() {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral port");
let addr = listener.local_addr().expect("read local addr");
let server = Server::new(AckThenRespondHandler).expect("construct server");
let accept_loop = tokio::spawn(async move {
let _ = server.run_server_with_listener(listener).await;
});
let mut stream = with_timeout("connect", TcpStream::connect(addr))
.await
.expect("connect to caller-bound server");
let (rx, tx) = stream.split();
let mut reader = FramedRead::new(rx, MessageCodec::new());
let mut writer = FramedWrite::new(tx, MessageCodec::new());
send_routing_activation(&mut writer, CLIENT_LOGICAL_ADDRESS).await;
let activation = read_message(&mut reader).await;
assert!(matches!(
activation.payload,
OwnedPayload::RoutingActivationResponse(_)
));
accept_loop.abort();
let _ = accept_loop.await;
}
#[tokio::test]
async fn server_keeps_accepting_after_clients_disconnect_abruptly() {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral port");
let addr = listener.local_addr().expect("read local addr");
let server = Server::new(AckThenRespondHandler).expect("construct server");
let accept_loop = tokio::spawn(async move {
let _ = server.run_server_with_listener(listener).await;
});
for _ in 0..5 {
let stream = with_timeout("connect", TcpStream::connect(addr))
.await
.expect("connect");
drop(stream);
}
let mut stream = with_timeout("connect", TcpStream::connect(addr))
.await
.expect("connect after churn");
let (rx, tx) = stream.split();
let mut reader = FramedRead::new(rx, MessageCodec::new());
let mut writer = FramedWrite::new(tx, MessageCodec::new());
send_routing_activation(&mut writer, CLIENT_LOGICAL_ADDRESS).await;
let activation = read_message(&mut reader).await;
assert!(matches!(
activation.payload,
OwnedPayload::RoutingActivationResponse(_)
));
accept_loop.abort();
let _ = accept_loop.await;
}
struct HeldPendingHandler;
#[async_trait]
impl ServerConnectionHandler for HeldPendingHandler {
fn get_vin(&self) -> [u8; 17] {
[0x00; 17]
}
fn get_logical_address(&self) -> LogicalAddress {
SERVER_LOGICAL_ADDRESS
}
fn get_entity_id(&self) -> [u8; 6] {
[0x00; 6]
}
fn get_group_id(&self) -> Option<[u8; 6]> {
None
}
async fn routing_activation(
&self,
request: &RoutingActivationRequest,
) -> Result<OwnedMessage, Error> {
Ok(OwnedMessage::routing_activation_response(
self.protocol_version(),
request.source_address,
self.get_logical_address(),
RoutingActivationResponseCode::RoutingSuccessfullyActivated,
[0; 4],
None,
))
}
async fn diagnostic_message(
&self,
message: &DiagnosticMessage<'_>,
responses: &mut dyn ResponseWriter,
) -> Result<(), Error> {
send_positive_ack(self, message, responses).await?;
for _ in 0..2 {
responses
.send(OwnedMessage::diagnostic_message(
self.protocol_version(),
self.get_logical_address(),
message.source_address,
vec![0x7F, 0x22, 0x78],
))
.await?;
tokio::time::sleep(Duration::from_millis(50)).await;
}
responses
.send(OwnedMessage::diagnostic_message(
self.protocol_version(),
self.get_logical_address(),
message.source_address,
vec![0x62, 0xFD, 0x69, 0xAA],
))
.await?;
Ok(())
}
}
const INTERLEAVING_MARGIN: Duration = Duration::from_millis(40);
#[tokio::test]
async fn handler_holds_pending_wait_open_between_sends() {
let (server_addr, accept_loop) = start_server_with(HeldPendingHandler).await;
let mut stream = with_timeout("connect", TcpStream::connect(server_addr))
.await
.expect("connect to test server");
let (rx, tx) = stream.split();
let mut reader = FramedRead::new(rx, MessageCodec::new());
let mut writer = FramedWrite::new(tx, MessageCodec::new());
send_routing_activation(&mut writer, CLIENT_LOGICAL_ADDRESS).await;
let activation = read_message(&mut reader).await;
assert!(
matches!(
activation.payload,
OwnedPayload::RoutingActivationResponse(_)
),
"routing activation must succeed before any diagnostic message is sent, otherwise \
the failures below describe the wrong cause; got {:?}",
activation.payload
);
let started = std::time::Instant::now();
send_diagnostic_message(&mut writer, CLIENT_LOGICAL_ADDRESS, &[0x22, 0xFD, 0x69]).await;
let ack = read_message(&mut reader).await;
let ack_at = started.elapsed();
match ack.payload {
OwnedPayload::DiagnosticMessageAck(ref ack) => {
assert_eq!(ack.ack_code, DiagnosticAckCode::RoutingConfirmationAck);
}
ref other => panic!("expected DiagnosticMessageAck first, got {other:?}"),
}
let mut pending_at = Vec::new();
for index in 0..2 {
let pending = read_message(&mut reader).await;
pending_at.push(started.elapsed());
match pending.payload {
OwnedPayload::DiagnosticMessage(ref diag) => {
assert_eq!(
diag.user_data,
vec![0x7F, 0x22, 0x78],
"message {index} should be an NRC 0x78 pending"
);
}
other => panic!("expected pending DiagnosticMessage, got {other:?}"),
}
}
let final_response = read_message(&mut reader).await;
match final_response.payload {
OwnedPayload::DiagnosticMessage(ref diag) => {
assert_eq!(diag.user_data, vec![0x62, 0xFD, 0x69, 0xAA]);
}
other => panic!("expected final DiagnosticMessage, got {other:?}"),
}
assert!(
ack_at < INTERLEAVING_MARGIN,
"the ack arrived {ack_at:?} after the request; a streamed sink delivers it before \
the handler's first sleep, so anything near the handler's total runtime means \
responses are being batched and flushed at the end"
);
let pending_gap = pending_at[1].saturating_sub(pending_at[0]);
assert!(
pending_gap >= INTERLEAVING_MARGIN,
"the two pending responses arrived {pending_gap:?} apart (at {:?} and {:?}); the \
handler sleeps 50ms between them, so a smaller gap means they were flushed \
together rather than as the handler produced them",
pending_at[0],
pending_at[1]
);
assert!(
started.elapsed() >= Duration::from_millis(100),
"responses arrived in {:?}; expected >=100ms of held pending waits",
started.elapsed()
);
accept_loop.abort();
let _ = accept_loop.await;
}
async fn answer_one_request_then_hang_up(listener: TcpListener) {
let (mut stream, _) = listener.accept().await.expect("accept");
let (rx, tx) = stream.split();
let mut reader = FramedRead::new(rx, MessageCodec::new());
let mut writer = FramedWrite::new(tx, MessageCodec::new());
let activation = read_message(&mut reader).await;
let OwnedPayload::RoutingActivationRequest(ref request) = activation.payload else {
panic!("expected a routing activation request, got {activation:?}");
};
writer
.send(&OwnedMessage::routing_activation_response(
ProtocolVersion::V2012,
request.source_address,
SERVER_LOGICAL_ADDRESS,
RoutingActivationResponseCode::RoutingSuccessfullyActivated,
[0; 4],
None,
))
.await
.expect("send routing activation response");
let request = read_message(&mut reader).await;
let OwnedPayload::DiagnosticMessage(ref diagnostic) = request.payload else {
panic!("expected a diagnostic message, got {request:?}");
};
let tester = diagnostic.source_address;
writer
.send(&OwnedMessage::diagnostic_message_ack(
ProtocolVersion::V2012,
SERVER_LOGICAL_ADDRESS,
tester,
DiagnosticAckCode::RoutingConfirmationAck,
diagnostic.user_data.clone(),
))
.await
.expect("send diagnostic message ack");
writer
.send(&OwnedMessage::diagnostic_message(
ProtocolVersion::V2012,
SERVER_LOGICAL_ADDRESS,
tester,
vec![0x62, 0xFD, 0x69, 0xAA],
))
.await
.expect("send diagnostic response");
}
#[tokio::test]
async fn buffered_response_outlives_the_connection_that_carried_it() {
let listener = TcpListener::bind(("127.0.0.1", 0))
.await
.expect("failed to bind test entity to an ephemeral port");
let server_addr = listener
.local_addr()
.expect("bound listener has an address");
let entity = tokio::spawn(answer_one_request_then_hang_up(listener));
let mut client = with_timeout(
"client connect + routing activation",
Client::<TestConnector>::connect(client_options(server_addr)),
)
.await
.expect("client should connect and activate routing");
with_timeout(
"send_diagnostic_message",
client.send_diagnostic_message(AddressType::Physical, vec![0x22, 0xFD, 0x69]),
)
.await
.expect("the entity acks immediately, so the send must succeed");
with_timeout("entity finishes and closes", entity)
.await
.expect("entity task should not panic");
tokio::time::sleep(Duration::from_millis(50)).await;
let received = with_timeout(
"receive_diagnostic_response after the connection closed",
client.receive_diagnostic_response(Duration::from_millis(500)),
)
.await
.expect(
"the response was received before the connection closed, so it must be delivered \
rather than discarded in favour of a transport error",
);
let OwnedPayload::DiagnosticMessage(ref diagnostic) = received.payload else {
panic!("expected a diagnostic message, got {received:?}");
};
assert_eq!(
diagnostic.user_data,
vec![0x62, 0xFD, 0x69, 0xAA],
"the delivered response must be the one the entity sent"
);
}
#[derive(Clone, Copy, Debug)]
struct UnreachableConnector;
#[async_trait]
impl Connector for UnreachableConnector {
async fn establish_connection(
_gateway_address: SocketAddr,
) -> Result<(OwnedReadHalf, OwnedWriteHalf), Error> {
Err(Error::NetworkError(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"simulated: nothing is listening at this address",
)))
}
}
#[tokio::test]
async fn a_failed_connection_reports_the_connect_error_not_socket_not_bound() {
let unused_address: SocketAddr = "127.0.0.1:13400".parse().expect("valid address");
let error = Client::<UnreachableConnector>::connect(client_options(unused_address))
.await
.expect_err("connect must fail when the transport cannot be established");
assert!(
!matches!(error, Error::SocketNotBound),
"the connect failure must not be masked by the routing activation that \
follows it, got: {error:?}"
);
assert!(
matches!(error, Error::NetworkError(_)),
"expected the underlying connect error to survive, got: {error:?}"
);
}
struct ActivationOutcomeHandler {
accept: bool,
}
#[async_trait]
impl ServerConnectionHandler for ActivationOutcomeHandler {
fn get_vin(&self) -> [u8; 17] {
[0x00; 17]
}
fn get_logical_address(&self) -> LogicalAddress {
SERVER_LOGICAL_ADDRESS
}
fn get_entity_id(&self) -> [u8; 6] {
[0x00; 6]
}
fn get_group_id(&self) -> Option<[u8; 6]> {
None
}
async fn routing_activation(
&self,
request: &RoutingActivationRequest,
) -> Result<OwnedMessage, Error> {
let code = if self.accept {
RoutingActivationResponseCode::RoutingSuccessfullyActivated
} else {
RoutingActivationResponseCode::DeniedUnknownSourceAddress
};
Ok(OwnedMessage::routing_activation_response(
self.protocol_version(),
request.source_address,
self.get_logical_address(),
code,
[0; 4],
None,
))
}
async fn diagnostic_message(
&self,
_message: &DiagnosticMessage<'_>,
_responses: &mut dyn ResponseWriter,
) -> Result<(), Error> {
Ok(())
}
}
fn alive_check_source_address(message: &OwnedMessage) -> LogicalAddress {
match &message.payload {
OwnedPayload::AliveCheckResponse(response) => response.source_address,
other => panic!("expected an alive check response, got {other:?}"),
}
}
#[tokio::test]
async fn alive_check_reports_the_activated_tester_logical_address() {
let (server_addr, accept_loop) =
start_server_with(ActivationOutcomeHandler { accept: true }).await;
let mut stream = with_timeout("connect", TcpStream::connect(server_addr))
.await
.expect("connect to test server");
let (rx, tx) = stream.split();
let mut reader = FramedRead::new(rx, MessageCodec::new());
let mut writer = FramedWrite::new(tx, MessageCodec::new());
send_routing_activation(&mut writer, CLIENT_LOGICAL_ADDRESS).await;
let _activation = read_message(&mut reader).await;
with_timeout(
"send alive check",
writer.send(&OwnedMessage::alive_check_request(ProtocolVersion::V2012)),
)
.await
.expect("send alive check");
let response = read_message(&mut reader).await;
assert_eq!(
alive_check_source_address(&response),
CLIENT_LOGICAL_ADDRESS,
"the alive check must name the address the tester activated with"
);
accept_loop.abort();
}
#[tokio::test]
async fn a_denied_activation_does_not_record_the_testers_address() {
let (server_addr, accept_loop) =
start_server_with(ActivationOutcomeHandler { accept: false }).await;
let mut stream = with_timeout("connect", TcpStream::connect(server_addr))
.await
.expect("connect to test server");
let (rx, tx) = stream.split();
let mut reader = FramedRead::new(rx, MessageCodec::new());
let mut writer = FramedWrite::new(tx, MessageCodec::new());
send_routing_activation(&mut writer, CLIENT_LOGICAL_ADDRESS).await;
let _denial = read_message(&mut reader).await;
with_timeout(
"send alive check",
writer.send(&OwnedMessage::alive_check_request(ProtocolVersion::V2012)),
)
.await
.expect("send alive check");
let response = read_message(&mut reader).await;
assert_eq!(
alive_check_source_address(&response),
LogicalAddress(0x0000),
"a refused tester must not have its claimed address reported back"
);
accept_loop.abort();
}