nushell/crates/nu-cmd-lang/src/example_support.rs

284 lines
10 KiB
Rust
Raw Normal View History

2023-02-27 21:58:56 +00:00
use itertools::Itertools;
use nu_protocol::{
ast::{Block, RangeInclusion},
2023-02-27 21:58:56 +00:00
engine::{EngineState, Stack, StateDelta, StateWorkingSet},
Example, PipelineData, Signature, Span, Type, Value,
};
use std::collections::HashSet;
2023-02-27 21:58:56 +00:00
pub fn check_example_input_and_output_types_match_command_signature(
example: &Example,
cwd: &std::path::Path,
engine_state: &mut Box<EngineState>,
signature_input_output_types: &[(Type, Type)],
2023-02-27 21:58:56 +00:00
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_type) =
eval_pipeline_without_terminal_expression(example.example, cwd, engine_state)
{
let example_input_type = example_input_type.get_type();
let example_output_type = example_output.get_type();
let example_matches_signature =
signature_input_output_types
.iter()
.any(|(sig_in_type, sig_out_type)| {
example_input_type.is_subtype(sig_in_type)
&& example_output_type.is_subtype(sig_out_type)
&& {
witnessed_type_transformations
.insert((sig_in_type.clone(), sig_out_type.clone()));
true
}
});
// 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) {
2023-02-27 21:58:56 +00:00
panic!(
"The example `{}` demonstrates a transformation of type {:?} -> {:?}. \
2023-02-27 21:58:56 +00:00
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
);
2023-02-27 21:58:56 +00:00
};
};
}
witnessed_type_transformations
}
fn eval_pipeline_without_terminal_expression(
src: &str,
cwd: &std::path::Path,
engine_state: &mut Box<EngineState>,
) -> Option<Value> {
let (mut block, delta) = parse(src, engine_state);
if block.pipelines.len() == 1 {
let n_expressions = block.pipelines[0].elements.len();
block.pipelines[0].elements.truncate(&n_expressions - 1);
if !block.pipelines[0].elements.is_empty() {
let empty_input = PipelineData::empty();
Some(eval_block(block, empty_input, cwd, engine_state, delta))
} else {
Some(Value::nothing(Span::test_data()))
}
} else {
// E.g. multiple semicolon-separated statements
None
}
}
pub fn parse(contents: &str, engine_state: &EngineState) -> (Block, StateDelta) {
let mut working_set = StateWorkingSet::new(engine_state);
let output = nu_parser::parse(&mut working_set, None, contents.as_bytes(), false);
2023-02-27 21:58:56 +00:00
if let Some(err) = working_set.parse_errors.first() {
2023-02-27 21:58:56 +00:00
panic!("test parse error in `{contents}`: {err:?}")
}
(output, working_set.render())
}
pub fn eval_block(
block: Block,
input: PipelineData,
cwd: &std::path::Path,
engine_state: &mut Box<EngineState>,
delta: StateDelta,
) -> Value {
engine_state
.merge_delta(delta)
.expect("Error merging delta");
let mut stack = Stack::new();
stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy()));
match nu_engine::eval_block(engine_state, &mut stack, &block, input, true, true) {
Err(err) => panic!("test eval error in `{}`: {:?}", "TODO", err),
Ok(result) => result.into_value(Span::test_data()),
}
}
pub fn check_example_evaluates_to_expected_output(
example: &Example,
cwd: &std::path::Path,
engine_state: &mut Box<EngineState>,
) {
let mut stack = Stack::new();
// Set up PWD
stack.add_env_var("PWD".to_string(), Value::test_string(cwd.to_string_lossy()));
engine_state
.merge_env(&mut stack, cwd)
2023-02-27 21:58:56 +00:00
.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() {
assert_eq!(
DebuggableValue(&result),
DebuggableValue(expected),
2023-02-27 21:58:56 +00:00
"The example result differs from the expected value",
)
}
}
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);
2023-02-27 21:58:56 +00:00
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<EngineState>,
) -> Value {
let (block, delta) = parse(contents, engine_state);
eval_block(block, input, cwd, engine_state, delta)
}
pub struct DebuggableValue<'a>(pub &'a Value);
impl PartialEq for DebuggableValue<'_> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<'a> std::fmt::Debug for DebuggableValue<'a> {
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.inclusion {
RangeInclusion::Inclusive => write!(
f,
"Range({:?}..{:?}, step: {:?})",
val.from, val.to, val.incr
),
RangeInclusion::RightExclusive => write!(
f,
"Range({:?}..<{:?}, step: {:?})",
val.from, val.to, val.incr
),
},
do not attempt to glob expand if the file path is wrapped in quotes (#11569) # Description Fixes: #11455 ### For arguments which is annotated with `:path/:directory/:glob` To fix the issue, we need to have a way to know if a path is originally quoted during runtime. So the information needed to be added at several levels: * parse time (from user input to expression) We need to add quoted information into `Expr::Filepath`, `Expr::Directory`, `Expr::GlobPattern` * eval time When convert from `Expr::Filepath`, `Expr::Directory`, `Expr::GlobPattern` to `Value::String` during runtime, we won't auto expanded the path if it's quoted ### For `ls` It's really special, because it accepts a `String` as a pattern, and it generates `glob` expression inside the command itself. So the idea behind the change is introducing a special SyntaxShape to ls: `SyntaxShape::LsGlobPattern`. So we can track if the pattern is originally quoted easier, and we don't auto expand the path either. Then when constructing a glob pattern inside ls, we check if input pattern is quoted, if so: we escape the input pattern, so we can run `ls a[123]b`, because it's already escaped. Finally, to accomplish the checking process, we also need to introduce a new value type called `Value::QuotedString` to differ from `Value::String`, it's used to generate an enum called `NuPath`, which is finally used in `ls` function. `ls` learned from `NuPath` to know if user input is quoted. # User-Facing Changes Actually it contains several changes ### For arguments which is annotated with `:path/:directory/:glob` #### Before ```nushell > def foo [p: path] { echo $p }; print (foo "~/a"); print (foo '~/a') /home/windsoilder/a /home/windsoilder/a > def foo [p: directory] { echo $p }; print (foo "~/a"); print (foo '~/a') /home/windsoilder/a /home/windsoilder/a > def foo [p: glob] { echo $p }; print (foo "~/a"); print (foo '~/a') /home/windsoilder/a /home/windsoilder/a ``` #### After ```nushell > def foo [p: path] { echo $p }; print (foo "~/a"); print (foo '~/a') ~/a ~/a > def foo [p: directory] { echo $p }; print (foo "~/a"); print (foo '~/a') ~/a ~/a > def foo [p: glob] { echo $p }; print (foo "~/a"); print (foo '~/a') ~/a ~/a ``` ### For ls command `touch '[uwu]'` #### Before ``` ❯ ls -D "[uwu]" Error: × No matches found for [uwu] ╭─[entry #6:1:1] 1 │ ls -D "[uwu]" · ───┬─── · ╰── Pattern, file or folder not found ╰──── help: no matches found ``` #### After ``` ❯ ls -D "[uwu]" ╭───┬───────┬──────┬──────┬──────────╮ │ # │ name │ type │ size │ modified │ ├───┼───────┼──────┼──────┼──────────┤ │ 0 │ [uwu] │ file │ 0 B │ now │ ╰───┴───────┴──────┴──────┴──────────╯ ``` # Tests + Formatting Done # After Submitting NaN
2024-01-21 15:22:25 +00:00
Value::String { val, .. } | Value::QuotedString { 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::Block { val, .. } => {
write!(f, "Block({:?})", val)
}
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::CustomValue { val, .. } => {
write!(f, "CustomValue({:?})", val)
}
Value::LazyRecord { val, .. } => {
let rec = val.collect().map_err(|_| std::fmt::Error)?;
write!(f, "LazyRecord({:?})", DebuggableValue(&rec))
}
}
}
}