From 58b1d5513fdf92dfc245295428307815d5e09ac2 Mon Sep 17 00:00:00 2001 From: Laurent Pelecq Date: Tue, 15 Mar 2022 17:04:45 +0100 Subject: [PATCH] Both sync and async tests pass --- Cargo.toml | 4 - examples/async-mio.rs | 51 ------------ examples/hello.rs | 85 +++++++++++++++++++- examples/list.rs | 40 ++++++---- examples/notifications.rs | 11 ++- src/client.rs | 41 +++------- src/fifo.rs | 159 ++++++++++++++++++++++++++------------ src/lib.rs | 8 +- tests/fifo_async_tests.rs | 114 +++++++++++++++++++++++++++ tests/fifo_sync_tests.rs | 7 +- tests/server.rs | 10 +++ 11 files changed, 368 insertions(+), 162 deletions(-) delete mode 100644 examples/async-mio.rs create mode 100644 tests/fifo_async_tests.rs diff --git a/Cargo.toml b/Cargo.toml index 434a837..454d4be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,3 @@ libc = "0" [features] metal-io = ["mio/net", "mio/os-poll"] - -[[example]] -name = "async-mio" -required-features = ["metal-io"] diff --git a/examples/async-mio.rs b/examples/async-mio.rs deleted file mode 100644 index 7e2a238..0000000 --- a/examples/async-mio.rs +++ /dev/null @@ -1,51 +0,0 @@ -use mio::{Events, Poll, Token}; - -use ssip_client::{ClientName, ClientResult}; - -fn main() -> ClientResult<()> { - let mut poll = Poll::new()?; - let mut events = Events::with_capacity(128); - let mut client = ssip_client::new_default_fifo_client()?; - let token = Token(0); - client.register(&poll, token)?; - - poll.poll(&mut events, None)?; - let mut is_opened = false; - while !is_opened { - for event in &events { - if event.token() == token && event.is_writable() { - println!("opening client"); - match client.set_client_name(ClientName::new("joe", "hello")) { - Ok(()) => { - is_opened = true; - break; - } - Err(err) if err.kind() == io::ErrorKing::WouldBlock => {} - Err(err) => panic!("Error opening client: {:?}", err), - } - break; - } - } - } - - poll.poll(&mut events, None)?; - for event in &events { - if event.token() == token && event.is_writable() { - println!("sending message"); - let msg_id = client.say_line("hello")?; - println!("message: {}", msg_id); - - break; - } - } - - poll.poll(&mut events, None)?; - for event in &events { - if event.token() == token && event.is_writable() { - println!("quitting"); - client.quit()?; - break; - } - } - Ok(()) -} diff --git a/examples/hello.rs b/examples/hello.rs index 29c8dbe..b73ae8e 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -1,12 +1,91 @@ -use ssip_client::{new_default_fifo_client, ClientName, ClientResult}; +use ssip_client::{ClientName, ClientResult, FifoBuilder}; +// ============================== +// Synchronous implementation +// ============================== + +#[cfg(not(feature = "metal-io"))] fn main() -> ClientResult<()> { - let mut client = new_default_fifo_client(None)?; + let mut client = FifoBuilder::new().build()?; client .set_client_name(ClientName::new("joe", "hello"))? .check_client_name_set()?; - let msg_id = client.speak()?.send_line("hello")?.receive_message_id()?; + let msg_id = client + .speak()? + .check_receiving_data()? + .send_line("hello")? + .receive_message_id()?; println!("message: {}", msg_id); client.quit()?; Ok(()) } + +// ============================== +// Asynchronous implementation +// ============================== + +#[cfg(feature = "metal-io")] +use mio::{Events, Poll, Token}; + +#[cfg(feature = "metal-io")] +use ssip_client::ClientError; + +#[cfg(feature = "metal-io")] +fn increment(result: ClientResult) -> ClientResult { + match result { + Ok(_) => Ok(1), + Err(ClientError::NotReady) => Ok(0), + Err(err) => Err(err), + } +} + +#[cfg(feature = "metal-io")] +fn get_value(result: ClientResult) -> ClientResult> { + match result { + Ok(value) => Ok(Some(value)), + Err(ClientError::NotReady) => Ok(None), + Err(err) => Err(err), + } +} + +#[cfg(feature = "metal-io")] +fn main() -> ClientResult<()> { + let mut poll = Poll::new()?; + let mut events = Events::with_capacity(128); + let mut client = FifoBuilder::new().build()?; + let token = Token(0); + client.register(&poll, token)?; + let mut step: u16 = 0; + while step < 7 { + poll.poll(&mut events, None)?; + for event in &events { + if event.token() == token { + if event.is_writable() { + match step { + 0 => { + step += + increment(client.set_client_name(ClientName::new("test", "test")))? + } + 2 => step += increment(client.speak())?, + 4 => step += increment(client.send_line("hello"))?, + 6 => step += increment(client.quit())?, + _ => (), + } + } else if event.is_readable() { + match step { + 1 => step += increment(client.check_client_name_set())?, + 3 => step += increment(client.check_receiving_data())?, + 5 => { + if let Some(msgid) = get_value(client.receive_message_id())? { + println!("Message identifier: {}", msgid); + step += 1; + } + } + _ => (), + } + } + } + } + } + Ok(()) +} diff --git a/examples/list.rs b/examples/list.rs index e1dd921..a30c10b 100644 --- a/examples/list.rs +++ b/examples/list.rs @@ -1,26 +1,29 @@ +#[cfg(not(feature = "metal-io"))] use ssip_client::{ - ClientName, ClientResult, SynthesisVoice, OK_OUTPUT_MODULES_LIST_SENT, OK_VOICES_LIST_SENT, + ClientName, ClientResult, FifoBuilder, SynthesisVoice, OK_OUTPUT_MODULES_LIST_SENT, + OK_VOICES_LIST_SENT, }; -fn voice_to_string(voice: &SynthesisVoice) -> String { - match &voice.language { - Some(language) => match &voice.dialect { - Some(dialect) => format!("{} [{}_{}]", voice.name, language, dialect), - None => format!("{} [{}]", voice.name, language), - }, - None => format!("{}", voice.name), +#[cfg(not(feature = "metal-io"))] +fn main() -> ClientResult<()> { + fn voice_to_string(voice: &SynthesisVoice) -> String { + match &voice.language { + Some(language) => match &voice.dialect { + Some(dialect) => format!("{} [{}_{}]", voice.name, language, dialect), + None => format!("{} [{}]", voice.name, language), + }, + None => format!("{}", voice.name), + } } -} -fn print_list(title: &str, values: &[String]) { - println!("{}:", title); - for val in values { - println!("- {}", val); + fn print_list(title: &str, values: &[String]) { + println!("{}:", title); + for val in values { + println!("- {}", val); + } } -} -fn main() -> ClientResult<()> { - let mut client = ssip_client::new_default_fifo_client(None)?; + let mut client = FifoBuilder::new().build()?; client .set_client_name(ClientName::new("joe", "list"))? .check_client_name_set()?; @@ -50,3 +53,8 @@ fn main() -> ClientResult<()> { client.quit().unwrap(); Ok(()) } + +#[cfg(feature = "metal-io")] +fn main() { + println!("asynchronous client not implemented"); +} diff --git a/examples/notifications.rs b/examples/notifications.rs index 09cf9c2..9c9e5f4 100644 --- a/examples/notifications.rs +++ b/examples/notifications.rs @@ -1,7 +1,9 @@ -use ssip_client::{ClientName, ClientResult, EventType, NotificationType}; +#[cfg(not(feature = "metal-io"))] +use ssip_client::{ClientName, ClientResult, EventType, FifoBuilder, NotificationType}; +#[cfg(not(feature = "metal-io"))] fn main() -> ClientResult<()> { - let mut client = ssip_client::new_default_fifo_client(None)?; + let mut client = FifoBuilder::new().build()?; client .set_client_name(ClientName::new("joe", "notifications"))? .check_client_name_set()?; @@ -28,3 +30,8 @@ fn main() -> ClientResult<()> { client.quit()?; Ok(()) } + +#[cfg(feature = "metal-io")] +fn main() { + println!("asynchronous client not implemented"); +} diff --git a/src/client.rs b/src/client.rs index ed131bc..93d1621 100644 --- a/src/client.rs +++ b/src/client.rs @@ -162,37 +162,16 @@ macro_rules! client_send_range { } /// SSIP client on generic stream -#[cfg(not(feature = "metal-io"))] -pub struct Client { - input: io::BufReader, - output: io::BufWriter, -} - -#[cfg(feature = "metal-io")] pub struct Client { input: io::BufReader, output: io::BufWriter, - socket: S, } impl Client { - #[cfg(not(feature = "metal-io"))] - pub(crate) fn new(input: io::BufReader, output: io::BufWriter) -> ClientResult { + /// Create a SSIP client on the reader and writer. + pub(crate) fn new(input: io::BufReader, output: io::BufWriter) -> Self { // https://stackoverflow.com/questions/58467659/how-to-store-tcpstream-with-bufreader-and-bufwriter-in-a-data-structure - Ok(Self { input, output }) - } - - #[cfg(feature = "metal-io")] - pub(crate) fn new( - input: io::BufReader, - output: io::BufWriter, - socket: S, - ) -> ClientResult { - Ok(Self { - socket, - input, - output, - }) + Self { input, output } } /// Return the only string in the list or an error if there is no line or too many. @@ -548,14 +527,18 @@ impl Client { self.check_status(OK_CLIENT_NAME_SET) } + /// Check if server accept data. + pub fn check_receiving_data(&mut self) -> ClientResult<&mut Client> { + self.check_status(OK_RECEIVING_DATA) + } + /// Register the socket for polling. #[cfg(feature = "metal-io")] pub fn register(&mut self, poll: &mio::Poll, token: mio::Token) -> ClientResult<()> { - poll.registry().register( - &mut self.socket, - token, - mio::Interest::READABLE | mio::Interest::WRITABLE, - )?; + poll.registry() + .register(self.output.get_mut(), token, mio::Interest::WRITABLE)?; + poll.registry() + .register(self.input.get_mut(), token, mio::Interest::READABLE)?; Ok(()) } } diff --git a/src/fifo.rs b/src/fifo.rs index 6c4ec74..51a02f3 100644 --- a/src/fifo.rs +++ b/src/fifo.rs @@ -8,99 +8,156 @@ // modified, or distributed except according to those terms. use std::io; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; const SPEECHD_APPLICATION_NAME: &str = "speech-dispatcher"; const SPEECHD_SOCKET_NAME: &str = "speechd.sock"; -/// Return the standard socket according to the [freedesktop.org](https://www.freedesktop.org/) specification. -fn speech_dispatcher_socket() -> io::Result { - match dirs::runtime_dir() { - Some(runtime_dir) => Ok(runtime_dir - .join(SPEECHD_APPLICATION_NAME) - .join(SPEECHD_SOCKET_NAME)), - None => Err(io::Error::new( - io::ErrorKind::NotFound, - "unix socket not found", - )), +struct FifoPath { + path: Option, +} + +impl FifoPath { + fn new() -> FifoPath { + FifoPath { path: None } + } + + fn set

