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/lib.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/lib.rs')
-rw-r--r-- | tvix/cli/src/lib.rs | 224 |
1 files changed, 224 insertions, 0 deletions
diff --git a/tvix/cli/src/lib.rs b/tvix/cli/src/lib.rs new file mode 100644 index 000000000000..008593c5d84f --- /dev/null +++ b/tvix/cli/src/lib.rs @@ -0,0 +1,224 @@ +use std::{collections::HashMap, path::PathBuf, rc::Rc}; + +use smol_str::SmolStr; +use std::fmt::Write; +use tracing::{instrument, Span}; +use tracing_indicatif::span_ext::IndicatifSpanExt; +use tvix_build::buildservice; +use tvix_eval::{ + builtins::impure_builtins, + observer::{DisassemblingObserver, TracingObserver}, + ErrorKind, EvalIO, Value, +}; +use tvix_glue::{ + builtins::{add_derivation_builtins, add_fetcher_builtins, add_import_builtins}, + configure_nix_path, + tvix_io::TvixIO, + tvix_store_io::TvixStoreIO, +}; + +pub mod args; +pub mod assignment; +pub mod repl; + +pub use args::Args; +pub use repl::Repl; + +pub fn init_io_handle(tokio_runtime: &tokio::runtime::Runtime, args: &Args) -> Rc<TvixStoreIO> { + let (blob_service, directory_service, path_info_service, nar_calculation_service) = + tokio_runtime + .block_on({ + let blob_service_addr = args.blob_service_addr.clone(); + let directory_service_addr = args.directory_service_addr.clone(); + let path_info_service_addr = args.path_info_service_addr.clone(); + async move { + tvix_store::utils::construct_services( + blob_service_addr, + directory_service_addr, + path_info_service_addr, + ) + .await + } + }) + .expect("unable to setup {blob|directory|pathinfo}service before interpreter setup"); + + let build_service = tokio_runtime + .block_on({ + let blob_service = blob_service.clone(); + let directory_service = directory_service.clone(); + async move { + buildservice::from_addr( + &args.build_service_addr, + blob_service.clone(), + directory_service.clone(), + ) + .await + } + }) + .expect("unable to setup buildservice before interpreter setup"); + + Rc::new(TvixStoreIO::new( + blob_service.clone(), + directory_service.clone(), + path_info_service.into(), + nar_calculation_service.into(), + build_service.into(), + tokio_runtime.handle().clone(), + )) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum AllowIncomplete { + Allow, + #[default] + RequireComplete, +} + +impl AllowIncomplete { + fn allow(&self) -> bool { + matches!(self, Self::Allow) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct IncompleteInput; + +/// Interprets the given code snippet, printing out warnings and errors and returning the result +pub fn evaluate( + tvix_store_io: Rc<TvixStoreIO>, + code: &str, + path: Option<PathBuf>, + args: &Args, + allow_incomplete: AllowIncomplete, + env: Option<&HashMap<SmolStr, Value>>, +) -> Result<Option<Value>, IncompleteInput> { + let span = Span::current(); + span.pb_start(); + span.pb_set_style(&tvix_tracing::PB_SPINNER_STYLE); + span.pb_set_message("Setting up evaluator…"); + + let mut eval_builder = tvix_eval::Evaluation::builder(Box::new(TvixIO::new( + tvix_store_io.clone() as Rc<dyn EvalIO>, + )) as Box<dyn EvalIO>) + .enable_import() + .with_strict(args.strict) + .add_builtins(impure_builtins()) + .env(env); + + eval_builder = add_derivation_builtins(eval_builder, Rc::clone(&tvix_store_io)); + eval_builder = add_fetcher_builtins(eval_builder, Rc::clone(&tvix_store_io)); + eval_builder = add_import_builtins(eval_builder, tvix_store_io); + eval_builder = configure_nix_path(eval_builder, &args.nix_search_path); + + let source_map = eval_builder.source_map().clone(); + let result = { + let mut compiler_observer = + DisassemblingObserver::new(source_map.clone(), std::io::stderr()); + if args.dump_bytecode { + eval_builder.set_compiler_observer(Some(&mut compiler_observer)); + } + + let mut runtime_observer = TracingObserver::new(std::io::stderr()); + if args.trace_runtime { + if args.trace_runtime_timing { + runtime_observer.enable_timing() + } + eval_builder.set_runtime_observer(Some(&mut runtime_observer)); + } + + span.pb_set_message("Evaluating…"); + + let eval = eval_builder.build(); + eval.evaluate(code, path) + }; + + if allow_incomplete.allow() + && result.errors.iter().any(|err| { + matches!( + &err.kind, + ErrorKind::ParseErrors(pes) + if pes.iter().any(|pe| matches!(pe, rnix::parser::ParseError::UnexpectedEOF)) + ) + }) + { + return Err(IncompleteInput); + } + + if args.display_ast { + if let Some(ref expr) = result.expr { + eprintln!("AST: {}", tvix_eval::pretty_print_expr(expr)); + } + } + + for error in &result.errors { + error.fancy_format_stderr(); + } + + if !args.no_warnings { + for warning in &result.warnings { + warning.fancy_format_stderr(&source_map); + } + } + + Ok(result.value) +} + +pub struct InterpretResult { + output: String, + success: bool, +} + +impl InterpretResult { + pub fn empty_success() -> Self { + Self { + output: String::new(), + success: true, + } + } + + pub fn finalize(self) -> bool { + print!("{}", self.output); + self.success + } + + pub fn output(&self) -> &str { + &self.output + } + + pub fn success(&self) -> bool { + self.success + } +} + +/// Interprets the given code snippet, printing out warnings, errors +/// and the result itself. The return value indicates whether +/// evaluation succeeded. +#[instrument(skip_all, fields(indicatif.pb_show=1))] +pub fn interpret( + tvix_store_io: Rc<TvixStoreIO>, + code: &str, + path: Option<PathBuf>, + args: &Args, + explain: bool, + allow_incomplete: AllowIncomplete, + env: Option<&HashMap<SmolStr, Value>>, +) -> Result<InterpretResult, IncompleteInput> { + let mut output = String::new(); + let result = evaluate(tvix_store_io, code, path, args, allow_incomplete, env)?; + + if let Some(value) = result.as_ref() { + if explain { + writeln!(&mut output, "=> {}", value.explain()).unwrap(); + } else if args.raw { + writeln!(&mut output, "{}", value.to_contextful_str().unwrap()).unwrap(); + } else { + writeln!(&mut output, "=> {} :: {}", value, value.type_of()).unwrap(); + } + } + + // inform the caller about any errors + Ok(InterpretResult { + output, + success: result.is_some(), + }) +} |