Skip to main content

nu_cmd_lang/
example_support.rs

1use itertools::Itertools;
2use nu_engine::{command_prelude::*, compile};
3use nu_protocol::{
4    CompareTypes, Range, ast::Block, debugger::WithoutDebug, engine::StateWorkingSet,
5    report_shell_error,
6};
7use std::{
8    sync::Arc,
9    {collections::HashSet, ops::Bound},
10};
11
12pub fn check_example_input_and_output_types_match_command_signature(
13    example: &Example,
14    cwd: &std::path::Path,
15    engine_state: &mut Box<EngineState>,
16    signature_input_output_types: &[(Type, Type)],
17    signature_operates_on_cell_paths: bool,
18) -> HashSet<(Type, Type)> {
19    let mut witnessed_type_transformations = HashSet::<(Type, Type)>::new();
20
21    // Skip tests that don't have results to compare to
22    if let Some(example_output) = example.result.as_ref()
23        && let Some(example_input) =
24            eval_pipeline_without_terminal_expression(example.example, cwd, engine_state)
25    {
26        let example_matches_signature =
27            signature_input_output_types
28                .iter()
29                .any(|(sig_in_type, sig_out_type)| {
30                    example_input.is_subtype_of(sig_in_type)
31                        && example_output.is_subtype_of(sig_out_type)
32                        && {
33                            witnessed_type_transformations
34                                .insert((sig_in_type.clone(), sig_out_type.clone()));
35                            true
36                        }
37                });
38
39        let example_input_type = example_input.get_type();
40        let example_output_type = example_output.get_type();
41
42        // The example type checks as a cell path operation if both:
43        // 1. The command is declared to operate on cell paths.
44        // 2. The example_input_type is list or record or table, and the example
45        //    output shape is the same as the input shape.
46        let example_matches_signature_via_cell_path_operation = signature_operates_on_cell_paths
47                       && example_input_type.accepts_cell_paths()
48                       // TODO: This is too permissive; it should make use of the signature.input_output_types at least.
49                       && example_output_type.to_shape() == example_input_type.to_shape();
50
51        if !(example_matches_signature || example_matches_signature_via_cell_path_operation) {
52            panic!(
53                "The example `{}` demonstrates a transformation of type {:?} -> {:?}. \
54                       However, this does not match the declared signature: {:?}.{} \
55                       For this command `operates_on_cell_paths()` is {}.",
56                example.example,
57                example_input_type,
58                example_output_type,
59                signature_input_output_types,
60                if signature_input_output_types.is_empty() {
61                    " (Did you forget to declare the input and output types for the command?)"
62                } else {
63                    ""
64                },
65                signature_operates_on_cell_paths
66            );
67        };
68    };
69    witnessed_type_transformations
70}
71
72pub fn eval_pipeline_without_terminal_expression(
73    src: &str,
74    cwd: &std::path::Path,
75    engine_state: &mut Box<EngineState>,
76) -> Option<Value> {
77    let (mut block, mut working_set) = parse(src, engine_state);
78    if block.pipelines.len() == 1 {
79        let n_expressions = block.pipelines[0].elements.len();
80        // Modify the block to remove the last element and recompile it
81        {
82            let mut_block = Arc::make_mut(&mut block);
83            mut_block.pipelines[0].elements.truncate(n_expressions - 1);
84            mut_block.ir_block = Some(compile(&working_set, mut_block).expect(
85                "failed to compile block modified by eval_pipeline_without_terminal_expression",
86            ));
87        }
88        working_set.add_block(block.clone());
89        engine_state
90            .merge_delta(working_set.render())
91            .expect("failed to merge delta");
92
93        if !block.pipelines[0].elements.is_empty() {
94            let empty_input = PipelineData::empty();
95            Some(eval_block(block, empty_input, cwd, engine_state))
96        } else {
97            Some(Value::nothing(Span::test_data()))
98        }
99    } else {
100        // E.g. multiple semicolon-separated statements
101        None
102    }
103}
104
105pub fn parse<'engine>(
106    contents: &str,
107    engine_state: &'engine EngineState,
108) -> (Arc<Block>, StateWorkingSet<'engine>) {
109    let mut working_set = StateWorkingSet::new(engine_state);
110    let output = nu_parser::parse(&mut working_set, None, contents.as_bytes(), false);
111
112    if let Some(err) = working_set.parse_errors.first() {
113        panic!("test parse error in `{contents}`: {err:?}");
114    }
115
116    if let Some(err) = working_set.compile_errors.first() {
117        panic!("test compile error in `{contents}`: {err:?}");
118    }
119
120    (output, working_set)
121}
122
123pub fn eval_block(
124    block: Arc<Block>,
125    input: PipelineData,
126    cwd: &std::path::Path,
127    engine_state: &EngineState,
128) -> Value {
129    let mut stack = Stack::new().collect_value();
130
131    stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy()));
132
133    nu_engine::eval_block::<WithoutDebug>(engine_state, &mut stack, &block, input)
134        .map(|p| p.body)
135        .and_then(|data| data.into_value(Span::test_data()))
136        .unwrap_or_else(|err| {
137            report_shell_error(None, engine_state, &err);
138            panic!("test eval error in `{}`: {:?}", "TODO", err)
139        })
140}
141
142pub fn check_example_evaluates_to_expected_output(
143    cmd_name: &str,
144    example: &Example,
145    cwd: &std::path::Path,
146    engine_state: &mut Box<EngineState>,
147) {
148    let mut stack = Stack::new().collect_value();
149
150    // Set up PWD
151    stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy()));
152
153    engine_state
154        .merge_env(&mut stack)
155        .expect("Error merging environment");
156
157    let empty_input = PipelineData::empty();
158    let result = eval(example.example, empty_input, cwd, engine_state);
159
160    // Note. Value implements PartialEq for Bool, Int, Float, String and Block
161    // If the command you are testing requires to compare another case, then
162    // you need to define its equality in the Value struct
163    if let Some(expected) = example.result.as_ref() {
164        let expected = DebuggableValue(expected);
165        let result = DebuggableValue(&result);
166        assert_eq!(
167            result, expected,
168            "Error: The result of example '{}' for the command '{}' differs from the expected value.\n\nExpected: {:?}\nActual:   {:?}\n",
169            example.description, cmd_name, expected, result,
170        );
171    }
172}
173
174pub fn check_all_signature_input_output_types_entries_have_examples(
175    signature: Signature,
176    witnessed_type_transformations: HashSet<(Type, Type)>,
177) {
178    let declared_type_transformations = HashSet::from_iter(signature.input_output_types);
179    assert!(
180        witnessed_type_transformations.is_subset(&declared_type_transformations),
181        "This should not be possible (bug in test): the type transformations \
182        collected in the course of matching examples to the signature type map \
183        contain type transformations not present in the signature type map."
184    );
185
186    if !signature.allow_variants_without_examples {
187        assert_eq!(
188            witnessed_type_transformations,
189            declared_type_transformations,
190            "There are entries in the signature type map which do not correspond to any example: \
191            {:?}",
192            declared_type_transformations
193                .difference(&witnessed_type_transformations)
194                .map(|(s1, s2)| format!("{s1} -> {s2}"))
195                .join(", ")
196        );
197    }
198}
199
200fn eval(
201    contents: &str,
202    input: PipelineData,
203    cwd: &std::path::Path,
204    engine_state: &mut Box<EngineState>,
205) -> Value {
206    let (block, working_set) = parse(contents, engine_state);
207    engine_state
208        .merge_delta(working_set.render())
209        .expect("failed to merge delta");
210    eval_block(block, input, cwd, engine_state)
211}
212
213pub struct DebuggableValue<'a>(pub &'a Value);
214
215impl PartialEq for DebuggableValue<'_> {
216    fn eq(&self, other: &Self) -> bool {
217        self.0 == other.0
218    }
219}
220
221impl std::fmt::Debug for DebuggableValue<'_> {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        match self.0 {
224            Value::Bool { val, .. } => {
225                write!(f, "{val:?}")
226            }
227            Value::Int { val, .. } => {
228                write!(f, "{val:?}")
229            }
230            Value::Float { val, .. } => {
231                write!(f, "{val:?}f")
232            }
233            Value::Filesize { val, .. } => {
234                write!(f, "Filesize({val:?})")
235            }
236            Value::Duration { val, .. } => {
237                let duration = std::time::Duration::from_nanos(*val as u64);
238                write!(f, "Duration({duration:?})")
239            }
240            Value::Date { val, .. } => {
241                write!(f, "Date({val:?})")
242            }
243            Value::Range { val, .. } => match **val {
244                Range::IntRange(range) => match range.end() {
245                    Bound::Included(end) => write!(
246                        f,
247                        "Range({:?}..{:?}, step: {:?})",
248                        range.start(),
249                        end,
250                        range.step(),
251                    ),
252                    Bound::Excluded(end) => write!(
253                        f,
254                        "Range({:?}..<{:?}, step: {:?})",
255                        range.start(),
256                        end,
257                        range.step(),
258                    ),
259                    Bound::Unbounded => {
260                        write!(f, "Range({:?}.., step: {:?})", range.start(), range.step())
261                    }
262                },
263                Range::FloatRange(range) => match range.end() {
264                    Bound::Included(end) => write!(
265                        f,
266                        "Range({:?}..{:?}, step: {:?})",
267                        range.start(),
268                        end,
269                        range.step(),
270                    ),
271                    Bound::Excluded(end) => write!(
272                        f,
273                        "Range({:?}..<{:?}, step: {:?})",
274                        range.start(),
275                        end,
276                        range.step(),
277                    ),
278                    Bound::Unbounded => {
279                        write!(f, "Range({:?}.., step: {:?})", range.start(), range.step())
280                    }
281                },
282            },
283            Value::String { val, .. } | Value::Glob { val, .. } => {
284                write!(f, "{val:?}")
285            }
286            Value::Record { val, .. } => {
287                write!(f, "{{")?;
288                let mut first = true;
289                for (col, value) in (&**val).into_iter() {
290                    if !first {
291                        write!(f, ", ")?;
292                    }
293                    first = false;
294                    write!(f, "{:?}: {:?}", col, DebuggableValue(value))?;
295                }
296                write!(f, "}}")
297            }
298            Value::List { vals, .. } => {
299                write!(f, "[")?;
300                for (i, value) in vals.iter().enumerate() {
301                    if i > 0 {
302                        write!(f, ", ")?;
303                    }
304                    write!(f, "{:?}", DebuggableValue(value))?;
305                }
306                write!(f, "]")
307            }
308            Value::Closure { val, .. } => {
309                write!(f, "Closure({val:?})")
310            }
311            Value::Nothing { .. } => {
312                write!(f, "Nothing")
313            }
314            Value::Error { error, .. } => {
315                write!(f, "Error({error:?})")
316            }
317            Value::Binary { val, .. } => {
318                write!(f, "Binary({val:?})")
319            }
320            Value::CellPath { val, .. } => {
321                write!(f, "CellPath({:?})", val.to_string())
322            }
323            Value::Custom { val, .. } => {
324                write!(f, "CustomValue({val:?})")
325            }
326        }
327    }
328}