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 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 let example_matches_signature_via_cell_path_operation = signature_operates_on_cell_paths
47 && example_input_type.accepts_cell_paths()
48 && 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 {
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 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 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 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}