From e3e4ae05910582fc29cdeeb773b7cbcd285204b3 Mon Sep 17 00:00:00 2001 From: Fernando Herrera Date: Sat, 9 Oct 2021 14:10:10 +0100 Subject: [PATCH 1/3] example unit test --- Cargo.lock | 1 + crates/nu-command/Cargo.toml | 1 + crates/nu-command/src/example_test.rs | 74 +++++++++++++++++++ crates/nu-command/src/filters/each.rs | 40 +++++++++- crates/nu-command/src/filters/for_.rs | 49 +++++++----- crates/nu-command/src/formats/from/json.rs | 58 ++++++++++++++- crates/nu-command/src/lib.rs | 4 +- crates/nu-command/src/strings/build_string.rs | 35 ++++++++- crates/nu-protocol/src/example.rs | 2 +- crates/nu-protocol/src/value/mod.rs | 46 ++++++++++++ 10 files changed, 285 insertions(+), 25 deletions(-) create mode 100644 crates/nu-command/src/example_test.rs diff --git a/Cargo.lock b/Cargo.lock index 5ff65ec7db..2edb362f39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -520,6 +520,7 @@ dependencies = [ "glob", "nu-engine", "nu-json", + "nu-parser", "nu-path", "nu-protocol", "nu-table", diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index d840fb7d33..6186de10d2 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -11,6 +11,7 @@ nu-json = { path = "../nu-json" } nu-path = { path = "../nu-path" } nu-protocol = { path = "../nu-protocol" } nu-table = { path = "../nu-table" } +nu-parser = { path = "../nu-parser" } # Potential dependencies for extras glob = "0.3.0" diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs new file mode 100644 index 0000000000..9d546407a1 --- /dev/null +++ b/crates/nu-command/src/example_test.rs @@ -0,0 +1,74 @@ +use std::{cell::RefCell, rc::Rc}; + +use nu_engine::eval_block; +use nu_parser::parse; +use nu_protocol::{ + engine::{Command, EngineState, EvaluationContext, StateWorkingSet}, + Value, +}; + +use super::From; + +pub fn test_examples(cmd: impl Command + 'static) { + let examples = cmd.examples(); + let engine_state = Rc::new(RefCell::new(EngineState::new())); + + let delta = { + // Base functions that are needed for testing + // Try to keep this working set as small to keep tests running as fast as possible + let engine_state = engine_state.borrow(); + let mut working_set = StateWorkingSet::new(&*engine_state); + working_set.add_decl(Box::new(From)); + + // Adding the command that is being tested to the working set + working_set.add_decl(Box::new(cmd)); + + working_set.render() + }; + + EngineState::merge_delta(&mut *engine_state.borrow_mut(), delta); + + for example in examples { + let start = std::time::Instant::now(); + + let (block, delta) = { + let engine_state = engine_state.borrow(); + let mut working_set = StateWorkingSet::new(&*engine_state); + let (output, err) = parse(&mut working_set, None, example.example.as_bytes(), false); + + if let Some(err) = err { + panic!("test parse error: {:?}", err) + } + + (output, working_set.render()) + }; + + EngineState::merge_delta(&mut *engine_state.borrow_mut(), delta); + + let state = EvaluationContext { + engine_state: engine_state.clone(), + stack: nu_protocol::engine::Stack::new(), + }; + + match eval_block(&state, &block, Value::nothing()) { + Err(err) => panic!("test eval error: {:?}", err), + Ok(result) => { + println!("input: {}", example.example); + println!("result: {:?}", result); + println!("done: {:?}", start.elapsed()); + + // 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 { + if result != expected { + panic!( + "the example result is different to expected value: {:?} != {:?}", + result, expected + ) + } + } + } + } + } +} diff --git a/crates/nu-command/src/filters/each.rs b/crates/nu-command/src/filters/each.rs index e48cfb719a..ce847b9174 100644 --- a/crates/nu-command/src/filters/each.rs +++ b/crates/nu-command/src/filters/each.rs @@ -1,7 +1,7 @@ use nu_engine::eval_block; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EvaluationContext}; -use nu_protocol::{IntoValueStream, Signature, SyntaxShape, Value}; +use nu_protocol::{Example, IntoValueStream, Signature, Span, SyntaxShape, Value}; pub struct Each; @@ -24,6 +24,32 @@ impl Command for Each { .switch("numbered", "iterate with an index", Some('n')) } + fn examples(&self) -> Vec { + let stream_test_1 = vec![ + Value::Int { + val: 2, + span: Span::unknown(), + }, + Value::Int { + val: 4, + span: Span::unknown(), + }, + Value::Int { + val: 6, + span: Span::unknown(), + }, + ]; + + vec![Example { + example: "[1 2 3] | each { 2 * $it }", + description: "Multiplies elements in list", + result: Some(Value::Stream { + stream: stream_test_1.into_iter().into_value_stream(), + span: Span::unknown(), + }), + }] + } + fn run( &self, context: &EvaluationContext, @@ -225,3 +251,15 @@ impl Command for Each { } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Each {}) + } +} diff --git a/crates/nu-command/src/filters/for_.rs b/crates/nu-command/src/filters/for_.rs index a3dfa56522..ddb69e0f96 100644 --- a/crates/nu-command/src/filters/for_.rs +++ b/crates/nu-command/src/filters/for_.rs @@ -98,34 +98,43 @@ impl Command for For { Example { description: "Echo the square of each integer", example: "for x in [1 2 3] { $x * $x }", - result: Some(vec![ - Value::Int { val: 1, span }, - Value::Int { val: 4, span }, - Value::Int { val: 9, span }, - ]), + result: Some(Value::List { + vals: vec![ + Value::Int { val: 1, span }, + Value::Int { val: 4, span }, + Value::Int { val: 9, span }, + ], + span: Span::unknown(), + }), }, Example { description: "Work with elements of a range", example: "for $x in 1..3 { $x }", - result: Some(vec![ - Value::Int { val: 1, span }, - Value::Int { val: 2, span }, - Value::Int { val: 3, span }, - ]), + result: Some(Value::List { + vals: vec![ + Value::Int { val: 1, span }, + Value::Int { val: 2, span }, + Value::Int { val: 3, span }, + ], + span: Span::unknown(), + }), }, Example { description: "Number each item and echo a message", example: "for $it in ['bob' 'fred'] --numbered { $\"($it.index) is ($it.item)\" }", - result: Some(vec![ - Value::String { - val: "0 is bob".into(), - span, - }, - Value::String { - val: "0 is fred".into(), - span, - }, - ]), + result: Some(Value::List { + vals: vec![ + Value::String { + val: "0 is bob".into(), + span, + }, + Value::String { + val: "0 is fred".into(), + span, + }, + ], + span: Span::unknown(), + }), }, ] } diff --git a/crates/nu-command/src/formats/from/json.rs b/crates/nu-command/src/formats/from/json.rs index fa206131f7..f7fe0d5f94 100644 --- a/crates/nu-command/src/formats/from/json.rs +++ b/crates/nu-command/src/formats/from/json.rs @@ -1,6 +1,6 @@ use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EvaluationContext}; -use nu_protocol::{IntoValueStream, ShellError, Signature, Span, Value}; +use nu_protocol::{Example, IntoValueStream, ShellError, Signature, Span, Value}; pub struct FromJson; @@ -21,6 +21,50 @@ impl Command for FromJson { ) } + fn examples(&self) -> Vec { + vec![ + Example { + example: "'{ a:1 }' | from json", + description: "Converts json formatted string to table", + result: Some(Value::Record { + cols: vec!["a".to_string()], + vals: vec![Value::Int { + val: 1, + span: Span::unknown(), + }], + span: Span::unknown(), + }), + }, + Example { + example: "'{ a:1, b: [1, 2] }' | from json", + description: "Converts json formatted string to table", + result: Some(Value::Record { + cols: vec!["a".to_string(), "b".to_string()], + vals: vec![ + Value::Int { + val: 1, + span: Span::unknown(), + }, + Value::List { + vals: vec![ + Value::Int { + val: 1, + span: Span::unknown(), + }, + Value::Int { + val: 2, + span: Span::unknown(), + }, + ], + span: Span::unknown(), + }, + ], + span: Span::unknown(), + }), + }, + ] + } + fn run( &self, _context: &EvaluationContext, @@ -109,3 +153,15 @@ fn convert_string_to_value(string_input: String, span: Span) -> Result Vec { + vec![ + Example { + example: "build-string a b c", + description: "Builds a string from letters a b c", + result: Some(Value::String { + val: "abc".to_string(), + span: Span::unknown(), + }), + }, + Example { + example: "build-string (1 + 2) = one ' ' plus ' ' two", + description: "Builds a string from letters a b c", + result: Some(Value::String { + val: "3=one plus two".to_string(), + span: Span::unknown(), + }), + }, + ] + } + fn run( &self, context: &EvaluationContext, @@ -36,3 +57,15 @@ impl Command for BuildString { }) } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(BuildString {}) + } +} diff --git a/crates/nu-protocol/src/example.rs b/crates/nu-protocol/src/example.rs index 894b4b2876..1abaca1774 100644 --- a/crates/nu-protocol/src/example.rs +++ b/crates/nu-protocol/src/example.rs @@ -3,5 +3,5 @@ use crate::Value; pub struct Example { pub example: &'static str, pub description: &'static str, - pub result: Option>, + pub result: Option, } diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index afc1f90161..86ec4ae581 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -374,6 +374,52 @@ impl PartialEq for Value { (Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => lhs == rhs, (Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => lhs == rhs, (Value::Block { val: b1, .. }, Value::Block { val: b2, .. }) => b1 == b2, + (Value::List { vals: vals_lhs, .. }, Value::List { vals: vals_rhs, .. }) => { + for (lhs, rhs) in vals_lhs.iter().zip(vals_rhs) { + if lhs != rhs { + return false; + } + } + + true + } + ( + Value::Record { + cols: cols_lhs, + vals: vals_lhs, + .. + }, + Value::Record { + cols: cols_rhs, + vals: vals_rhs, + .. + }, + ) => { + if cols_lhs != cols_rhs { + return false; + } + + for (lhs, rhs) in vals_lhs.iter().zip(vals_rhs) { + if lhs != rhs { + return false; + } + } + + true + } + ( + Value::Stream { + stream: stream_lhs, .. + }, + Value::Stream { + stream: stream_rhs, .. + }, + ) => { + let vals_lhs = stream_lhs.clone().collect_string(); + let vals_rhs = stream_rhs.clone().collect_string(); + + vals_lhs == vals_rhs + } _ => false, } } From 8756e88e3c9a5eb53d5c6c8f8536fd9acc4205eb Mon Sep 17 00:00:00 2001 From: Fernando Herrera Date: Sat, 9 Oct 2021 14:28:09 +0100 Subject: [PATCH 2/3] command split --- crates/nu-command/src/example_test.rs | 5 +- crates/nu-command/src/strings/split/chars.rs | 80 ++++++++++---------- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs index 9d546407a1..366ab69cb7 100644 --- a/crates/nu-command/src/example_test.rs +++ b/crates/nu-command/src/example_test.rs @@ -7,7 +7,7 @@ use nu_protocol::{ Value, }; -use super::From; +use super::{From, Split}; pub fn test_examples(cmd: impl Command + 'static) { let examples = cmd.examples(); @@ -15,10 +15,11 @@ pub fn test_examples(cmd: impl Command + 'static) { let delta = { // Base functions that are needed for testing - // Try to keep this working set as small to keep tests running as fast as possible + // Try to keep this working set small to keep tests running as fast as possible let engine_state = engine_state.borrow(); let mut working_set = StateWorkingSet::new(&*engine_state); working_set.add_decl(Box::new(From)); + working_set.add_decl(Box::new(Split)); // Adding the command that is being tested to the working set working_set.add_decl(Box::new(cmd)); diff --git a/crates/nu-command/src/strings/split/chars.rs b/crates/nu-command/src/strings/split/chars.rs index 39df74ab9f..382b50ee9d 100644 --- a/crates/nu-command/src/strings/split/chars.rs +++ b/crates/nu-command/src/strings/split/chars.rs @@ -19,6 +19,38 @@ impl Command for SubCommand { "splits a string's characters into separate rows" } + fn examples(&self) -> Vec { + vec![Example { + description: "Split the string's characters into separate rows", + example: "'hello' | split chars", + result: Some(Value::List { + vals: vec![ + Value::String { + val: "h".into(), + span: Span::unknown(), + }, + Value::String { + val: "e".into(), + span: Span::unknown(), + }, + Value::String { + val: "l".into(), + span: Span::unknown(), + }, + Value::String { + val: "l".into(), + span: Span::unknown(), + }, + Value::String { + val: "o".into(), + span: Span::unknown(), + }, + ], + span: Span::unknown(), + }), + }] + } + fn run( &self, _context: &EvaluationContext, @@ -27,35 +59,6 @@ impl Command for SubCommand { ) -> Result { split_chars(call, input) } - - fn examples(&self) -> Vec { - vec![Example { - description: "Split the string's characters into separate rows", - example: "echo 'hello' | split chars", - result: Some(vec![ - Value::String { - val: "h".into(), - span: Span::unknown(), - }, - Value::String { - val: "e".into(), - span: Span::unknown(), - }, - Value::String { - val: "l".into(), - span: Span::unknown(), - }, - Value::String { - val: "l".into(), - span: Span::unknown(), - }, - Value::String { - val: "o".into(), - span: Span::unknown(), - }, - ]), - }] - } } fn split_chars(call: &Call, input: Value) -> Result { @@ -86,15 +89,14 @@ fn split_chars_helper(v: &Value, name: Span) -> Vec { } } -// #[cfg(test)] -// mod tests { -// use super::ShellError; -// use super::SubCommand; +#[cfg(test)] +mod test { + use super::*; -// #[test] -// fn examples_work_as_expected() -> Result<(), ShellError> { -// use crate::examples::test as test_examples; + #[test] + fn test_examples() { + use crate::test_examples; -// test_examples(SubCommand {}) -// } -// } + test_examples(SubCommand {}) + } +} From a1bfa2788cf03711b9f187d4056239b14fdea05f Mon Sep 17 00:00:00 2001 From: Fernando Herrera Date: Sat, 9 Oct 2021 16:44:45 +0100 Subject: [PATCH 3/3] not found message for windows --- src/tests.rs | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/tests.rs b/src/tests.rs index b3921acbf4..38a9376239 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -54,6 +54,14 @@ fn fail_test(input: &str, expected: &str) -> TestResult { Ok(()) } +fn not_found_msg() -> &'static str { + if cfg!(windows) { + "not recognized" + } else { + "not found" + } +} + #[test] fn add_simple() -> TestResult { run_test("3 + 4", "7") @@ -393,7 +401,7 @@ fn module_import_uses_internal_command() -> TestResult { #[test] fn hides_def() -> TestResult { - fail_test(r#"def foo [] { "foo" }; hide foo; foo"#, "not found") + fail_test(r#"def foo [] { "foo" }; hide foo; foo"#, not_found_msg()) } #[test] @@ -406,7 +414,10 @@ fn hides_def_then_redefines() -> TestResult { #[test] fn hides_def_in_scope_1() -> TestResult { - fail_test(r#"def foo [] { "foo" }; do { hide foo; foo }"#, "not found") + fail_test( + r#"def foo [] { "foo" }; do { hide foo; foo }"#, + not_found_msg(), + ) } #[test] @@ -421,7 +432,7 @@ fn hides_def_in_scope_2() -> TestResult { fn hides_def_in_scope_3() -> TestResult { fail_test( r#"def foo [] { "foo" }; do { hide foo; def foo [] { "bar" }; hide foo; foo }"#, - "not found", + not_found_msg(), ) } @@ -429,7 +440,7 @@ fn hides_def_in_scope_3() -> TestResult { fn hides_def_in_scope_4() -> TestResult { fail_test( r#"def foo [] { "foo" }; do { def foo [] { "bar" }; hide foo; hide foo; foo }"#, - "not found", + not_found_msg(), ) } @@ -445,7 +456,7 @@ fn hide_twice_not_allowed() -> TestResult { fn hides_import_1() -> TestResult { fail_test( r#"module spam { export def foo [] { "foo" } }; use spam; hide spam.foo; foo"#, - "not found", + not_found_msg(), ) } @@ -453,7 +464,7 @@ fn hides_import_1() -> TestResult { fn hides_import_2() -> TestResult { fail_test( r#"module spam { export def foo [] { "foo" } }; use spam; hide spam.*; foo"#, - "not found", + not_found_msg(), ) } @@ -461,7 +472,7 @@ fn hides_import_2() -> TestResult { fn hides_import_3() -> TestResult { fail_test( r#"module spam { export def foo [] { "foo" } }; use spam; hide spam.[foo]; foo"#, - "not found", + not_found_msg(), ) } @@ -469,7 +480,7 @@ fn hides_import_3() -> TestResult { fn hides_import_4() -> TestResult { fail_test( r#"module spam { export def foo [] { "foo" } }; use spam.foo; hide foo; foo"#, - "not found", + not_found_msg(), ) } @@ -477,7 +488,7 @@ fn hides_import_4() -> TestResult { fn hides_import_5() -> TestResult { fail_test( r#"module spam { export def foo [] { "foo" } }; use spam.*; hide foo; foo"#, - "not found", + not_found_msg(), ) }