rty 0.2.2

A simple command line retry tool.
use crate::prelude::*;

pub struct TokioChildProcess(tokio::process::Child);

impl super::AsyncReadUnpin for tokio::process::ChildStdout {}

impl super::AsyncReadUnpin for tokio::process::ChildStderr {}

impl Future for TokioChildProcess {
    type Output = Result<super::ExitCode>;

    fn poll(
        mut self: std::pin::Pin<&mut Self>,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Self::Output> {
        use futures::prelude::*;

        self.0
            .poll_unpin(cx)
            .map(|status| match status.map(|status| status.code()) {
                Ok(Some(code)) => Ok(code),
                _ => Err(anyhow!(
                    "failed to start child process or terminated abnormally"
                )),
            })
    }
}

impl super::ChildProcess for TokioChildProcess {
    fn stdout(&mut self) -> Result<Box<dyn super::AsyncReadUnpin>> {
        let boxed_stdout = self
            .0
            .stdout
            .take()
            .map(Box::new)
            .ok_or_else(|| anyhow!("failed to take stdout"))?;

        Ok(boxed_stdout)
    }

    fn stderr(&mut self) -> Result<Box<dyn super::AsyncReadUnpin>> {
        let boxed_stderr = self
            .0
            .stderr
            .take()
            .map(Box::new)
            .ok_or_else(|| anyhow!("failed to take stderr"))?;

        Ok(boxed_stderr)
    }
}

pub struct TokioCmdExecutor;

impl TokioCmdExecutor {
    fn parse(command: &str) -> (String, Vec<String>) {
        let mut elements = command.split(' ').map(Into::into).collect::<Vec<_>>();

        let options = elements.drain(1..).collect::<Vec<_>>();

        let command = match elements.get(0) {
            Some(_) => elements.remove(0),
            None => String::new(),
        };

        (command, options)
    }
}

#[async_trait]
impl super::SpawnChild for TokioCmdExecutor {
    async fn spawn(&self, command: &str) -> Result<Box<dyn super::ChildProcess + Send>> {
        let (command, options) = Self::parse(command);

        let mut cmd = tokio::process::Command::new(command);

        let child = cmd
            .args(options)
            .stdout(std::process::Stdio::piped())
            .stderr(std::process::Stdio::piped())
            .spawn()?
            .apply(TokioChildProcess)
            .apply(Box::new);

        Ok(child)
    }
}

#[cfg(test)]
pub struct StubChildProcess {
    stdout: std::io::Cursor<Vec<u8>>,
    stderr: std::io::Cursor<Vec<u8>>,
}

#[cfg(test)]
impl super::AsyncReadUnpin for std::io::Cursor<Vec<u8>> {}

#[cfg(test)]
impl Future for StubChildProcess {
    type Output = Result<super::ExitCode>;

    fn poll(
        self: std::pin::Pin<&mut Self>,
        _cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Self::Output> {
        std::task::Poll::Ready(Ok(0))
    }
}

#[cfg(test)]
impl super::ChildProcess for StubChildProcess {
    fn stdout(&mut self) -> Result<Box<dyn super::AsyncReadUnpin>> {
        Ok(Box::new(self.stdout.clone()))
    }

    fn stderr(&mut self) -> Result<Box<dyn super::AsyncReadUnpin>> {
        Ok(Box::new(self.stderr.clone()))
    }
}

#[cfg(test)]
pub struct StubCmdExecutor {
    pub child_stdout: Vec<u8>,
    pub child_stderr: Vec<u8>,
}

#[cfg(test)]
#[async_trait]
impl super::SpawnChild for StubCmdExecutor {
    async fn spawn(&self, _: &str) -> Result<Box<dyn super::ChildProcess + Send>> {
        Ok(Box::new(StubChildProcess {
            stdout: std::io::Cursor::new(self.child_stdout.clone()),
            stderr: std::io::Cursor::new(self.child_stderr.clone()),
        }))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse() {
        let (command, options) = TokioCmdExecutor::parse("ping 8.8.8.8");
        assert_eq!(command, "ping".to_string());
        assert_eq!(options, vec!["8.8.8.8".to_string()]);
    }
}