From 3676a8a48d2fe32c1800728a54bddae540a32175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1dn=C3=ADk?= Date: Sun, 10 Jul 2022 13:45:46 +0300 Subject: [PATCH] Expand Hooks Functionality (#5982) * (WIP) Initial messy support for hooks as strings * Cleanup after running condition & hook code Also, remove prints * Move env hooks eval into its own function * Add env change hooks to simulator * Fix hooks simulator not running env hooks properly * Add missing hooks test file * Expand hooks tests * Add blocks as env hooks; Preserve hook environment * Add full eval to pre prompt/exec hooks; Fix panic * Rename env change hook back to orig. name * Print err on test failure; Add list of hooks test * Consolidate condition block; Fix panic; Misc * CHange test to use real file * Remove unused stuff * Fix potential panics; Clean up errors * Remove commented unused code * Clippy: Fix extra references * Add back support for old-style hooks * Reorder functions; Fmt * Fix test on Windows * Add more test cases; Simplify some error reporting * Add more tests for setting correct before/after * Move pre_prompt hook to the beginning Since we don't have a prompt or blocking on user input, all hooks just follow after each other. --- crates/nu-cli/src/lib.rs | 1 + crates/nu-cli/src/repl.rs | 406 ++++++++++++++------ crates/nu-cli/src/util.rs | 9 + crates/nu-protocol/src/cli_error.rs | 4 +- tests/hooks/mod.rs | 564 ++++++++++++++++++++++++++++ tests/hooks/samples/.nu-env | 1 + tests/main.rs | 1 + tests/nu_repl/mod.rs | 59 ++- 8 files changed, 917 insertions(+), 128 deletions(-) create mode 100644 tests/hooks/mod.rs create mode 100644 tests/hooks/samples/.nu-env diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index cd729486b3..23f64bb5bf 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -22,6 +22,7 @@ pub use nu_highlight::NuHighlight; pub use print::Print; pub use prompt::NushellPrompt; pub use repl::evaluate_repl; +pub use repl::{eval_env_change_hook, eval_hook}; pub use syntax_highlight::NuHighlighter; pub use util::{eval_source, gather_parent_env_vars, get_init_cwd, report_error}; pub use validation::NuValidator; diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index a3203e3a27..11df52226c 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -2,17 +2,18 @@ use crate::{ completions::NuCompleter, prompt_update, reedline_config::{add_menus, create_keybindings, KeybindingsMode}, - util::{eval_source, report_error}, + util::{eval_source, get_init_cwd, report_error, report_error_new}, NuHighlighter, NuValidator, NushellPrompt, }; use log::{info, trace}; use miette::{IntoDiagnostic, Result}; use nu_color_config::get_color_config; use nu_engine::{convert_env_values, eval_block}; -use nu_parser::lex; +use nu_parser::{lex, parse}; use nu_protocol::{ + ast::PathMember, engine::{EngineState, Stack, StateWorkingSet}, - BlockId, HistoryFileFormat, PipelineData, PositionalArg, ShellError, Span, Value, + BlockId, HistoryFileFormat, PipelineData, PositionalArg, ShellError, Span, Type, Value, VarId, }; use reedline::{DefaultHinter, Emacs, SqliteBackedHistory, Vi}; use std::io::{self, Write}; @@ -79,7 +80,7 @@ pub fn evaluate_repl( // Get the config once for the history `max_history_size` // Updating that will not be possible in one session - let mut config = engine_state.get_config(); + let config = engine_state.get_config(); if is_perf_true { info!("setup reedline {}:{}:{}", file!(), line!(), column!()); @@ -130,7 +131,7 @@ pub fn evaluate_repl( sig_quit.store(false, Ordering::SeqCst); } - config = engine_state.get_config(); + let config = engine_state.get_config(); if is_perf_true { info!("setup colors {}:{}:{}", file!(), line!(), column!()); @@ -236,58 +237,22 @@ pub fn evaluate_repl( // Right before we start our prompt and take input from the user, // fire the "pre_prompt" hook - if let Some(hook) = &config.hooks.pre_prompt { - if let Err(err) = run_hook(engine_state, stack, vec![], hook) { - let working_set = StateWorkingSet::new(engine_state); - report_error(&working_set, &err); + if let Some(hook) = config.hooks.pre_prompt.clone() { + if let Err(err) = eval_hook(engine_state, stack, vec![], &hook) { + report_error_new(engine_state, &err); } } // Next, check all the environment variables they ask for // fire the "env_change" hook - if let Some(hook) = config.hooks.env_change.clone() { - match hook { - Value::Record { - cols, vals: blocks, .. - } => { - for (idx, env_var) in cols.iter().enumerate() { - let before = engine_state - .previous_env_vars - .get(env_var) - .cloned() - .unwrap_or_default(); - let after = stack.get_env_var(engine_state, env_var).unwrap_or_default(); - if before != after { - if let Err(err) = run_hook( - engine_state, - stack, - vec![before, after.clone()], - &blocks[idx], - ) { - let working_set = StateWorkingSet::new(engine_state); - report_error(&working_set, &err); - } - - engine_state - .previous_env_vars - .insert(env_var.to_string(), after); - } - } - } - x => { - let working_set = StateWorkingSet::new(engine_state); - report_error( - &working_set, - &ShellError::TypeMismatch( - "record for 'env_change' hook".to_string(), - x.span().unwrap_or_else(|_| Span::new(0, 0)), - ), - ) - } - } + let config = engine_state.get_config(); + if let Err(error) = + eval_env_change_hook(config.hooks.env_change.clone(), engine_state, stack) + { + report_error_new(engine_state, &error) } - config = engine_state.get_config(); + let config = engine_state.get_config(); let shell_integration = config.shell_integration; if shell_integration { @@ -328,10 +293,9 @@ pub fn evaluate_repl( // Right before we start running the code the user gave us, // fire the "pre_execution" hook - if let Some(hook) = &config.hooks.pre_execution { - if let Err(err) = run_hook(engine_state, stack, vec![], hook) { - let working_set = StateWorkingSet::new(engine_state); - report_error(&working_set, &err); + if let Some(hook) = config.hooks.pre_execution.clone() { + if let Err(err) = eval_hook(engine_state, stack, vec![], &hook) { + report_error_new(engine_state, &err); } } @@ -491,6 +455,280 @@ pub fn evaluate_repl( Ok(()) } +pub fn eval_env_change_hook( + env_change_hook: Option, + engine_state: &mut EngineState, + stack: &mut Stack, +) -> Result<(), ShellError> { + if let Some(hook) = env_change_hook { + match hook { + Value::Record { + cols: env_names, + vals: hook_values, + .. + } => { + for (env_name, hook_value) in env_names.iter().zip(hook_values.iter()) { + let before = engine_state + .previous_env_vars + .get(env_name) + .cloned() + .unwrap_or_default(); + + let after = stack + .get_env_var(engine_state, env_name) + .unwrap_or_default(); + + if before != after { + eval_hook( + engine_state, + stack, + vec![("$before".into(), before), ("$after".into(), after.clone())], + hook_value, + )?; + + engine_state + .previous_env_vars + .insert(env_name.to_string(), after); + } + } + } + x => { + return Err(ShellError::TypeMismatch( + "record for the 'env_change' hook".to_string(), + x.span()?, + )); + } + } + } + + Ok(()) +} + +pub fn eval_hook( + engine_state: &mut EngineState, + stack: &mut Stack, + arguments: Vec<(String, Value)>, + value: &Value, +) -> Result<(), ShellError> { + let value_span = value.span()?; + + let condition_path = PathMember::String { + val: "condition".to_string(), + span: value_span, + }; + + let code_path = PathMember::String { + val: "code".to_string(), + span: value_span, + }; + + match value { + Value::List { vals, .. } => { + for val in vals { + eval_hook(engine_state, stack, arguments.clone(), val)? + } + } + Value::Record { .. } => { + let do_run_hook = + if let Ok(condition) = value.clone().follow_cell_path(&[condition_path], false) { + match condition { + Value::Block { + val: block_id, + span: block_span, + .. + } => { + match run_hook_block( + engine_state, + stack, + block_id, + arguments.clone(), + block_span, + ) { + Ok(value) => match value { + Value::Bool { val, .. } => val, + other => { + return Err(ShellError::UnsupportedConfigValue( + "boolean output".to_string(), + format!("{}", other.get_type()), + other.span()?, + )); + } + }, + Err(err) => { + return Err(err); + } + } + } + other => { + return Err(ShellError::UnsupportedConfigValue( + "block".to_string(), + format!("{}", other.get_type()), + other.span()?, + )); + } + } + } else { + // always run the hook + true + }; + + if do_run_hook { + match value.clone().follow_cell_path(&[code_path], false)? { + Value::String { + val, + span: source_span, + } => { + let (block, delta, vars) = { + let mut working_set = StateWorkingSet::new(engine_state); + + let mut vars: Vec<(VarId, Value)> = vec![]; + + for (name, val) in arguments { + let var_id = working_set.add_variable( + name.as_bytes().to_vec(), + val.span()?, + Type::Any, + ); + + vars.push((var_id, val)); + } + + let (output, err) = + parse(&mut working_set, Some("hook"), val.as_bytes(), false, &[]); + if let Some(err) = err { + report_error(&working_set, &err); + + return Err(ShellError::UnsupportedConfigValue( + "valid source code".into(), + "source code with syntax errors".into(), + source_span, + )); + } + + (output, working_set.render(), vars) + }; + + let cwd = match nu_engine::env::current_dir(engine_state, stack) { + Ok(p) => p, + Err(e) => { + report_error_new(engine_state, &e); + get_init_cwd() + } + }; + + let _ = engine_state.merge_delta(delta, Some(stack), &cwd); + let input = PipelineData::new(value_span); + + let var_ids: Vec = vars + .into_iter() + .map(|(var_id, val)| { + stack.add_var(var_id, val); + var_id + }) + .collect(); + + match eval_block(engine_state, stack, &block, input, false, false) { + Ok(_) => {} + Err(err) => { + report_error_new(engine_state, &err); + } + } + + for var_id in var_ids.iter() { + stack.vars.remove(var_id); + } + } + Value::Block { + val: block_id, + span: block_span, + .. + } => { + run_hook_block(engine_state, stack, block_id, arguments, block_span)?; + } + other => { + return Err(ShellError::UnsupportedConfigValue( + "block or string".to_string(), + format!("{}", other.get_type()), + other.span()?, + )); + } + } + } + } + Value::Block { + val: block_id, + span: block_span, + .. + } => { + run_hook_block(engine_state, stack, *block_id, arguments, *block_span)?; + } + other => { + return Err(ShellError::UnsupportedConfigValue( + "block, record, or list of records".into(), + format!("{}", other.get_type()), + other.span()?, + )); + } + } + + Ok(()) +} + +pub fn run_hook_block( + engine_state: &EngineState, + stack: &mut Stack, + block_id: BlockId, + arguments: Vec<(String, Value)>, + span: Span, +) -> Result { + let block = engine_state.get_block(block_id); + + let input = PipelineData::new(span); + + let mut callee_stack = stack.gather_captures(&block.captures); + + for (idx, PositionalArg { var_id, .. }) in + block.signature.required_positional.iter().enumerate() + { + if let Some(var_id) = var_id { + if let Some(arg) = arguments.get(idx) { + callee_stack.add_var(*var_id, arg.1.clone()) + } else { + return Err(ShellError::IncompatibleParametersSingle( + "This hook block has too many parameters".into(), + span, + )); + } + } + } + + match eval_block(engine_state, &mut callee_stack, block, input, false, false) { + Ok(pipeline_data) => match pipeline_data.into_value(span) { + Value::Error { error } => Err(error), + val => { + // If all went fine, preserve the environment of the called block + let caller_env_vars = stack.get_env_var_names(engine_state); + + // remove env vars that are present in the caller but not in the callee + // (the callee hid them) + for var in caller_env_vars.iter() { + if !callee_stack.has_env_var(engine_state, var) { + stack.remove_env_var(engine_state, var); + } + } + + // add new env vars from callee to caller + for (var, value) in callee_stack.get_stack_env_vars() { + stack.add_env_var(var, value); + } + + Ok(val) + } + }, + Err(err) => Err(err), + } +} + fn run_ansi_sequence(seq: &str) -> Result<(), ShellError> { match io::stdout().write_all(seq.as_bytes()) { Ok(it) => it, @@ -514,63 +752,3 @@ fn run_ansi_sequence(seq: &str) -> Result<(), ShellError> { ) }) } - -pub fn run_hook( - engine_state: &EngineState, - stack: &mut Stack, - arguments: Vec, - value: &Value, -) -> Result<(), ShellError> { - match value { - Value::List { vals, .. } => { - for val in vals { - run_hook(engine_state, stack, arguments.clone(), val)? - } - Ok(()) - } - Value::Block { - val: block_id, - span, - .. - } => run_hook_block(engine_state, stack, *block_id, arguments, *span), - x => match x.span() { - Ok(span) => Err(ShellError::MissingConfigValue( - "block for hook in config".into(), - span, - )), - _ => Err(ShellError::MissingConfigValue( - "block for hook in config".into(), - Span { start: 0, end: 0 }, - )), - }, - } -} - -pub fn run_hook_block( - engine_state: &EngineState, - stack: &mut Stack, - block_id: BlockId, - arguments: Vec, - span: Span, -) -> Result<(), ShellError> { - let block = engine_state.get_block(block_id); - let input = PipelineData::new(span); - - let mut callee_stack = stack.gather_captures(&block.captures); - - for (idx, PositionalArg { var_id, .. }) in - block.signature.required_positional.iter().enumerate() - { - if let Some(var_id) = var_id { - callee_stack.add_var(*var_id, arguments[idx].clone()) - } - } - - match eval_block(engine_state, &mut callee_stack, block, input, false, false) { - Ok(pipeline_data) => match pipeline_data.into_value(span) { - Value::Error { error } => Err(error), - _ => Ok(()), - }, - Err(err) => Err(err), - } -} diff --git a/crates/nu-cli/src/util.rs b/crates/nu-cli/src/util.rs index 48f56254c2..4c9f6d0852 100644 --- a/crates/nu-cli/src/util.rs +++ b/crates/nu-cli/src/util.rs @@ -297,6 +297,15 @@ pub fn report_error( } } +pub fn report_error_new( + engine_state: &EngineState, + error: &(dyn miette::Diagnostic + Send + Sync + 'static), +) { + let working_set = StateWorkingSet::new(engine_state); + + report_error(&working_set, error); +} + pub fn get_init_cwd() -> PathBuf { match std::env::current_dir() { Ok(cwd) => cwd, diff --git a/crates/nu-protocol/src/cli_error.rs b/crates/nu-protocol/src/cli_error.rs index dfaa8ad5d5..e49f895184 100644 --- a/crates/nu-protocol/src/cli_error.rs +++ b/crates/nu-protocol/src/cli_error.rs @@ -31,7 +31,9 @@ impl std::fmt::Debug for CliError<'_> { .terminal_links(ansi_support) .build(); - miette_handler.debug(self, f)?; + // Ignore error to prevent format! panics. This can happen if span points at some + // inaccessible location, for example by calling `report_error()` with wrong working set. + let _ = miette_handler.debug(self, f); Ok(()) } diff --git a/tests/hooks/mod.rs b/tests/hooks/mod.rs new file mode 100644 index 0000000000..87fc953ad8 --- /dev/null +++ b/tests/hooks/mod.rs @@ -0,0 +1,564 @@ +use super::nu_repl::nu_repl; + +fn env_change_hook_code_list(name: &str, code_list: &[&str]) -> String { + let mut list = String::new(); + + for code in code_list.iter() { + list.push_str("{ code: "); + list.push_str(code); + list.push_str(" }\n"); + } + + format!( + r#"let-env config = {{ + hooks: {{ + env_change: {{ + {name} : [ + {list} + ] + }} + }} + }}"# + ) +} + +fn env_change_hook(name: &str, code: &str) -> String { + format!( + r#"let-env config = {{ + hooks: {{ + env_change: {{ + {name} : {code} + }} + }} + }}"# + ) +} + +fn env_change_hook_code(name: &str, code: &str) -> String { + format!( + r#"let-env config = {{ + hooks: {{ + env_change: {{ + {name} : {{ + code: {code} + }} + }} + }} + }}"# + ) +} + +fn env_change_hook_code_condition(name: &str, condition: &str, code: &str) -> String { + format!( + r#"let-env config = {{ + hooks: {{ + env_change: {{ + {name} : {{ + condition: {condition} + code: {code} + }} + }} + }} + }}"# + ) +} + +fn pre_prompt_hook(code: &str) -> String { + format!( + r#"let-env config = {{ + hooks: {{ + pre_prompt: {code} + }} + }}"# + ) +} + +fn pre_prompt_hook_code(code: &str) -> String { + format!( + r#"let-env config = {{ + hooks: {{ + pre_prompt: {{ + code: {code} + }} + }} + }}"# + ) +} + +fn pre_execution_hook(code: &str) -> String { + format!( + r#"let-env config = {{ + hooks: {{ + pre_execution: {code} + }} + }}"# + ) +} + +fn pre_execution_hook_code(code: &str) -> String { + format!( + r#"let-env config = {{ + hooks: {{ + pre_execution: {{ + code: {code} + }} + }} + }}"# + ) +} + +#[test] +fn env_change_define_command() { + let inp = &[ + &env_change_hook_code("FOO", r#"'def foo [] { "got foo!" }'"#), + "let-env FOO = 1", + "foo", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "got foo!"); +} + +#[test] +fn env_change_define_variable() { + let inp = &[ + &env_change_hook_code("FOO", r#"'let x = "spam"'"#), + "let-env FOO = 1", + "$x", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "spam"); +} + +#[test] +fn env_change_define_env_var() { + let inp = &[ + &env_change_hook_code("FOO", r#"'let-env SPAM = "spam"'"#), + "let-env FOO = 1", + "$env.SPAM", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "spam"); +} + +#[test] +fn env_change_define_alias() { + let inp = &[ + &env_change_hook_code("FOO", r#"'alias spam = "spam"'"#), + "let-env FOO = 1", + "spam", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "spam"); +} + +#[test] +fn env_change_simple_block_preserve_env_var() { + let inp = &[ + &env_change_hook("FOO", r#"{ let-env SPAM = "spam" }"#), + "let-env FOO = 1", + "$env.SPAM", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "spam"); +} + +#[test] +fn env_change_simple_block_list_shadow_env_var() { + let inp = &[ + &env_change_hook( + "FOO", + r#"[ + { let-env SPAM = "foo" } + { let-env SPAM = "spam" } + ]"#, + ), + "let-env FOO = 1", + "$env.SPAM", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "spam"); +} + +#[test] +fn env_change_block_preserve_env_var() { + let inp = &[ + &env_change_hook_code("FOO", r#"{ let-env SPAM = "spam" }"#), + "let-env FOO = 1", + "$env.SPAM", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "spam"); +} + +#[test] +fn pre_prompt_define_command() { + let inp = &[ + &pre_prompt_hook_code(r#"'def foo [] { "got foo!" }'"#), + "", + "foo", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "got foo!"); +} + +#[test] +fn pre_prompt_simple_block_preserve_env_var() { + let inp = &[ + &pre_prompt_hook(r#"{ let-env SPAM = "spam" }"#), + "", + "$env.SPAM", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "spam"); +} + +#[test] +fn pre_prompt_simple_block_list_shadow_env_var() { + let inp = &[ + &pre_prompt_hook( + r#"[ + { let-env SPAM = "foo" } + { let-env SPAM = "spam" } + ]"#, + ), + "", + "$env.SPAM", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "spam"); +} + +#[test] +fn pre_prompt_block_preserve_env_var() { + let inp = &[ + &pre_prompt_hook_code(r#"{ let-env SPAM = "spam" }"#), + "", + "$env.SPAM", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "spam"); +} + +#[test] +fn pre_execution_define_command() { + let inp = &[ + &pre_execution_hook_code(r#"'def foo [] { "got foo!" }'"#), + "", + "foo", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "got foo!"); +} + +#[test] +fn pre_execution_simple_block_preserve_env_var() { + let inp = &[ + &pre_execution_hook(r#"{ let-env SPAM = "spam" }"#), + "", + "$env.SPAM", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "spam"); +} + +#[test] +fn pre_execution_simple_block_list_shadow_env_var() { + let inp = &[ + &pre_execution_hook( + r#"[ + { let-env SPAM = "foo" } + { let-env SPAM = "spam" } + ]"#, + ), + "", + "$env.SPAM", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "spam"); +} + +#[test] +fn pre_execution_block_preserve_env_var() { + let inp = &[ + &pre_execution_hook_code(r#"{ let-env SPAM = "spam" }"#), + "", + "$env.SPAM", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "spam"); +} + +#[test] +fn env_change_shadow_command() { + let inp = &[ + &env_change_hook_code_list( + "FOO", + &[ + r#"'def foo [] { "got spam!" }'"#, + r#"'def foo [] { "got foo!" }'"#, + ], + ), + "let-env FOO = 1", + "foo", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "got foo!"); +} + +#[test] +fn env_change_block_dont_preserve_command() { + let inp = &[ + &env_change_hook_code("FOO", r#"{ def foo [] { "foo" } }"#), + "let-env FOO = 1", + "foo", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + #[cfg(windows)] + assert!(actual_repl.out != "foo"); + #[cfg(not(windows))] + assert!(actual_repl.err.contains("ExternalCommand")); +} + +#[test] +fn env_change_block_condition_pwd() { + let inp = &[ + &env_change_hook_code_condition( + "PWD", + r#"{|before, after| ($after | path basename) == samples }"#, + r#"'source .nu-env'"#, + ), + "cd samples", + "$env.SPAM", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "spam"); +} + +#[test] +fn env_change_block_condition_correct_args() { + let inp = &[ + r#"let-env FOO = 1"#, + &env_change_hook_code_condition( + "FOO", + r#"{|before, after| $before == 1 and $after == 2}"#, + r#"{|before, after| let-env SPAM = ($before == 1 and $after == 2) }"#, + ), + "", + r#"let-env FOO = 2"#, + "$env.SPAM", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert_eq!(actual_repl.err, ""); + assert_eq!(actual_repl.out, "true"); +} + +#[test] +fn env_change_dont_panic_with_many_args() { + let inp = &[ + &env_change_hook_code("FOO", r#"{ |a, b, c| let-env SPAM = 'spam' }"#), + "let-env FOO = 1", + "", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert!(actual_repl.err.contains("IncompatibleParametersSingle")); + assert_eq!(actual_repl.out, ""); +} + +#[test] +fn err_hook_wrong_env_type_1() { + let inp = &[ + r#"let-env config = { + hooks: { + env_change: { + FOO : 1 + } + } + }"#, + "let-env FOO = 1", + "", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert!(actual_repl.err.contains("UnsupportedConfigValue")); + assert_eq!(actual_repl.out, ""); +} + +#[test] +fn err_hook_wrong_env_type_2() { + let inp = &[ + r#"let-env config = { + hooks: { + env_change: "print spam" + } + }"#, + "", + "", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert!(actual_repl.err.contains("TypeMismatch")); + assert_eq!(actual_repl.out, ""); +} + +#[test] +fn err_hook_wrong_env_type_3() { + let inp = &[ + r#"let-env config = { + hooks: { + env_change: { + FOO : { + code: 1 + } + } + } + }"#, + "let-env FOO = 1", + "", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert!(actual_repl.err.contains("UnsupportedConfigValue")); + assert_eq!(actual_repl.out, ""); +} + +#[test] +fn err_hook_non_boolean_condition_output() { + let inp = &[ + r#"let-env config = { + hooks: { + env_change: { + FOO : { + condition: { "foo" } + code: "print spam" + } + } + } + }"#, + "let-env FOO = 1", + "", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert!(actual_repl.err.contains("UnsupportedConfigValue")); + assert_eq!(actual_repl.out, ""); +} + +#[test] +fn err_hook_non_condition_not_a_block() { + let inp = &[ + r#"let-env config = { + hooks: { + env_change: { + FOO : { + condition: "foo" + code: "print spam" + } + } + } + }"#, + "let-env FOO = 1", + "", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert!(actual_repl.err.contains("UnsupportedConfigValue")); + assert_eq!(actual_repl.out, ""); +} + +#[test] +fn err_hook_parse_error() { + let inp = &[ + r#"let-env config = { + hooks: { + env_change: { + FOO : { + code: "def foo { 'foo' }" + } + } + } + }"#, + "let-env FOO = 1", + "", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert!(actual_repl.err.contains("UnsupportedConfigValue")); + assert_eq!(actual_repl.out, ""); +} + +#[test] +fn err_hook_dont_allow_string() { + let inp = &[ + &pre_prompt_hook(r#"'def foo [] { "got foo!" }'"#), + "", + "foo", + ]; + + let actual_repl = nu_repl("tests/hooks", inp); + + assert!(actual_repl.out.is_empty()); + assert!(actual_repl.err.contains("UnsupportedConfigValue")); +} diff --git a/tests/hooks/samples/.nu-env b/tests/hooks/samples/.nu-env new file mode 100644 index 0000000000..e9f11dca08 --- /dev/null +++ b/tests/hooks/samples/.nu-env @@ -0,0 +1 @@ +load-env { SPAM: "spam" } diff --git a/tests/main.rs b/tests/main.rs index ef20f920df..5630ea8968 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -1,5 +1,6 @@ extern crate nu_test_support; +mod hooks; mod nu_repl; mod overlays; mod parsing; diff --git a/tests/nu_repl/mod.rs b/tests/nu_repl/mod.rs index 7ab30b4ade..21b757a9ef 100644 --- a/tests/nu_repl/mod.rs +++ b/tests/nu_repl/mod.rs @@ -1,15 +1,23 @@ +use nu_cli::{eval_env_change_hook, eval_hook}; use nu_command::create_default_context; use nu_engine::eval_block; use nu_parser::parse; -use nu_protocol::engine::{Stack, StateDelta, StateWorkingSet}; -use nu_protocol::{PipelineData, Span, Value}; +use nu_protocol::engine::{EngineState, Stack, StateDelta, StateWorkingSet}; +use nu_protocol::{CliError, PipelineData, Span, Value}; use nu_test_support::fs::in_directory; use nu_test_support::Outcome; -fn outcome_err(msg: String) -> Outcome { +fn outcome_err( + engine_state: &EngineState, + error: &(dyn miette::Diagnostic + Send + Sync + 'static), +) -> Outcome { + let working_set = StateWorkingSet::new(&engine_state); + + eprintln!("{}", format!("Error: {:?}", CliError(error, &working_set))); + Outcome { out: String::new(), - err: msg, + err: format!("{:?}", error), } } @@ -36,12 +44,39 @@ pub fn nu_repl(cwd: &str, source_lines: &[&str]) -> Outcome { let delta = StateDelta::new(&engine_state); if let Err(err) = engine_state.merge_delta(delta, Some(&mut stack), cwd) { - return outcome_err(format!("{:?}", &err)); + return outcome_err(&engine_state, &err); } let mut last_output = String::new(); for (i, line) in source_lines.iter().enumerate() { + // Check for pre_prompt hook + let config = engine_state.get_config(); + if let Some(hook) = config.hooks.pre_prompt.clone() { + if let Err(err) = eval_hook(&mut engine_state, &mut stack, vec![], &hook) { + return outcome_err(&engine_state, &err); + } + } + + // Check for env change hook + let config = engine_state.get_config(); + if let Err(err) = eval_env_change_hook( + config.hooks.env_change.clone(), + &mut engine_state, + &mut stack, + ) { + return outcome_err(&engine_state, &err); + } + + // Check for pre_execution hook + let config = engine_state.get_config(); + if let Some(hook) = config.hooks.pre_execution.clone() { + if let Err(err) = eval_hook(&mut engine_state, &mut stack, vec![], &hook) { + return outcome_err(&engine_state, &err); + } + } + + // Eval the REPL line let (block, delta) = { let mut working_set = StateWorkingSet::new(&engine_state); let (block, err) = parse( @@ -53,7 +88,7 @@ pub fn nu_repl(cwd: &str, source_lines: &[&str]) -> Outcome { ); if let Some(err) = err { - return outcome_err(format!("{:?}", err)); + return outcome_err(&engine_state, &err); } (block, working_set.render()) }; @@ -61,12 +96,12 @@ pub fn nu_repl(cwd: &str, source_lines: &[&str]) -> Outcome { let cwd = match nu_engine::env::current_dir(&engine_state, &stack) { Ok(p) => p, Err(e) => { - return outcome_err(format!("{:?}", &e)); + return outcome_err(&engine_state, &e); } }; if let Err(err) = engine_state.merge_delta(delta, Some(&mut stack), &cwd) { - return outcome_err(format!("{:?}", err)); + return outcome_err(&engine_state, &err); } let input = PipelineData::new(Span::test_data()); @@ -75,17 +110,15 @@ pub fn nu_repl(cwd: &str, source_lines: &[&str]) -> Outcome { match eval_block(&engine_state, &mut stack, &block, input, false, false) { Ok(pipeline_data) => match pipeline_data.collect_string("", config) { Ok(s) => last_output = s, - Err(err) => return outcome_err(format!("{:?}", err)), + Err(err) => return outcome_err(&engine_state, &err), }, - Err(err) => return outcome_err(format!("{:?}", err)), + Err(err) => return outcome_err(&engine_state, &err), } - // FIXME: permanent state changes like this hopefully in time can be removed - // and be replaced by just passing the cwd in where needed if let Some(cwd) = stack.get_env_var(&engine_state, "PWD") { let path = match cwd.as_string() { Ok(p) => p, - Err(err) => return outcome_err(format!("{:?}", err)), + Err(err) => return outcome_err(&engine_state, &err), }; let _ = std::env::set_current_dir(path); engine_state.add_env_var("PWD".into(), cwd);