use itertools::Itertools; use nu_engine::{command_prelude::*, compile}; use nu_protocol::{ ast::Block, debugger::WithoutDebug, engine::StateWorkingSet, report_shell_error, Range, }; use std::{ sync::Arc, {collections::HashSet, ops::Bound}, }; pub fn check_example_input_and_output_types_match_command_signature( example: &Example, cwd: &std::path::Path, engine_state: &mut Box, signature_input_output_types: &[(Type, Type)], signature_operates_on_cell_paths: bool, ) -> HashSet<(Type, Type)> { let mut witnessed_type_transformations = HashSet::<(Type, Type)>::new(); // Skip tests that don't have results to compare to if let Some(example_output) = example.result.as_ref() { if let Some(example_input) = eval_pipeline_without_terminal_expression(example.example, cwd, engine_state) { let example_matches_signature = signature_input_output_types .iter() .any(|(sig_in_type, sig_out_type)| { example_input.is_subtype_of(sig_in_type) && example_output.is_subtype_of(sig_out_type) && { witnessed_type_transformations .insert((sig_in_type.clone(), sig_out_type.clone())); true } }); let example_input_type = example_input.get_type(); let example_output_type = example_output.get_type(); // The example type checks as a cell path operation if both: // 1. The command is declared to operate on cell paths. // 2. The example_input_type is list or record or table, and the example // output shape is the same as the input shape. let example_matches_signature_via_cell_path_operation = signature_operates_on_cell_paths && example_input_type.accepts_cell_paths() // TODO: This is too permissive; it should make use of the signature.input_output_types at least. && example_output_type.to_shape() == example_input_type.to_shape(); if !(example_matches_signature || example_matches_signature_via_cell_path_operation) { panic!( "The example `{}` demonstrates a transformation of type {:?} -> {:?}. \ However, this does not match the declared signature: {:?}.{} \ For this command `operates_on_cell_paths()` is {}.", example.example, example_input_type, example_output_type, signature_input_output_types, if signature_input_output_types.is_empty() { " (Did you forget to declare the input and output types for the command?)" } else { "" }, signature_operates_on_cell_paths ); }; }; } witnessed_type_transformations } pub fn eval_pipeline_without_terminal_expression( src: &str, cwd: &std::path::Path, engine_state: &mut Box, ) -> Option { let (mut block, mut working_set) = parse(src, engine_state); if block.pipelines.len() == 1 { let n_expressions = block.pipelines[0].elements.len(); // Modify the block to remove the last element and recompile it { let mut_block = Arc::make_mut(&mut block); mut_block.pipelines[0].elements.truncate(n_expressions - 1); mut_block.ir_block = Some(compile(&working_set, mut_block).expect( "failed to compile block modified by eval_pipeline_without_terminal_expression", )); } working_set.add_block(block.clone()); engine_state .merge_delta(working_set.render()) .expect("failed to merge delta"); if !block.pipelines[0].elements.is_empty() { let empty_input = PipelineData::empty(); Some(eval_block(block, empty_input, cwd, engine_state)) } else { Some(Value::nothing(Span::test_data())) } } else { // E.g. multiple semicolon-separated statements None } } pub fn parse<'engine>( contents: &str, engine_state: &'engine EngineState, ) -> (Arc, StateWorkingSet<'engine>) { let mut working_set = StateWorkingSet::new(engine_state); let output = nu_parser::parse(&mut working_set, None, contents.as_bytes(), false); if let Some(err) = working_set.parse_errors.first() { panic!("test parse error in `{contents}`: {err:?}"); } if let Some(err) = working_set.compile_errors.first() { panic!("test compile error in `{contents}`: {err:?}"); } (output, working_set) } pub fn eval_block( block: Arc, input: PipelineData, cwd: &std::path::Path, engine_state: &EngineState, ) -> Value { let mut stack = Stack::new().collect_value(); stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy())); nu_engine::eval_block::(engine_state, &mut stack, &block, input) .and_then(|data| data.into_value(Span::test_data())) .unwrap_or_else(|err| { report_shell_error(engine_state, &err); panic!("test eval error in `{}`: {:?}", "TODO", err) }) } pub fn check_example_evaluates_to_expected_output( cmd_name: &str, example: &Example, cwd: &std::path::Path, engine_state: &mut Box, ) { let mut stack = Stack::new().collect_value(); // Set up PWD stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy())); engine_state .merge_env(&mut stack) .expect("Error merging environment"); let empty_input = PipelineData::empty(); let result = eval(example.example, empty_input, cwd, engine_state); // Note. Value implements PartialEq for Bool, Int, Float, String and Block // If the command you are testing requires to compare another case, then // you need to define its equality in the Value struct if let Some(expected) = example.result.as_ref() { let expected = DebuggableValue(expected); let result = DebuggableValue(&result); assert_eq!( result, expected, "Error: The result of example '{}' for the command '{}' differs from the expected value.\n\nExpected: {:?}\nActual: {:?}\n", example.description, cmd_name, expected, result, ); } } pub fn check_all_signature_input_output_types_entries_have_examples( signature: Signature, witnessed_type_transformations: HashSet<(Type, Type)>, ) { let declared_type_transformations = HashSet::from_iter(signature.input_output_types); assert!( witnessed_type_transformations.is_subset(&declared_type_transformations), "This should not be possible (bug in test): the type transformations \ collected in the course of matching examples to the signature type map \ contain type transformations not present in the signature type map." ); if !signature.allow_variants_without_examples { assert_eq!( witnessed_type_transformations, declared_type_transformations, "There are entries in the signature type map which do not correspond to any example: \ {:?}", declared_type_transformations .difference(&witnessed_type_transformations) .map(|(s1, s2)| format!("{s1} -> {s2}")) .join(", ") ); } } fn eval( contents: &str, input: PipelineData, cwd: &std::path::Path, engine_state: &mut Box, ) -> Value { let (block, working_set) = parse(contents, engine_state); engine_state .merge_delta(working_set.render()) .expect("failed to merge delta"); eval_block(block, input, cwd, engine_state) } pub struct DebuggableValue<'a>(pub &'a Value); impl PartialEq for DebuggableValue<'_> { fn eq(&self, other: &Self) -> bool { self.0 == other.0 } } impl std::fmt::Debug for DebuggableValue<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.0 { Value::Bool { val, .. } => { write!(f, "{:?}", val) } Value::Int { val, .. } => { write!(f, "{:?}", val) } Value::Float { val, .. } => { write!(f, "{:?}f", val) } Value::Filesize { val, .. } => { write!(f, "Filesize({:?})", val) } Value::Duration { val, .. } => { let duration = std::time::Duration::from_nanos(*val as u64); write!(f, "Duration({:?})", duration) } Value::Date { val, .. } => { write!(f, "Date({:?})", val) } Value::Range { val, .. } => match **val { Range::IntRange(range) => match range.end() { Bound::Included(end) => write!( f, "Range({:?}..{:?}, step: {:?})", range.start(), end, range.step(), ), Bound::Excluded(end) => write!( f, "Range({:?}..<{:?}, step: {:?})", range.start(), end, range.step(), ), Bound::Unbounded => { write!(f, "Range({:?}.., step: {:?})", range.start(), range.step()) } }, Range::FloatRange(range) => match range.end() { Bound::Included(end) => write!( f, "Range({:?}..{:?}, step: {:?})", range.start(), end, range.step(), ), Bound::Excluded(end) => write!( f, "Range({:?}..<{:?}, step: {:?})", range.start(), end, range.step(), ), Bound::Unbounded => { write!(f, "Range({:?}.., step: {:?})", range.start(), range.step()) } }, }, Value::String { val, .. } | Value::Glob { val, .. } => { write!(f, "{:?}", val) } Value::Record { val, .. } => { write!(f, "{{")?; let mut first = true; for (col, value) in (&**val).into_iter() { if !first { write!(f, ", ")?; } first = false; write!(f, "{:?}: {:?}", col, DebuggableValue(value))?; } write!(f, "}}") } Value::List { vals, .. } => { write!(f, "[")?; for (i, value) in vals.iter().enumerate() { if i > 0 { write!(f, ", ")?; } write!(f, "{:?}", DebuggableValue(value))?; } write!(f, "]") } Value::Closure { val, .. } => { write!(f, "Closure({:?})", val) } Value::Nothing { .. } => { write!(f, "Nothing") } Value::Error { error, .. } => { write!(f, "Error({:?})", error) } Value::Binary { val, .. } => { write!(f, "Binary({:?})", val) } Value::CellPath { val, .. } => { write!(f, "CellPath({:?})", val.to_string()) } Value::Custom { val, .. } => { write!(f, "CustomValue({:?})", val) } } } }