noshell 0.5.0

noshell, a no_std argument parser and a shell for constrained systems.
Documentation
use futures::pin_mut;
use rstest::{Context, rstest};
use speculoos::prelude::*;

use noterm::{events, io};

use super::{Prompt, readline, unescape};

#[rstest]
#[case::empty(r#""#, "")]
#[case::quote(r#"'"#, "'")]
#[case::double_quote(r#"''"#, "''")]
#[case::single_quoted(r#"word"#, "word")]
#[case::special_dollar(r#"\$word"#, "$word")]
#[case::special_backslash(r#"\\word"#, "\\word")]
#[case::special_double_quote(r#"\"word"#, "\"word")]
#[case::hex(r#"\x33word"#, "\\x33word")]
#[case::multiline("word0 \\\nword1", "word0 word1")]
fn it_should_unescape_string(#[case] input: &str, #[case] expected: &str) {
    assert_that!(unescape::<256>(input).as_str()).is_equal_to(expected);
}

struct StringBuf {
    inner: String,
    cursor: usize,
}

impl StringBuf {
    fn new(inner: String) -> Self {
        StringBuf { inner, cursor: 0 }
    }
}

impl io::Read for StringBuf {
    async fn read(&mut self, data: &mut [u8]) -> io::Result<usize> {
        let n = (self.inner.len() - self.cursor).min(data.len());

        let (input, _) = self.inner.as_bytes().split_at(self.cursor + n);
        let (output, _) = data.split_at_mut(n);

        output.copy_from_slice(&input[self.cursor..]);
        self.cursor += n;

        Ok(n)
    }
}

impl io::blocking::Write for StringBuf {
    fn write(&mut self, data: &[u8]) -> io::Result<usize> {
        self.inner.push_str(str::from_utf8(data).unwrap());
        Ok(data.len())
    }

    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

#[tokio::test]
async fn it_should_print_prompt() {
    let cmdline = String::from("\x0d");
    let mut input = StringBuf::new(cmdline.clone());
    let mut output = StringBuf::new(String::default());

    let stream = events::stream(&mut input);
    let prompt = Prompt::new("prompt>");

    pin_mut!(stream);

    let line: Result<heapless::String<256>, _> = readline(&prompt, stream, &mut output).await;
    assert_that!(line).is_ok();

    let result = output.inner.as_str();
    insta::with_settings!({
        description => format!("cmdline: {}", cmdline),
        omit_expression => true,
    }, {
        insta::assert_snapshot!(result);
    });
}

#[rstest]
#[case::empty("\x0d")]
#[case::single("word\x0d")]
#[case::multiple("word0 word1\x0d")]
#[case::newline("word0 \\\nword1\x0d")]
#[tokio::test]
async fn it_should_read_line(#[context] ctx: Context, #[case] input: &str) {
    let cmdline = String::from(input);
    let mut input = StringBuf::new(cmdline.clone());
    let mut output = StringBuf::new(String::default());

    let stream = events::stream(&mut input);
    let prompt = Prompt::new("prompt>");

    pin_mut!(stream);

    let line: Result<heapless::String<256>, _> = readline(&prompt, stream, &mut output).await;
    let result = assert_that!(line).is_ok().subject;

    insta::with_settings!({
        description => format!("cmdline: {}", cmdline),
        omit_expression => true,
        snapshot_suffix => ctx.description.unwrap_or_default(),
    }, {
        insta::assert_snapshot!(result);
    });
}