nu_cmd_lang/
example_support.rs

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