Repository: zhiburt/expectrl (language: rust)

Issue:
Add `log` support for `repl`s
#24 removes this support its necessary to figure out the best way how to return this support back.

So code like this could be possible.

```rust
    let mut bash = expectrl::repl::spawn_bash().unwrap().with_log(Stdin).unwrap();
```

#### Test

```rust
#[test]
fn log_bash() {
    let writer = StubWriter::default();
    let mut bash = expectrl::repl::spawn_bash().unwrap().with_log(writer.clone()).unwrap();

    bash.send_line("echo Hello World").unwrap();

    let mut buf = String::new();
    let _ = bash.read_line(&mut buf).unwrap();

    let bytes = writer.inner.lock().unwrap();
    let s = String::from_utf8_lossy(bytes.get_ref());
    assert!(s.contains("write \"echo Hello World\""));
    // We use contains and not direct comparision because the actuall output depends on the shell.
    assert!(s.contains("read"));
}
```


Discussion / hints:

Fix patch:
diff --git a/examples/powershell.rs b/examples/powershell.rs
index 454ea19..993a6c8 100644
--- a/examples/powershell.rs
+++ b/examples/powershell.rs
@@ -27,9 +27,9 @@ fn main() {
             p.expect_prompt().await.unwrap();
             println!(
                 "/etc/passwd has {} lines, {} words, {} chars",
-                String::from_utf8_lossy(lines.get(0).unwrap()),
-                String::from_utf8_lossy(words.get(0).unwrap()),
-                String::from_utf8_lossy(bytes.matches()[1]),
+                String::from_utf8_lossy(&lines[0]),
+                String::from_utf8_lossy(&words[0]),
+                String::from_utf8_lossy(&bytes[1]),
             );
 
             // case 3: read while program is still executing
@@ -67,9 +67,9 @@ fn main() {
         p.expect_prompt().unwrap();
         println!(
             "/etc/passwd has {} lines, {} words, {} chars",
-            String::from_utf8_lossy(lines.get(0).unwrap()),
-            String::from_utf8_lossy(words.get(0).unwrap()),
-            String::from_utf8_lossy(bytes.matches()[1]),
+            String::from_utf8_lossy(&lines[0]),
+            String::from_utf8_lossy(&words[0]),
+            String::from_utf8_lossy(&bytes[1]),
         );
 
         // case 3: read while program is still executing
diff --git a/src/needle.rs b/src/needle.rs
index 7d4f697..5b431c2 100644
--- a/src/needle.rs
+++ b/src/needle.rs
@@ -49,8 +49,7 @@ impl<Re: AsRef<str>> Needle for Regex<Re> {
         let regex = regex::bytes::Regex::new(self.0.as_ref()).map_err(|_| Error::RegexParsing)?;
         let matches = regex
             .captures_iter(buf)
-            .map(|c| c.iter().flatten().map(|m| m.into()).collect::<Vec<Match>>())
-            .flatten()
+            .flat_map(|c| c.iter().flatten().map(|m| m.into()).collect::<Vec<Match>>())
             .collect();
         Ok(matches)
     }
diff --git a/src/repl.rs b/src/repl.rs
index 83697c1..01c5666 100644
--- a/src/repl.rs
+++ b/src/repl.rs
@@ -1,13 +1,23 @@
 //! This module contains a list of special Sessions that can be spawned.
 
-use crate::{error::Error, Captures, Session};
-use std::ops::{Deref, DerefMut};
+use crate::{
+    error::Error,
+    process::NonBlocking,
+    session::{Proc, Stream},
+    Captures, Session,
+};
+use std::{
+    io::{Read, Write},
+    ops::{Deref, DerefMut},
+};
 
 #[cfg(unix)]
 use std::process::Command;
 
-#[cfg(windows)]
-use conpty::ProcAttr;
+use crate::spawn;
+
+#[cfg(feature = "async")]
+use futures_lite::{AsyncRead, AsyncWrite};
 
 /// Spawn a bash session.
 ///
@@ -29,7 +39,15 @@ pub fn spawn_bash() -> Result<ReplSession, Error> {
         "PROMPT_COMMAND",
         "PS1=EXPECT_PROMPT; unset PROMPT_COMMAND; bind 'set enable-bracketed-paste off'",
     );
-    let mut bash = ReplSession::spawn(cmd, DEFAULT_PROMPT, Some("quit"))?;
+
+    let session = crate::session::Session::spawn(cmd)?;
+
+    let mut bash = ReplSession::new(
+        session,
+        DEFAULT_PROMPT.to_string(),
+        Some("quit".to_string()),
+        false,
+    );
 
     // read a prompt to make it not available on next read.
     //
@@ -59,7 +77,15 @@ pub async fn spawn_bash() -> Result<ReplSession, Error> {
         "PROMPT_COMMAND",
         "PS1=EXPECT_PROMPT; unset PROMPT_COMMAND; bind 'set enable-bracketed-paste off'",
     );
-    let mut bash = ReplSession::spawn(cmd, DEFAULT_PROMPT, Some("quit"))?;
+
+    let session = crate::session::Session::spawn(cmd)?;
+
+    let mut bash = ReplSession::new(
+        session,
+        DEFAULT_PROMPT.to_string(),
+        Some("quit".to_string()),
+        false,
+    );
 
     // read a prompt to make it not available on next read.
     bash.expect_prompt().await?;
@@ -70,39 +96,27 @@ pub async fn spawn_bash() -> Result<ReplSession, Error> {
 /// Spawn default python's IDLE.
 #[cfg(not(feature = "async"))]
 pub fn spawn_python() -> Result<ReplSession, Error> {
-    #[cfg(unix)]
-    {
-        let mut idle = ReplSession::spawn(Command::new("python"), ">>> ", Some("quit()"))?;
-        idle._expect_prompt()?;
-        Ok(idle)
-    }
-    #[cfg(windows)]
-    {
-        // If we spawn it as ProcAttr::default().commandline("python") it will spawn processes endlessly....
-        let mut idle =
-            ReplSession::spawn(ProcAttr::cmd("python".to_string()), ">>> ", Some("quit()"))?;
-        idle._expect_prompt()?;
-        Ok(idle)
-    }
+    // todo: check windows here
+    // If we spawn it as ProcAttr::default().commandline("python") it will spawn processes endlessly....
+
+    let session = spawn("python")?;
+
+    let mut idle = ReplSession::new(session, ">>> ".to_owned(), Some("quit()".to_owned()), false);
+    idle.expect_prompt()?;
+    Ok(idle)
 }
 
 /// Spawn default python's IDLE.
 #[cfg(feature = "async")]
 pub async fn spawn_python() -> Result<ReplSession, Error> {
-    #[cfg(unix)]
-    {
-        let mut idle = ReplSession::spawn(Command::new("python"), ">>> ", Some("quit()"))?;
-        idle._expect_prompt().await?;
-        Ok(idle)
-    }
-    #[cfg(windows)]
-    {
-        // If we spawn it as ProcAttr::default().commandline("python") it will spawn processes endlessly....
-        let mut idle =
-            ReplSession::spawn(ProcAttr::cmd("python".to_string()), ">>> ", Some("quit()"))?;
-        idle._expect_prompt().await?;
-        Ok(idle)
-    }
+    // todo: check windows here
+    // If we spawn it as ProcAttr::default().commandline("python") it will spawn processes endlessly....
+
+    let session = spawn("python")?;
+
+    let mut idle = ReplSession::new(session, ">>> ".to_owned(), Some("quit()".to_owned()), false);
+    idle.expect_prompt().await?;
+    Ok(idle)
 }
 
 /// Spawn a powershell session.
@@ -112,12 +126,13 @@ pub async fn spawn_python() -> Result<ReplSession, Error> {
 #[cfg(not(feature = "async"))]
 pub fn spawn_powershell() -> Result<ReplSession, Error> {
     const DEFAULT_PROMPT: &str = "EXPECTED_PROMPT>";
-    let mut powershell = ReplSession::spawn(
-        ProcAttr::default().commandline(r"pwsh -NoProfile -NonInteractive -NoLogo".to_string()),
-        DEFAULT_PROMPT,
-        Some("exit"),
-    )?;
-    powershell.is_echo_on = true;
+    let session = spawn("pwsh -NoProfile -NonInteractive -NoLogo")?;
+    let mut powershell = ReplSession::new(
+        session,
+        DEFAULT_PROMPT.to_owned(),
+        Some("exit".to_owned()),
+        true,
+    );
 
     // https://stackoverflow.com/questions/5725888/windows-powershell-changing-the-command-prompt
     powershell.execute(format!(
@@ -140,12 +155,13 @@ pub fn spawn_powershell() -> Result<ReplSession, Error> {
 #[cfg(feature = "async")]
 pub async fn spawn_powershell() -> Result<ReplSession, Error> {
     const DEFAULT_PROMPT: &str = "EXPECTED_PROMPT>";
-    let mut powershell = ReplSession::spawn(
-        ProcAttr::default().commandline(r"pwsh -NoProfile -NonInteractive -NoLogo".to_string()),
-        DEFAULT_PROMPT,
-        Some("exit"),
-    )?;
-    powershell.is_echo_on = true;
+    let session = spawn("pwsh -NoProfile -NonInteractive -NoLogo")?;
+    let mut powershell = ReplSession::new(
+        session,
+        DEFAULT_PROMPT.to_owned(),
+        Some("exit".to_owned()),
+        true,
+    );
 
     // https://stackoverflow.com/questions/5725888/windows-powershell-changing-the-command-prompt
     powershell
@@ -170,58 +186,48 @@ pub async fn spawn_powershell() -> Result<ReplSession, Error> {
 /// A repl session: e.g. bash or the python shell:
 /// you have a prompt where a user inputs commands and the shell
 /// which executes them and manages IO streams.
-pub struct ReplSession {
+pub struct ReplSession<P = Proc, S = Stream> {
     /// The prompt, used for `wait_for_prompt`,
     /// e.g. ">>> " for python.
     prompt: String,
     /// A pseudo-teletype session with a spawned process.
-    session: Session,
+    session: Session<P, S>,
     /// A command which will be called before termination.
     quit_command: Option<String>,
     /// Flag to see if a echo is turned on.
     is_echo_on: bool,
 }
 
-impl ReplSession {
+impl<P, S> ReplSession<P, S> {
     /// Spawn function spawns a repl session.
-    #[cfg(unix)]
-    pub fn spawn<PP: AsRef<str>, Q: AsRef<str>>(
-        cmd: Command,
-        prompt: PP,
-        quit_command: Option<Q>,
-    ) -> Result<Self, Error> {
-        let session = Session::spawn(cmd)?;
-        let is_echo_on = session
-            .get_echo()
-            .map_err(|e| Error::unknown("failed to get echo", e))?;
-        let prompt = prompt.as_ref().to_owned();
-        let quit_command = quit_command.map(|q| q.as_ref().to_owned());
-
-        Ok(Self {
-            prompt,
+    fn new(
+        session: Session<P, S>,
+        prompt: String,
+        quit_command: Option<String>,
+        is_echo_on: bool,
+    ) -> Self {
+        Self {
             session,
+            prompt,
             quit_command,
             is_echo_on,
-        })
+        }
     }
 
-    /// Spawn function spawns a repl session.
-    #[cfg(windows)]
-    pub fn spawn<P: AsRef<str>, Q: AsRef<str>>(
-        attr: crate::ProcAttr,
-        prompt: P,
-        quit_command: Option<Q>,
-    ) -> Result<Self, Error> {
-        let session = Session::spawn(attr)?;
-        let prompt = prompt.as_ref().to_owned();
-        let quit_command = quit_command.map(|q| q.as_ref().to_owned());
-
-        Ok(Self {
-            prompt,
+    /// Update an underlying session.
+    ///
+    /// Can be used to set a logger for example.
+    pub fn upgrade_session<NP, NS, F>(self, build_session: F) -> Result<ReplSession<NP, NS>, Error>
+    where
+        F: FnOnce(Session<P, S>) -> Result<Session<NP, NS>, Error>,
+    {
+        let session = build_session(self.session)?;
+        Ok(ReplSession::new(
             session,
-            quit_command,
-            is_echo_on: false,
-        })
+            self.prompt,
+            self.quit_command,
+            self.is_echo_on,
+        ))
     }
 
     /// Get a size in bytes of a prompt, may be usefull for triming it.
@@ -230,50 +236,44 @@ impl ReplSession {
     }
 }
 
-impl ReplSession {
+#[cfg(not(feature = "async"))]
+impl<P, S: Read + NonBlocking> ReplSession<P, S> {
     /// Block until prompt is found
-    #[cfg(not(feature = "async"))]
     pub fn expect_prompt(&mut self) -> Result<(), Error> {
         self._expect_prompt()?;
         Ok(())
     }
 
+    fn _expect_prompt(&mut self) -> Result<Captures, Error> {
+        let prompt = self.prompt.clone();
+        self.expect(prompt)
+    }
+}
+
+#[cfg(feature = "async")]
+impl<P, S: AsyncRead + Unpin> ReplSession<P, S> {
     /// Block until prompt is found
-    #[cfg(feature = "async")]
     pub async fn expect_prompt(&mut self) -> Result<(), Error> {
         self._expect_prompt().await?;
         Ok(())
     }
 
-    #[cfg(not(feature = "async"))]
-    fn _expect_prompt(&mut self) -> Result<Captures, Error> {
-        let prompt = self.prompt.clone();
-        self.expect(prompt)
-    }
-
-    #[cfg(feature = "async")]
     async fn _expect_prompt(&mut self) -> Result<Captures, Error> {
         let prompt = self.prompt.clone();
         self.expect(prompt).await
     }
+}
 
+#[cfg(not(feature = "async"))]
+impl<P, S: Read + NonBlocking + Write> ReplSession<P, S> {
     /// Send a command to a repl and verifies that it exited.
     /// Returning it's output.
-    #[cfg(not(feature = "async"))]
     pub fn execute<SS: AsRef<str> + Clone>(&mut self, cmd: SS) -> Result<Vec<u8>, Error> {
         self.send_line(cmd)?;
         let found = self._expect_prompt()?;
         Ok(found.before().to_vec())
     }
 
-    /// Send a command to a repl and verifies that it exited.
-    #[cfg(feature = "async")]
-    pub async fn execute<SS: AsRef<str> + Clone>(&mut self, cmd: SS) -> Result<Vec<u8>, Error> {
-        self.send_line(cmd).await?;
-        let found = self._expect_prompt().await?;
-        Ok(found.before().to_vec())
-    }
-
     /// Sends line to repl (and flush the output).
     ///
     /// If echo_on=true wait for the input to appear.
@@ -286,11 +286,32 @@ impl ReplSession {
         Ok(())
     }
 
+    /// Send a quit command.
+    ///
+    /// In async version we it won't be send on Drop so,
+    /// If you wan't it to be send you must do it yourself.
+    pub fn exit(&mut self) -> Result<(), Error> {
+        if let Some(quit_command) = self.quit_command.clone() {
+            self.session.send_line(quit_command)?;
+        }
+
+        Ok(())
+    }
+}
+
+#[cfg(feature = "async")]
+impl<P, S: AsyncRead + AsyncWrite + Unpin> ReplSession<P, S> {
+    /// Send a command to a repl and verifies that it exited.
+    pub async fn execute(&mut self, cmd: impl AsRef<str>) -> Result<Vec<u8>, Error> {
+        self.send_line(cmd).await?;
+        let found = self._expect_prompt().await?;
+        Ok(found.before().to_vec())
+    }
+
     /// Sends line to repl (and flush the output).
     ///
     /// If echo_on=true wait for the input to appear.
-    #[cfg(feature = "async")]
-    pub async fn send_line<S: AsRef<str>>(&mut self, line: S) -> Result<(), Error> {
+    pub async fn send_line(&mut self, line: impl AsRef<str>) -> Result<(), Error> {
         self.session.send_line(line.as_ref()).await?;
         if self.is_echo_on {
             self.expect(line.as_ref()).await?;
@@ -302,7 +323,6 @@ impl ReplSession {
     ///
     /// In async version we it won't be send on Drop so,
     /// If you wan't it to be send you must do it yourself.
-    #[cfg(feature = "async")]
     pub async fn exit(&mut self) -> Result<(), Error> {
         if let Some(quit_command) = self.quit_command.clone() {
             self.session.send_line(quit_command).await?;
@@ -312,24 +332,15 @@ impl ReplSession {
     }
 }
 
-#[cfg(not(feature = "async"))]
-impl Drop for ReplSession {
-    fn drop(&mut self) {
-        if let Some(quit_command) = self.quit_command.clone() {
-            self.send_line(&quit_command).unwrap()
-        }
-    }
-}
-
-impl Deref for ReplSession {
-    type Target = Session;
+impl<P, S> Deref for ReplSession<P, S> {
+    type Target = Session<P, S>;
 
     fn deref(&self) -> &Self::Target {
         &self.session
     }
 }
 
-impl DerefMut for ReplSession {
+impl<P, S> DerefMut for ReplSession<P, S> {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.session
     }


Test patch:
diff --git a/tests/repl.rs b/tests/repl.rs
index 3cba15b..f0ddbc2 100644
--- a/tests/repl.rs
+++ b/tests/repl.rs
@@ -26,6 +26,25 @@ fn bash() {
     assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0));
 }
 
+#[cfg(not(feature = "async"))]
+#[test]
+fn bash_with_log() {
+    let mut p = spawn_bash()
+        .unwrap()
+        .upgrade_session(|s| s.with_log(std::io::stderr()))
+        .unwrap();
+
+    p.send_line("echo Hello World").unwrap();
+    let mut msg = String::new();
+    p.read_line(&mut msg).unwrap();
+    assert!(msg.ends_with("Hello World\r\n"));
+
+    thread::sleep(Duration::from_millis(300));
+    p.send_control(ControlCode::EOT).unwrap();
+
+    assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0));
+}
+
 #[cfg(feature = "async")]
 #[test]
 fn bash() {
@@ -46,6 +65,30 @@ fn bash() {
     })
 }
 
+#[cfg(feature = "async")]
+#[test]
+fn bash_with_log() {
+    use futures_lite::AsyncReadExt;
+
+    futures_lite::future::block_on(async {
+        let mut p = spawn_bash()
+            .await
+            .unwrap()
+            .upgrade_session(|s| s.with_log(std::io::stderr()))
+            .unwrap();
+
+        p.send_line("echo Hello World").await.unwrap();
+        let mut msg = String::new();
+        p.read_line(&mut msg).await.unwrap();
+        assert!(msg.ends_with("Hello World\r\n"));
+
+        thread::sleep(Duration::from_millis(300));
+        p.send_control(ControlCode::EOT).await.unwrap();
+
+        assert_eq!(p.wait().unwrap(), WaitStatus::Exited(p.pid(), 0));
+    })
+}
+
 #[cfg(feature = "async")]
 #[test]
 fn python() {