(&mut self, path: P) + where + P: AsRef, + { + self.path = Some(path.as_ref().to_path_buf()); + } + + /// Return the standard socket according to the [freedesktop.org](https://www.freedesktop.org/) specification. + fn default_path() -> io::Result { + match dirs::runtime_dir() { + Some(runtime_dir) => Ok(runtime_dir + .join(SPEECHD_APPLICATION_NAME) + .join(SPEECHD_SOCKET_NAME)), + None => Err(io::Error::new( + io::ErrorKind::NotFound, + "unix socket not found", + )), + } + } + + fn get(&self) -> io::Result { + match &self.path { + Some(path) => Ok(path.to_path_buf()), + _ => FifoPath::default_path(), + } } } #[cfg(not(feature = "metal-io"))] mod synchronous { - use std::io::{BufReader, BufWriter}; + use std::io::{self, BufReader, BufWriter}; pub use std::os::unix::net::UnixStream; use std::path::Path; use std::time::Duration; - use crate::client::{Client, ClientResult}; + use crate::client::Client; + + use super::FifoPath; - /// New FIFO client - pub fn new_fifo_client

( - socket_path: P, + pub struct FifoBuilder { + path: FifoPath, read_timeout: Option, - ) -> ClientResult> - where - P: AsRef, - { - let stream = UnixStream::connect(socket_path.as_ref())?; - stream.set_read_timeout(read_timeout)?; - Client::new(BufReader::new(stream.try_clone()?), BufWriter::new(stream)) } - /// New FIFO client on the standard socket `${XDG_RUNTIME_DIR}/speech-dispatcher/speechd.sock` - pub fn new_default_fifo_client( - read_timeout: Option, - ) -> ClientResult> { - let socket_path = super::speech_dispatcher_socket()?; - new_fifo_client(socket_path.as_path(), read_timeout) + impl FifoBuilder { + pub fn new() -> FifoBuilder { + FifoBuilder { + path: FifoPath::new(), + read_timeout: None, + } + } + + pub fn path

(&mut self, socket_path: P) -> &mut FifoBuilder + where + P: AsRef, + { + self.path.set(socket_path); + self + } + + pub fn timeout(&mut self, read_timeout: Duration) -> &mut FifoBuilder { + self.read_timeout = Some(read_timeout); + self + } + + pub fn build(&self) -> io::Result> { + let input = UnixStream::connect(self.path.get()?)?; + input.set_read_timeout(self.read_timeout)?; + let output = input.try_clone()?; + Ok(Client::new(BufReader::new(input), BufWriter::new(output))) + } } } #[cfg(not(feature = "metal-io"))] -pub use synchronous::{new_default_fifo_client, new_fifo_client, UnixStream}; +pub use synchronous::{FifoBuilder, UnixStream}; #[cfg(feature = "metal-io")] mod asynchronous { pub use mio::net::UnixStream; - use std::io::{BufReader, BufWriter}; + use std::io::{self, BufReader, BufWriter}; use std::os::unix::net::UnixStream as StdUnixStream; use std::path::Path; - use crate::client::{Client, ClientResult}; + use crate::client::Client; - /// New FIFO client - pub fn new_fifo_client

