Both sync and async tests pass

main
Laurent Pelecq 2 years ago
parent 28c3533628
commit 58b1d5513f

@ -21,7 +21,3 @@ libc = "0"
[features]
metal-io = ["mio/net", "mio/os-poll"]
[[example]]
name = "async-mio"
required-features = ["metal-io"]

@ -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(())
}

@ -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<V>(result: ClientResult<V>) -> ClientResult<u16> {
match result {
Ok(_) => Ok(1),
Err(ClientError::NotReady) => Ok(0),
Err(err) => Err(err),
}
}
#[cfg(feature = "metal-io")]
fn get_value<V>(result: ClientResult<V>) -> ClientResult<Option<V>> {
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(())
}

@ -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");
}

@ -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");
}

@ -162,37 +162,16 @@ macro_rules! client_send_range {
}
/// SSIP client on generic stream
#[cfg(not(feature = "metal-io"))]
pub struct Client<S: Read + Write> {
input: io::BufReader<S>,
output: io::BufWriter<S>,
}
#[cfg(feature = "metal-io")]
pub struct Client<S: Read + Write + Source> {
input: io::BufReader<S>,
output: io::BufWriter<S>,
socket: S,
}
impl<S: Read + Write + Source> Client<S> {
#[cfg(not(feature = "metal-io"))]
pub(crate) fn new(input: io::BufReader<S>, output: io::BufWriter<S>) -> ClientResult<Self> {
/// Create a SSIP client on the reader and writer.
pub(crate) fn new(input: io::BufReader<S>, output: io::BufWriter<S>) -> 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<S>,
output: io::BufWriter<S>,
socket: S,
) -> ClientResult<Self> {
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<S: Read + Write + Source> Client<S> {
self.check_status(OK_CLIENT_NAME_SET)
}
/// Check if server accept data.
pub fn check_receiving_data(&mut self) -> ClientResult<&mut Client<S>> {
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(())
}
}

@ -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<PathBuf> {
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<PathBuf>,
}
impl FifoPath {
fn new() -> FifoPath {
FifoPath { path: None }
}
fn set<P>(&mut self, path: P)
where
P: AsRef<Path>,
{
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<PathBuf> {
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<PathBuf> {
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<P>(
socket_path: P,
pub struct FifoBuilder {
path: FifoPath,
read_timeout: Option<Duration>,
) -> ClientResult<Client<UnixStream>>
where
P: AsRef<Path>,
{
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<Duration>,
) -> ClientResult<Client<UnixStream>> {
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<P>(&mut self, socket_path: P) -> &mut FifoBuilder
where
P: AsRef<Path>,
{
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<Client<UnixStream>> {
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<P>(socket_path: P) -> ClientResult<Client<UnixStream>>
where
P: AsRef<Path>,
{
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<Client<UnixStream>> {
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<StdUnixStream> {
socket.set_nonblocking(true)?;
Ok(socket)
}
pub fn path<P>(&mut self, socket_path: P) -> &mut FifoBuilder
where
P: AsRef<Path>,
{
self.path.set(socket_path);
self
}
pub fn build(&self) -> io::Result<Client<UnixStream>> {
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"));

@ -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
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-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::*;

@ -0,0 +1,114 @@
// Copyright (c) 2022 Laurent Pelecq
//
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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<V>(&mut self, result: ClientResult<V>) -> Option<V> {
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<u16> {
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(())
}

@ -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()

@ -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(())
}

Loading…
Cancel
Save