Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion zlink-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ syn = { version = "2.0", default-features = false, features = [
] }

[dev-dependencies]
zlink = { path = "../zlink", default-features = false, features = ["tokio", "introspection"] }
zlink = { path = "../zlink", default-features = false, features = [
"tokio",
"introspection",
"idl-parse",
"service",
"proxy",
"tracing",
] }
serde = "1.0"
serde_json = "1.0"
serde-json-core = { version = "0.6.0", default-features = false, features = [
Expand All @@ -43,4 +50,9 @@ tokio = { version = "1.42.0", features = [
"rt",
"test-util",
"rt-multi-thread",
"fs",
] }
test-log = { version = "0.2.17", default-features = false, features = [
"trace",
"color",
] }
21 changes: 21 additions & 0 deletions zlink-macros/tests/service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#![cfg(feature = "service")]

// Tests for the service macro functionality.
// This file includes all service-related tests organized by feature.

#[path = "service/basic.rs"]
mod basic;
#[path = "service/custom_bounds.rs"]
mod custom_bounds;
#[path = "service/fd_passing.rs"]
mod fd_passing;
#[path = "service/introspection.rs"]
mod introspection;
#[path = "service/metadata.rs"]
mod metadata;
#[path = "service/multiple_interfaces.rs"]
mod multiple_interfaces;
#[path = "service/streaming.rs"]
mod streaming;
#[path = "service/streaming_fds.rs"]
mod streaming_fds;
178 changes: 178 additions & 0 deletions zlink-macros/tests/service/basic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//! Basic service macro tests using a BankAccount example.

use serde::{Deserialize, Serialize};
use zlink::{
introspect::{self, CustomType},
unix::{bind, connect},
Server,
};

#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn service_macro_basic() -> Result<(), Box<dyn std::error::Error>> {
// Remove the socket file if it exists (from a previous run of this test).
let socket_path = "/tmp/zlink-service-macro-test.sock";
if let Err(e) = tokio::fs::remove_file(socket_path).await {
if e.kind() != std::io::ErrorKind::NotFound {
return Err(e.into());
}
}

// Setup the server and run it in a separate task.
let listener = bind(socket_path).unwrap();
let service = BankAccount::new(1000, false);
let server = Server::new(listener, service);
tokio::select! {
res = server.run() => res?,
res = run_client(socket_path) => res?,
}

Ok(())
}

async fn run_client(socket_path: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut conn = connect(socket_path).await?;

// Test GetBalance method - returns plain value, no Result.
let reply = conn.get_balance().await?.unwrap();
assert_eq!(reply.amount, 1000);

// Test successful Deposit (returns Result<Balance, BankError>).
let reply = conn.deposit(500).await?.unwrap();
assert_eq!(reply.amount, 1500);

// Test GetBalance again to verify state was updated.
let reply = conn.get_balance().await?.unwrap();
assert_eq!(reply.amount, 1500);

// Test successful Withdraw.
let reply = conn.withdraw(200).await?.unwrap();
assert_eq!(reply.amount, 1300);

// Test error: withdraw more than available (InsufficientFunds).
let err = conn.withdraw(5000).await?.unwrap_err();
assert_eq!(
err,
BankError::InsufficientFunds {
available: 1300,
requested: 5000,
}
);

// Verify balance unchanged after failed withdrawal.
let reply = conn.get_balance().await?.unwrap();
assert_eq!(reply.amount, 1300);

// Test error: invalid amount (negative deposit).
let err = conn.deposit(-100).await?.unwrap_err();
assert_eq!(err, BankError::InvalidAmount { amount: -100 });

// Test LockAccount - returns no value (void method).
conn.lock_account().await?.unwrap();

// Test error: operations on locked account.
let err = conn.deposit(100).await?.unwrap_err();
assert_eq!(err, BankError::AccountLocked);

let err = conn.withdraw(100).await?.unwrap_err();
assert_eq!(err, BankError::AccountLocked);

// GetBalance should still work on locked account.
let reply = conn.get_balance().await?.unwrap();
assert_eq!(reply.amount, 1300);

Ok(())
}

// Response type for balance operations.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)]
pub(crate) struct Balance {
pub amount: i64,
}

// Error type with parameters - demonstrates error handling.
#[derive(Debug, Clone, PartialEq, zlink::ReplyError, introspect::ReplyError)]
#[zlink(interface = "org.example.bank")]
pub(crate) enum BankError {
InsufficientFunds { available: i64, requested: i64 },
InvalidAmount { amount: i64 },
AccountLocked,
}

// Define the service type.
pub(crate) struct BankAccount {
balance: i64,
locked: bool,
}

impl BankAccount {
pub fn new(initial_balance: i64, locked: bool) -> Self {
Self {
balance: initial_balance,
locked,
}
}
}

// Apply the service macro.
#[zlink::service(types = [Balance])]
impl BankAccount {
// Method that returns a plain value (not Result).
#[zlink(interface = "org.example.bank")]
async fn get_balance(&self) -> Balance {
Balance {
amount: self.balance,
}
}

// Method that can fail - returns Result<Balance, BankError>.
async fn deposit(&mut self, amount: i64) -> Result<Balance, BankError> {
if self.locked {
return Err(BankError::AccountLocked);
}
if amount <= 0 {
return Err(BankError::InvalidAmount { amount });
}
self.balance += amount;
Ok(Balance {
amount: self.balance,
})
}

// Another method that can fail.
async fn withdraw(&mut self, amount: i64) -> Result<Balance, BankError> {
if self.locked {
return Err(BankError::AccountLocked);
}
if amount <= 0 {
return Err(BankError::InvalidAmount { amount });
}
if amount > self.balance {
return Err(BankError::InsufficientFunds {
available: self.balance,
requested: amount,
});
}
self.balance -= amount;
Ok(Balance {
amount: self.balance,
})
}

// Method returning Result<(), BankError> (void success, can fail).
async fn lock_account(&mut self) -> Result<(), BankError> {
if self.locked {
return Err(BankError::AccountLocked);
}
self.locked = true;
Ok(())
}
}

// Define a proxy for the client side.
#[zlink::proxy("org.example.bank")]
trait BankProxy {
async fn get_balance(&mut self) -> zlink::Result<Result<Balance, BankError>>;
async fn deposit(&mut self, amount: i64) -> zlink::Result<Result<Balance, BankError>>;
async fn withdraw(&mut self, amount: i64) -> zlink::Result<Result<Balance, BankError>>;
async fn lock_account(&mut self) -> zlink::Result<Result<(), BankError>>;
}
86 changes: 86 additions & 0 deletions zlink-macros/tests/service/custom_bounds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//! Tests for custom socket bounds via user-provided generics.

use super::basic::Balance;
use zlink::{
connection::socket::FetchPeerCredentials,
introspect::{self},
unix::{bind, connect},
Server,
};

#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn with_custom_socket_bounds() -> Result<(), Box<dyn std::error::Error>> {
// Remove the socket file if it exists.
let socket_path = "/tmp/zlink-service-macro-creds-test.sock";
if let Err(e) = tokio::fs::remove_file(socket_path).await {
if e.kind() != std::io::ErrorKind::NotFound {
return Err(e.into());
}
}

// Setup the server with the credential-checking service.
let listener = bind(socket_path).unwrap();
let service = CredentialCheckingService { balance: 1000 };
let server = Server::new(listener, service);
tokio::select! {
res = server.run() => res?,
res = async {
let mut conn = connect(socket_path).await?;
// Test that the service works and can check credentials.
// The multiplier parameter is used AFTER an await point in the service method,
// which tests the fix for issue #216 (parameters with #[zlink(connection)]).
let reply = conn.get_balance_with_creds(2).await?.unwrap();
assert_eq!(reply.amount, 2000); // 1000 * 2
Ok::<(), Box<dyn std::error::Error>>(())
} => res?,
}

Ok(())
}

