diff options
author | Aspen Smith <root@gws.fyi> | 2024-07-06T13·00-0400 |
---|---|---|
committer | clbot <clbot@tvl.fyi> | 2024-07-07T14·19+0000 |
commit | 0ad986169d06922945d035aab8d82266e798bffc (patch) | |
tree | 683e4633d8fee9636aa14bf5c329281e0b3733a9 /tvix/cli/src/repl.rs | |
parent | 3a79f937951d34c7293bb093e4c21ddc3c2d88fc (diff) |
test(tvix/cli): Make the REPL testable r/8353
Juggle around the internals of the tvix-cli crate so that we expose the Repl as a public type with a `send` method, that sends a string to the repl and *captures all output* so that it can be subsequently asserted on in tests. Then, demonstrate that this works with a single (for now) REPL test using expect-test to assert on the output of a single command sent to the REPL. As the REPL gets more complicated, this will allow us to make tests that cover that complex behavior. Change-Id: I88175bd72d8760c79faade95ebb1d956f08a7b83 Reviewed-on: https://cl.tvl.fyi/c/depot/+/11958 Autosubmit: aspen <root@gws.fyi> Tested-by: BuildkiteCI Reviewed-by: flokli <flokli@flokli.de>
Diffstat (limited to 'tvix/cli/src/repl.rs')
-rw-r--r-- | tvix/cli/src/repl.rs | 218 |
1 files changed, 132 insertions, 86 deletions
diff --git a/tvix/cli/src/repl.rs b/tvix/cli/src/repl.rs index 758874016326..5098fbaeedc3 100644 --- a/tvix/cli/src/repl.rs +++ b/tvix/cli/src/repl.rs @@ -6,8 +6,10 @@ use smol_str::SmolStr; use tvix_eval::Value; use tvix_glue::tvix_store_io::TvixStoreIO; -use crate::evaluate; -use crate::{assignment::Assignment, interpret, AllowIncomplete, Args, IncompleteInput}; +use crate::{ + assignment::Assignment, evaluate, interpret, AllowIncomplete, Args, IncompleteInput, + InterpretResult, +}; fn state_dir() -> Option<PathBuf> { let mut path = dirs::data_dir(); @@ -18,7 +20,7 @@ fn state_dir() -> Option<PathBuf> { } #[derive(Debug, Clone, PartialEq, Eq)] -pub enum ReplCommand<'a> { +pub(crate) enum ReplCommand<'a> { Expr(&'a str), Assign(Assignment<'a>), Explain(&'a str), @@ -65,27 +67,47 @@ The following commands are supported: } } -#[derive(Debug)] -pub struct Repl { +pub struct CommandResult { + output: String, + continue_: bool, +} + +impl CommandResult { + pub fn finalize(self) -> bool { + print!("{}", self.output); + self.continue_ + } + + pub fn output(&self) -> &str { + &self.output + } +} + +pub struct Repl<'a> { /// In-progress multiline input, when the input so far doesn't parse as a complete expression multiline_input: Option<String>, rl: Editor<()>, /// Local variables defined at the top-level in the repl env: HashMap<SmolStr, Value>, + + io_handle: Rc<TvixStoreIO>, + args: &'a Args, } -impl Repl { - pub fn new() -> Self { +impl<'a> Repl<'a> { + pub fn new(io_handle: Rc<TvixStoreIO>, args: &'a Args) -> Self { let rl = Editor::<()>::new().expect("should be able to launch rustyline"); Self { multiline_input: None, rl, env: HashMap::new(), + io_handle, + args, } } - pub fn run(&mut self, io_handle: Rc<TvixStoreIO>, args: &Args) { - if args.compile_only { + pub fn run(&mut self) { + if self.args.compile_only { eprintln!("warning: `--compile-only` has no effect on REPL usage!"); } @@ -112,83 +134,8 @@ impl Repl { let readline = self.rl.readline(prompt); match readline { Ok(line) => { - if line.is_empty() { - continue; - } - - let input = if let Some(mi) = &mut self.multiline_input { - mi.push('\n'); - mi.push_str(&line); - mi - } else { - &line - }; - - let res = match ReplCommand::parse(input) { - ReplCommand::Quit => break, - ReplCommand::Help => { - println!("{}", ReplCommand::HELP); - Ok(false) - } - ReplCommand::Expr(input) => interpret( - Rc::clone(&io_handle), - input, - None, - args, - false, - AllowIncomplete::Allow, - Some(&self.env), - ), - ReplCommand::Assign(Assignment { ident, value }) => { - match evaluate( - Rc::clone(&io_handle), - &value.to_string(), /* FIXME: don't re-parse */ - None, - args, - AllowIncomplete::Allow, - Some(&self.env), - ) { - Ok(Some(value)) => { - self.env.insert(ident.into(), value); - Ok(true) - } - Ok(None) => Ok(true), - Err(incomplete) => Err(incomplete), - } - } - ReplCommand::Explain(input) => interpret( - Rc::clone(&io_handle), - input, - None, - args, - true, - AllowIncomplete::Allow, - Some(&self.env), - ), - ReplCommand::Print(input) => interpret( - Rc::clone(&io_handle), - input, - None, - &Args { - strict: true, - ..(args.clone()) - }, - false, - AllowIncomplete::Allow, - Some(&self.env), - ), - }; - - match res { - Ok(_) => { - self.rl.add_history_entry(input); - self.multiline_input = None; - } - Err(IncompleteInput) => { - if self.multiline_input.is_none() { - self.multiline_input = Some(line); - } - } + if !self.send(line).finalize() { + break; } } Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break, @@ -204,4 +151,103 @@ impl Repl { self.rl.save_history(&path).unwrap(); } } + + /// Send a line of user input to the REPL. Returns a result indicating the output to show to the + /// user, and whether or not to continue + pub fn send(&mut self, line: String) -> CommandResult { + if line.is_empty() { + return CommandResult { + output: String::new(), + continue_: true, + }; + } + + let input = if let Some(mi) = &mut self.multiline_input { + mi.push('\n'); + mi.push_str(&line); + mi + } else { + &line + }; + + let res = match ReplCommand::parse(input) { + ReplCommand::Quit => { + return CommandResult { + output: String::new(), + continue_: true, + }; + } + ReplCommand::Help => { + println!("{}", ReplCommand::HELP); + Ok(InterpretResult::empty_success()) + } + ReplCommand::Expr(input) => interpret( + Rc::clone(&self.io_handle), + input, + None, + self.args, + false, + AllowIncomplete::Allow, + Some(&self.env), + ), + ReplCommand::Assign(Assignment { ident, value }) => { + match evaluate( + Rc::clone(&self.io_handle), + &value.to_string(), /* FIXME: don't re-parse */ + None, + self.args, + AllowIncomplete::Allow, + Some(&self.env), + ) { + Ok(Some(value)) => { + self.env.insert(ident.into(), value); + Ok(InterpretResult::empty_success()) + } + Ok(None) => Ok(InterpretResult::empty_success()), + Err(incomplete) => Err(incomplete), + } + } + ReplCommand::Explain(input) => interpret( + Rc::clone(&self.io_handle), + input, + None, + self.args, + true, + AllowIncomplete::Allow, + Some(&self.env), + ), + ReplCommand::Print(input) => interpret( + Rc::clone(&self.io_handle), + input, + None, + &Args { + strict: true, + ..(self.args.clone()) + }, + false, + AllowIncomplete::Allow, + Some(&self.env), + ), + }; + + match res { + Ok(InterpretResult { output, .. }) => { + self.rl.add_history_entry(input); + self.multiline_input = None; + CommandResult { + output, + continue_: true, + } + } + Err(IncompleteInput) => { + if self.multiline_input.is_none() { + self.multiline_input = Some(line); + } + CommandResult { + output: String::new(), + continue_: true, + } + } + } + } } |