(socket_path: P) -> ClientResult> - where - P: AsRef, - { - let stream = StdUnixStream::connect(socket_path.as_ref())?; - stream.set_nonblocking(true)?; - Client::new( - BufReader::new(UnixStream::from_std(stream.try_clone()?)), - BufWriter::new(UnixStream::from_std(stream.try_clone()?)), - UnixStream::from_std(stream), - ) + use super::FifoPath; + + pub struct FifoBuilder { + path: FifoPath, } - /// New FIFO client on the standard socket `${XDG_RUNTIME_DIR}/speech-dispatcher/speechd.sock` - pub fn new_default_fifo_client() -> ClientResult> { - let socket_path = super::speech_dispatcher_socket()?; - new_fifo_client(socket_path.as_path()) + impl FifoBuilder { + pub fn new() -> FifoBuilder { + FifoBuilder { + path: FifoPath::new(), + } + } + + fn non_blocking(socket: StdUnixStream) -> io::Result { + socket.set_nonblocking(true)?; + Ok(socket) + } + + pub fn path

(&mut self, socket_path: P) -> &mut FifoBuilder + where + P: AsRef, + { + self.path.set(socket_path); + self + } + + pub fn build(&self) -> io::Result> { + let stream = StdUnixStream::connect(self.path.get()?)?; + Ok(Client::new( + BufReader::new(UnixStream::from_std(FifoBuilder::non_blocking( + stream.try_clone()?, + )?)), + BufWriter::new(UnixStream::from_std(FifoBuilder::non_blocking(stream)?)), + )) + } } } #[cfg(feature = "metal-io")] -pub use asynchronous::{new_default_fifo_client, new_fifo_client, UnixStream}; +pub use asynchronous::{FifoBuilder, UnixStream}; #[cfg(test)] mod tests { #[test] - fn test_speech_dispatcher_socket() -> std::io::Result<()> { + fn test_fifo_path() -> std::io::Result<()> { if std::env::var("XDG_RUNTIME_DIR").is_ok() { - let socket_path = super::speech_dispatcher_socket()?; + let socket_path = super::FifoPath::new(); assert!(socket_path + .get()? .to_str() .unwrap() .ends_with("/speech-dispatcher/speechd.sock")); diff --git a/src/lib.rs b/src/lib.rs index c7420a2..214fb09 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ // ssip-client -- Speech Dispatcher client in Rust -// Copyright (c) 2021 Laurent Pelecq +// Copyright (c) 2021-2022 Laurent Pelecq // // Licensed under the Apache License, Version 2.0 // or the MIT @@ -16,8 +16,8 @@ //! //! Example //! ```no_run -//! use ssip_client::{new_default_fifo_client, ClientName, OK_CLIENT_NAME_SET}; -//! let mut client = new_default_fifo_client(None)?; +//! use ssip_client::{FifoBuilder, ClientName, OK_CLIENT_NAME_SET}; +//! let mut client = FifoBuilder::new().build()?; //! client //! .set_client_name(ClientName::new("joe", "hello"))? //! .check_client_name_set()?; @@ -36,6 +36,6 @@ mod types; pub use client::{Client, ClientError, ClientName, ClientResult, ClientStatus}; pub use constants::*; -pub use fifo::{new_default_fifo_client, new_fifo_client}; +pub use fifo::FifoBuilder; pub use types::StatusLine; pub use types::*; diff --git a/tests/fifo_async_tests.rs b/tests/fifo_async_tests.rs new file mode 100644 index 0000000..c981a70 --- /dev/null +++ b/tests/fifo_async_tests.rs @@ -0,0 +1,114 @@ +// Copyright (c) 2022 Laurent Pelecq +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + +#[cfg(feature = "metal-io")] +use mio::{Events, Poll, Token}; +#[cfg(feature = "metal-io")] +use ssip_client::*; + +#[cfg(feature = "metal-io")] +mod server; + +#[cfg(feature = "metal-io")] +use server::Server; + +#[cfg(feature = "metal-io")] +mod utils { + + use ssip_client::*; + + const MAX_RETRIES: u16 = 10; + + pub struct Controler { + step: u16, + retry: u16, + } + + impl Controler { + pub fn new() -> Controler { + Controler { + step: 0, + retry: MAX_RETRIES, + } + } + + pub fn step(&self) -> u16 { + self.step + } + + pub fn check_result(&mut self, result: ClientResult) -> Option { + match result { + Ok(value) => { + self.step += 1; + self.retry = MAX_RETRIES; + Some(value) + } + Err(ClientError::NotReady) if self.retry > 0 => { + self.retry -= 1; + None + } + Err(err) => panic!("{:?}", err), + } + } + } +} + +#[cfg(feature = "metal-io")] +use utils::Controler; + +#[test] +#[cfg(feature = "metal-io")] +fn basic_async_communication() -> std::io::Result<()> { + const COMMUNICATION: [(&str, &str); 1] = [( + "SET self CLIENT_NAME test:test:main\r\n", + "208 OK CLIENT NAME SET\r\n", + )]; + + let socket_path = Server::temporary_path(); + assert!(!socket_path.exists()); + let server_path = socket_path.clone(); + let result = std::panic::catch_unwind(move || -> std::io::Result { + let handle = Server::run(&server_path, &COMMUNICATION); + let mut poll = Poll::new()?; + let mut events = Events::with_capacity(128); + let mut client = FifoBuilder::new().path(&server_path).build().unwrap(); + let token = Token(0); + client.register(&poll, token).unwrap(); + let mut controler = Controler::new(); + use std::io::Write; + let mut log_file = std::fs::File::create("/home/laurent/tmp/test_client.log")?; + while controler.step() < 2 { + poll.poll(&mut events, None)?; + log_file.write_all(format!("Step: {}\n", controler.step()).as_bytes())?; + for event in &events { + if event.token() == token { + if event.is_writable() { + log_file.write_all(b"Event writable\n")?; + if controler.step() == 0 { + log_file.write_all(b"Send: set client name\n")?; + controler.check_result( + client.set_client_name(ClientName::new("test", "test")), + ); + } + } else if event.is_readable() { + log_file.write_all(b"Event readable\n")?; + if controler.step() == 1 { + log_file.write_all(b"Receive: client name set\n")?; + controler.check_result(client.check_client_name_set()); + } + } + } + } + } + handle.join().unwrap().unwrap(); + Ok(controler.step()) + }); + std::fs::remove_file(socket_path)?; + assert_eq!(2, result.unwrap().unwrap()); + Ok(()) +} diff --git a/tests/fifo_sync_tests.rs b/tests/fifo_sync_tests.rs index 1bb764b..95f5050 100644 --- a/tests/fifo_sync_tests.rs +++ b/tests/fifo_sync_tests.rs @@ -34,7 +34,10 @@ where let mut process_wrapper = std::panic::AssertUnwindSafe(process); let result = std::panic::catch_unwind(move || { let handle = Server::run(&server_path, communication); - let mut client = ssip_client::new_fifo_client(&server_path, None).unwrap(); + let mut client = ssip_client::FifoBuilder::new() + .path(&server_path) + .build() + .unwrap(); client .set_client_name(ClientName::new("test", "test")) .unwrap() @@ -377,7 +380,7 @@ fn receive_notification() -> io::Result<()> { client .speak() .unwrap() - .check_status(OK_RECEIVING_DATA) + .check_receiving_data() .unwrap() .send_line("Hello, world") .unwrap() diff --git a/tests/server.rs b/tests/server.rs index 788175c..b22e310 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -49,10 +49,16 @@ impl Server { let (stream, _) = self.listener.accept()?; let mut input = BufReader::new(stream.try_clone()?); let mut output = BufWriter::new(stream); + let mut log_file = std::fs::File::create("/home/laurent/tmp/test_server.log")?; for (questions, answer) in self.communication.iter() { + log_file.write_all(format!("Next answer: {}", answer).as_bytes())?; for question in Server::split_lines(questions).iter() { + log_file.write_all(format!("Expecting: {}", question).as_bytes())?; + log_file.flush()?; let mut line = String::new(); input.read_line(&mut line)?; + log_file.write_all(format!("Read: {}", line).as_bytes())?; + log_file.flush()?; if line != *question { return Err(io::Error::new( io::ErrorKind::InvalidInput, @@ -60,8 +66,12 @@ impl Server { )); } } + log_file.write_all(format!("Write: {}", answer).as_bytes())?; + log_file.flush()?; output.write_all(answer.as_bytes())?; output.flush()?; + log_file.write_all(b"Flushed\r\n")?; + log_file.flush()?; } Ok(()) }