/// Error type for credential-checking service.
#[derive(Debug, Clone, PartialEq, zlink::ReplyError, introspect::ReplyError)]
#[zlink(interface = "org.example.creds")]
enum CredsError {
CredentialCheckFailed,
}

/// A service that uses custom socket bounds to check peer credentials.
struct CredentialCheckingService {
balance: i64,
}

// Service implementation with custom socket bounds using user-provided generics.
// The first type parameter (Sock) is used as the socket type. The Socket bound is added
// automatically, so we only specify additional bounds.
#[zlink::service]
impl<Sock> CredentialCheckingService
where
Sock::ReadHalf: FetchPeerCredentials,
{
#[zlink(interface = "org.example.creds")]
async fn get_balance_with_creds(
&self,
multiplier: i64,
#[zlink(connection)] conn: &mut zlink::Connection<Sock>,
) -> Result<Balance, CredsError> {
// Actually check credentials using the connection parameter.
let creds = conn.peer_credentials().await.unwrap();
// Verify we got valid credentials (check that unix_user_id is returned).
let _ = creds.unix_user_id();
// Use multiplier AFTER the await point - this tests the fix for issue #216.
// Without `async move`, the multiplier would be captured by reference and not live long
// enough.
Ok(Balance {
amount: self.balance * multiplier,
})
}
}

#[zlink::proxy("org.example.creds")]
trait CredsProxy {
async fn get_balance_with_creds(
&mut self,
multiplier: i64,
) -> zlink::Result<Result<Balance, CredsError>>;
}
Loading