about summary refs log tree commit diff
path: root/tvix/cli/src/repl.rs
diff options
context:
space:
mode:
authorAspen Smith <root@gws.fyi>2024-07-06T13·00-0400
committerclbot <clbot@tvl.fyi>2024-07-07T14·19+0000
commit0ad986169d06922945d035aab8d82266e798bffc (patch)
tree683e4633d8fee9636aa14bf5c329281e0b3733a9 /tvix/cli/src/repl.rs
parent3a79f937951d34c7293bb093e4c21ddc3c2d88fc (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.rs218
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,
+                }
+            }
+        }
+    }
 }