forked from extern/nushell
cratification: Example support (#8231)
# Description When the crate nu_cmd_lang crate was created last week example_test.rs was copied over from nu_command to nu_cmd_lang. By doing this there was a set of methods in example_test.rs that existed in both crates... This PR removes the redundancy by moving all of those duplicated methods into the crate nu_test_support in a newly created file called example_support.rs _(Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes.)_ _(Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience.)_ # User-Facing Changes _(List of all changes that impact the user experience here. This helps us keep track of breaking changes.)_ # Tests + Formatting Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass # After Submitting If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date.
This commit is contained in:
parent
0a2e711351
commit
f8d2bff283
222
crates/nu-cmd-lang/src/example_support.rs
Normal file
222
crates/nu-cmd-lang/src/example_support.rs
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
use itertools::Itertools;
|
||||||
|
use nu_protocol::{
|
||||||
|
ast::Block,
|
||||||
|
engine::{EngineState, Stack, StateDelta, StateWorkingSet},
|
||||||
|
Example, PipelineData, Signature, Span, Type, Value,
|
||||||
|
};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
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: &Vec<(Type, Type)>,
|
||||||
|
signature_operates_on_cell_paths: bool,
|
||||||
|
signature_vectorizes_over_list: 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 vectorization over an input list if both:
|
||||||
|
// 1. The command is declared to vectorize over list input.
|
||||||
|
// 2. There exists an entry t -> u in the type map such that the
|
||||||
|
// example_input_type is a subtype of list<t> and the
|
||||||
|
// example_output_type is a subtype of list<u>.
|
||||||
|
let example_matches_signature_via_vectorization_over_list =
|
||||||
|
signature_vectorizes_over_list
|
||||||
|
&& match &example_input_type {
|
||||||
|
Type::List(ex_in_type) => {
|
||||||
|
match signature_input_output_types.iter().find_map(
|
||||||
|
|(sig_in_type, sig_out_type)| {
|
||||||
|
if ex_in_type.is_subtype(sig_in_type) {
|
||||||
|
Some((sig_in_type, sig_out_type))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Some((sig_in_type, sig_out_type)) => match &example_output_type {
|
||||||
|
Type::List(ex_out_type)
|
||||||
|
if ex_out_type.is_subtype(sig_out_type) =>
|
||||||
|
{
|
||||||
|
witnessed_type_transformations
|
||||||
|
.insert((sig_in_type.clone(), sig_out_type.clone()));
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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_vectorization_over_list
|
||||||
|
|| example_matches_signature_via_cell_path_operation)
|
||||||
|
{
|
||||||
|
panic!(
|
||||||
|
"The example `{}` demonstrates a transformation of type {:?} -> {:?}. \
|
||||||
|
However, this does not match the declared signature: {:?}.{} \
|
||||||
|
For this command, `vectorizes_over_list` is {} and `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_vectorizes_over_list,
|
||||||
|
signature_operates_on_cell_paths
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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, err) = nu_parser::parse(&mut working_set, None, contents.as_bytes(), false, &[]);
|
||||||
|
|
||||||
|
if let Some(err) = err {
|
||||||
|
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)
|
||||||
|
.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!(
|
||||||
|
&result, expected,
|
||||||
|
"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.into_iter());
|
||||||
|
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)
|
||||||
|
}
|
@ -8,13 +8,16 @@ pub fn test_examples(cmd: impl Command + 'static) {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_examples {
|
mod test_examples {
|
||||||
|
use crate::example_support::{
|
||||||
|
check_all_signature_input_output_types_entries_have_examples,
|
||||||
|
check_example_evaluates_to_expected_output,
|
||||||
|
check_example_input_and_output_types_match_command_signature,
|
||||||
|
};
|
||||||
use crate::{Break, Describe, Mut};
|
use crate::{Break, Describe, Mut};
|
||||||
use crate::{Echo, If, Let};
|
use crate::{Echo, If, Let};
|
||||||
use itertools::Itertools;
|
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::Block,
|
engine::{Command, EngineState, StateWorkingSet},
|
||||||
engine::{Command, EngineState, Stack, StateDelta, StateWorkingSet},
|
Type,
|
||||||
Example, PipelineData, Signature, Span, Type, Value,
|
|
||||||
};
|
};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
@ -75,224 +78,4 @@ mod test_examples {
|
|||||||
.expect("Error merging delta");
|
.expect("Error merging delta");
|
||||||
engine_state
|
engine_state
|
||||||
}
|
}
|
||||||
|
|
||||||
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: &Vec<(Type, Type)>,
|
|
||||||
signature_operates_on_cell_paths: bool,
|
|
||||||
signature_vectorizes_over_list: 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 vectorization over an input list if both:
|
|
||||||
// 1. The command is declared to vectorize over list input.
|
|
||||||
// 2. There exists an entry t -> u in the type map such that the
|
|
||||||
// example_input_type is a subtype of list<t> and the
|
|
||||||
// example_output_type is a subtype of list<u>.
|
|
||||||
let example_matches_signature_via_vectorization_over_list =
|
|
||||||
signature_vectorizes_over_list
|
|
||||||
&& match &example_input_type {
|
|
||||||
Type::List(ex_in_type) => {
|
|
||||||
match signature_input_output_types.iter().find_map(
|
|
||||||
|(sig_in_type, sig_out_type)| {
|
|
||||||
if ex_in_type.is_subtype(sig_in_type) {
|
|
||||||
Some((sig_in_type, sig_out_type))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Some((sig_in_type, sig_out_type)) => match &example_output_type
|
|
||||||
{
|
|
||||||
Type::List(ex_out_type)
|
|
||||||
if ex_out_type.is_subtype(sig_out_type) =>
|
|
||||||
{
|
|
||||||
witnessed_type_transformations.insert((
|
|
||||||
sig_in_type.clone(),
|
|
||||||
sig_out_type.clone(),
|
|
||||||
));
|
|
||||||
true
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
},
|
|
||||||
None => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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_vectorization_over_list
|
|
||||||
|| example_matches_signature_via_cell_path_operation)
|
|
||||||
{
|
|
||||||
panic!(
|
|
||||||
"The example `{}` demonstrates a transformation of type {:?} -> {:?}. \
|
|
||||||
However, this does not match the declared signature: {:?}.{} \
|
|
||||||
For this command, `vectorizes_over_list` is {} and `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_vectorizes_over_list,
|
|
||||||
signature_operates_on_cell_paths
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
witnessed_type_transformations
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
.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!(
|
|
||||||
&result, expected,
|
|
||||||
"The example result differs from the expected value",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.into_iter());
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse(contents: &str, engine_state: &EngineState) -> (Block, StateDelta) {
|
|
||||||
let mut working_set = StateWorkingSet::new(engine_state);
|
|
||||||
let (output, err) =
|
|
||||||
nu_parser::parse(&mut working_set, None, contents.as_bytes(), false, &[]);
|
|
||||||
|
|
||||||
if let Some(err) = err {
|
|
||||||
panic!("test parse error in `{contents}`: {err:?}")
|
|
||||||
}
|
|
||||||
|
|
||||||
(output, working_set.render())
|
|
||||||
}
|
|
||||||
|
|
||||||
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()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
mod core_commands;
|
mod core_commands;
|
||||||
mod default_context;
|
mod default_context;
|
||||||
|
pub mod example_support;
|
||||||
mod example_test;
|
mod example_test;
|
||||||
|
|
||||||
pub use core_commands::*;
|
pub use core_commands::*;
|
||||||
pub use default_context::*;
|
pub use default_context::*;
|
||||||
|
pub use example_support::*;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub use example_test::test_examples;
|
pub use example_test::test_examples;
|
||||||
|
@ -14,12 +14,15 @@ mod test_examples {
|
|||||||
StrJoin, StrLength, StrReplace, Update, Url, Values, Wrap,
|
StrJoin, StrLength, StrReplace, Update, Url, Values, Wrap,
|
||||||
};
|
};
|
||||||
use crate::{Each, To};
|
use crate::{Each, To};
|
||||||
use itertools::Itertools;
|
use nu_cmd_lang::example_support::{
|
||||||
|
check_all_signature_input_output_types_entries_have_examples,
|
||||||
|
check_example_evaluates_to_expected_output,
|
||||||
|
check_example_input_and_output_types_match_command_signature,
|
||||||
|
};
|
||||||
use nu_cmd_lang::{Break, Echo, If, Let, Mut};
|
use nu_cmd_lang::{Break, Echo, If, Let, Mut};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::Block,
|
engine::{Command, EngineState, StateWorkingSet},
|
||||||
engine::{Command, EngineState, Stack, StateDelta, StateWorkingSet},
|
Type,
|
||||||
Example, PipelineData, Signature, Span, Type, Value,
|
|
||||||
};
|
};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
@ -109,224 +112,4 @@ mod test_examples {
|
|||||||
.expect("Error merging delta");
|
.expect("Error merging delta");
|
||||||
engine_state
|
engine_state
|
||||||
}
|
}
|
||||||
|
|
||||||
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: &Vec<(Type, Type)>,
|
|
||||||
signature_operates_on_cell_paths: bool,
|
|
||||||
signature_vectorizes_over_list: 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 vectorization over an input list if both:
|
|
||||||
// 1. The command is declared to vectorize over list input.
|
|
||||||
// 2. There exists an entry t -> u in the type map such that the
|
|
||||||
// example_input_type is a subtype of list<t> and the
|
|
||||||
// example_output_type is a subtype of list<u>.
|
|
||||||
let example_matches_signature_via_vectorization_over_list =
|
|
||||||
signature_vectorizes_over_list
|
|
||||||
&& match &example_input_type {
|
|
||||||
Type::List(ex_in_type) => {
|
|
||||||
match signature_input_output_types.iter().find_map(
|
|
||||||
|(sig_in_type, sig_out_type)| {
|
|
||||||
if ex_in_type.is_subtype(sig_in_type) {
|
|
||||||
Some((sig_in_type, sig_out_type))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Some((sig_in_type, sig_out_type)) => match &example_output_type
|
|
||||||
{
|
|
||||||
Type::List(ex_out_type)
|
|
||||||
if ex_out_type.is_subtype(sig_out_type) =>
|
|
||||||
{
|
|
||||||
witnessed_type_transformations.insert((
|
|
||||||
sig_in_type.clone(),
|
|
||||||
sig_out_type.clone(),
|
|
||||||
));
|
|
||||||
true
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
},
|
|
||||||
None => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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_vectorization_over_list
|
|
||||||
|| example_matches_signature_via_cell_path_operation)
|
|
||||||
{
|
|
||||||
panic!(
|
|
||||||
"The example `{}` demonstrates a transformation of type {:?} -> {:?}. \
|
|
||||||
However, this does not match the declared signature: {:?}.{} \
|
|
||||||
For this command, `vectorizes_over_list` is {} and `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_vectorizes_over_list,
|
|
||||||
signature_operates_on_cell_paths
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
witnessed_type_transformations
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
.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!(
|
|
||||||
&result, expected,
|
|
||||||
"The example result differs from the expected value",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.into_iter());
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse(contents: &str, engine_state: &EngineState) -> (Block, StateDelta) {
|
|
||||||
let mut working_set = StateWorkingSet::new(engine_state);
|
|
||||||
let (output, err) =
|
|
||||||
nu_parser::parse(&mut working_set, None, contents.as_bytes(), false, &[]);
|
|
||||||
|
|
||||||
if let Some(err) = err {
|
|
||||||
panic!("test parse error in `{contents}`: {err:?}")
|
|
||||||
}
|
|
||||||
|
|
||||||
(output, working_set.render())
|
|
||||||
}
|
|
||||||
|
|
||||||
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()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user