From c8b068c2c4b133580aed3cb74fca40d3837651e4 Mon Sep 17 00:00:00 2001 From: warren2k <846021+warren2k@users.noreply.github.com> Date: Sun, 2 Jun 2024 10:32:12 -0400 Subject: [PATCH] feat: multiline results Co-authored-by: ruben beck Co-authored-by: Shahar "Dawn" Or --- src/app/state/repl_state.rs | 5 -- src/repl/driver.rs | 24 +++++++-- src/repl/example.rs | 100 ++++++++++++++++-------------------- tests/repl.rs | 29 +++++++++-- 4 files changed, 89 insertions(+), 69 deletions(-) diff --git a/src/app/state/repl_state.rs b/src/app/state/repl_state.rs index 81d382c..f3ed2a1 100644 --- a/src/app/state/repl_state.rs +++ b/src/app/state/repl_state.rs @@ -77,11 +77,6 @@ impl Iterator for ReplSessionLive { #[derive(Debug, Clone, PartialEq, Eq, derive_more::Deref, derive_more::Display)] pub(crate) struct ExpectedResult(pub(crate) String); -impl ExpectedResult { - pub(crate) fn empty() -> ExpectedResult { - Self(String::new()) - } -} impl From for ExpectedResult { fn from(expected_result: LFLine) -> Self { diff --git a/src/repl/driver.rs b/src/repl/driver.rs index dab98a4..7266d5f 100644 --- a/src/repl/driver.rs +++ b/src/repl/driver.rs @@ -7,10 +7,8 @@ use crate::example_id::ExampleId; #[derive(Debug, Clone, PartialEq, Eq, derive_more::Deref, derive_more::Display)] pub(crate) struct LFLine(String); -impl std::str::FromStr for LFLine { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { +impl LFLine { + fn validate_str(s: &str) -> anyhow::Result<()> { s.chars() .with_position() .try_for_each(|(position, character)| { @@ -23,10 +21,28 @@ impl std::str::FromStr for LFLine { (_, _) => Ok(()), } })?; + Ok(()) + } +} + +impl std::str::FromStr for LFLine { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Self::validate_str(s)?; Ok(Self(s.to_string())) } } +impl TryFrom for LFLine { + type Error = anyhow::Error; + + fn try_from(s: String) -> Result { + Self::validate_str(&s)?; + Ok(Self(s)) + } +} + #[derive(Debug, Clone, derive_more::Deref, PartialEq, Eq)] pub(crate) struct ReplQuery(LFLine); diff --git a/src/repl/example.rs b/src/repl/example.rs index 86bee3b..ebcc99b 100644 --- a/src/repl/example.rs +++ b/src/repl/example.rs @@ -1,6 +1,8 @@ +use anyhow::bail; + use crate::{app::state::repl_state::ExpectedResult, example_id::ExampleId}; -use super::driver::{LFLine, ReplQuery}; +use super::driver::ReplQuery; #[derive(Debug, Clone)] pub(crate) struct ReplExample { @@ -20,66 +22,29 @@ impl ReplExample { #[derive(Debug, Clone, derive_more::IntoIterator)] pub(crate) struct ReplExampleEntries(Vec); -#[derive(Debug, Default)] -struct ParseState { - entries: Vec, - expecting: Expecting, -} - -#[derive(Debug, Default, PartialEq, Eq)] -enum Expecting { - #[default] - PromptAndQuery, - ResultOrBlankLine(ReplQuery), - BlankLine(ReplQuery, ExpectedResult), -} - impl std::str::FromStr for ReplExampleEntries { type Err = anyhow::Error; fn from_str(s: &str) -> Result { - let final_state = - s.split_inclusive('\n') - .try_fold(ParseState::default(), |mut state, line| { - let line = LFLine::from_str(line)?; - match state.expecting { - Expecting::PromptAndQuery => { - let Some(line) = line.strip_prefix("nix-repl> ") else { - anyhow::bail!("expected prompt, found {line:?}"); - }; - let query = LFLine::from_str(line).unwrap(); - let query = ReplQuery::new(query); - state.expecting = Expecting::ResultOrBlankLine(query); - } - Expecting::ResultOrBlankLine(query) => { - if line.as_str() == "\n" { - state - .entries - .push(ReplEntry::new(query, ExpectedResult::empty())); - state.expecting = Expecting::PromptAndQuery; - } else { - let expected = ExpectedResult::from(line); - state.expecting = Expecting::BlankLine(query, expected); - } - } - Expecting::BlankLine(query, expected) => { - anyhow::ensure!( - line.as_str() == "\n", - "expected blank line, found {line:?}" - ); - state.entries.push(ReplEntry::new(query, expected)); - state.expecting = Expecting::PromptAndQuery; - } - } - Ok(state) - })?; - - anyhow::ensure!( - final_state.expecting == Expecting::PromptAndQuery, - "failed to parse" - ); - - Ok(Self(final_state.entries)) + let Some(rest) = s.strip_prefix("nix-repl> ") else { + bail!("repl example must start with a prompt") + }; + + let entries = rest + .split("\nnix-repl> ") + .map(|pair| { + let Some((query, expected)) = pair.split_once('\n') else { + bail!("query must be followed by a line feed") + }; + + Ok(ReplEntry::new( + ReplQuery::new(format!("{query}\n").try_into().unwrap()), + ExpectedResult(expected.trim_end().to_owned()), + )) + }) + .collect::>()?; + + Ok(Self(entries)) } } @@ -236,5 +201,26 @@ mod test { }, ], }, + { + input: indoc! {r#" + nix-repl> { a=1; b=2; } + { + a = 1 + b = 2 + } + + "#}, + expected_output: [ + { + query: "{ a=1; b=2; }\n", + expected_result: indoc! {" + { + a = 1 + b = 2 + }\ + "}, + }, + ], + }, ]; } diff --git a/tests/repl.rs b/tests/repl.rs index 9338934..2c3645e 100644 --- a/tests/repl.rs +++ b/tests/repl.rs @@ -19,9 +19,8 @@ fn fails_to_parse() { let file_path = file.path().to_str().unwrap(); eelco.assert().failure().stderr( - predicates::str::starts_with(format!("Error: {file_path}:1")).and( - predicates::str::contains("expected prompt, found LFLine(\"nix-shnepl> nope\\n\")"), - ), + predicates::str::starts_with(format!("Error: {file_path}:1")) + .and(predicates::str::contains("prompt")), ); }); } @@ -111,3 +110,27 @@ fn pass_subsequent_query() { .stderr(format!("PASS: {file_path}:1\n")); }); } + +#[test] +fn multiline_result() { + with_eelco(|file, eelco| { + file.write_str(indoc! {" + ```nix-repl + nix-repl> { a = 2; b = 3; } + { + a = 2; + b = 3; + } + + ``` + "}) + .unwrap(); + + let file_path = file.path().to_str().unwrap(); + + eelco + .assert() + .success() + .stderr(format!("PASS: {file_path}:1\n")); + }); +}