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