diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 40cda47bbc..23d6a5ff95 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -26,7 +26,7 @@ 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` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) -- `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library +- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ecda698c7..d5cf758b56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,6 @@ It is good practice to cover your changes with a test. Also, try to think about Tests can be found in different places: * `/tests` -* `src/tests` * command examples * crate-specific tests diff --git a/Cargo.lock b/Cargo.lock index f55ed5ceb4..656f859a63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2034,9 +2034,9 @@ dependencies = [ [[package]] name = "interprocess" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c7fb8583fab9503654385e2bafda123376445a77027a1b106dd7e44cf51122f" +checksum = "7b4d0250d41da118226e55b3d50ca3f0d9e0a0f6829b92f543ac0054aeea1572" dependencies = [ "libc", "recvmsg", @@ -3259,6 +3259,7 @@ dependencies = [ "nu-test-support", "nu-utils", "num-format", + "os_pipe", "pretty_assertions", "rmp-serde", "rstest", @@ -5056,9 +5057,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.3.0" +version = "8.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb78f46d0066053d16d4ca7b898e9343bc3530f71c61d5ad84cd404ada068745" +checksum = "19549741604902eb99a7ed0ee177a0663ee1eda51a29f71401f166e47e77806a" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -5067,9 +5068,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.3.0" +version = "8.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91ac2a3c6c0520a3fb3dd89321177c3c692937c4eb21893378219da10c44fc8" +checksum = "cb9f96e283ec64401f30d3df8ee2aaeb2561f34c824381efa24a35f79bf40ee4" dependencies = [ "proc-macro2", "quote", @@ -5080,9 +5081,9 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.3.0" +version = "8.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f69089032567ffff4eada41c573fc43ff466c7db7c5688b2e7969584345581" +checksum = "38c74a686185620830701348de757fd36bef4aa9680fd23c49fc539ddcc1af32" dependencies = [ "sha2", "walkdir", diff --git a/Cargo.toml b/Cargo.toml index beabd26e20..1366ecbdd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,7 +94,7 @@ heck = "0.5.0" human-date-parser = "0.1.1" indexmap = "2.2" indicatif = "0.17" -interprocess = "2.0.1" +interprocess = "2.1.0" is_executable = "1.0" itertools = "0.12" libc = "0.2" @@ -119,7 +119,7 @@ num-traits = "0.2" omnipath = "0.1" once_cell = "1.18" open = "5.1" -os_pipe = "1.1" +os_pipe = { version = "1.1", features = ["io_safety"] } pathdiff = "0.2" percent-encoding = "2" pretty_assertions = "1.4" @@ -140,7 +140,7 @@ ropey = "1.6.1" roxmltree = "0.19" rstest = { version = "0.18", default-features = false } rusqlite = "0.31" -rust-embed = "8.3.0" +rust-embed = "8.4.0" same-file = "1.0" serde = { version = "1.0", default-features = false } serde_json = "1.0" diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index 2482a7920e..29c2f62734 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -542,7 +542,9 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { let shell_integration_osc633 = config.shell_integration_osc633; let shell_integration_reset_application_mode = config.shell_integration_reset_application_mode; - let mut stack = Stack::unwrap_unique(stack_arc); + // TODO: we may clone the stack, this can lead to major performance issues + // so we should avoid it or making stack cheaper to clone. + let mut stack = Arc::unwrap_or_clone(stack_arc); perf( "line_editor setup", diff --git a/crates/nu-cmd-extra/src/extra/filters/each_while.rs b/crates/nu-cmd-extra/src/extra/filters/each_while.rs index 2dda815d43..939f194f43 100644 --- a/crates/nu-cmd-extra/src/extra/filters/each_while.rs +++ b/crates/nu-cmd-extra/src/extra/filters/each_while.rs @@ -10,7 +10,7 @@ impl Command for EachWhile { } fn usage(&self) -> &str { - "Run a block on each row of the input list until a null is found, then create a new list with the results." + "Run a closure on each row of the input list until a null is found, then create a new list with the results." } fn search_terms(&self) -> Vec<&str> { diff --git a/crates/nu-command/src/database/mod.rs b/crates/nu-command/src/database/mod.rs index 4fddf6638e..22b92a81d3 100644 --- a/crates/nu-command/src/database/mod.rs +++ b/crates/nu-command/src/database/mod.rs @@ -5,7 +5,7 @@ use commands::add_commands_decls; pub use values::{ convert_sqlite_row_to_nu_value, convert_sqlite_value_to_nu_value, open_connection_in_memory, - open_connection_in_memory_custom, SQLiteDatabase, MEMORY_DB, + open_connection_in_memory_custom, values_to_sql, SQLiteDatabase, MEMORY_DB, }; use nu_protocol::engine::StateWorkingSet; diff --git a/crates/nu-command/src/database/values/mod.rs b/crates/nu-command/src/database/values/mod.rs index 4442ec0783..b9bd3d5d4c 100644 --- a/crates/nu-command/src/database/values/mod.rs +++ b/crates/nu-command/src/database/values/mod.rs @@ -3,5 +3,5 @@ pub mod sqlite; pub use sqlite::{ convert_sqlite_row_to_nu_value, convert_sqlite_value_to_nu_value, open_connection_in_memory, - open_connection_in_memory_custom, SQLiteDatabase, MEMORY_DB, + open_connection_in_memory_custom, values_to_sql, SQLiteDatabase, MEMORY_DB, }; diff --git a/crates/nu-command/src/env/with_env.rs b/crates/nu-command/src/env/with_env.rs index 7eaf64cb54..ff66d21f49 100644 --- a/crates/nu-command/src/env/with_env.rs +++ b/crates/nu-command/src/env/with_env.rs @@ -90,10 +90,7 @@ fn with_env( return Err(ShellError::CantConvert { to_type: "record".into(), from_type: x.get_type().to_string(), - span: call - .positional_nth(1) - .expect("already checked through .req") - .span, + span: x.span(), help: None, }); } @@ -124,10 +121,7 @@ fn with_env( return Err(ShellError::CantConvert { to_type: "record".into(), from_type: x.get_type().to_string(), - span: call - .positional_nth(1) - .expect("already checked through .req") - .span, + span: x.span(), help: None, }); } diff --git a/crates/nu-command/src/filesystem/touch.rs b/crates/nu-command/src/filesystem/touch.rs index ce8ed0dad3..08843b11f0 100644 --- a/crates/nu-command/src/filesystem/touch.rs +++ b/crates/nu-command/src/filesystem/touch.rs @@ -117,7 +117,7 @@ impl Command for Touch { #[allow(deprecated)] let cwd = current_dir(engine_state, stack)?; - for (index, glob) in files.into_iter().enumerate() { + for glob in files { let path = expand_path_with(glob.item.as_ref(), &cwd, glob.item.is_expand()); // If --no-create is passed and the file/dir does not exist there's nothing to do @@ -135,10 +135,7 @@ impl Command for Touch { { return Err(ShellError::CreateNotPossible { msg: format!("Failed to create file: {err}"), - span: call - .positional_nth(index) - .expect("already checked positional") - .span, + span: glob.span, }); }; } @@ -148,10 +145,7 @@ impl Command for Touch { { return Err(ShellError::ChangeModifiedTimeNotPossible { msg: format!("Failed to change the modified time: {err}"), - span: call - .positional_nth(index) - .expect("already checked positional") - .span, + span: glob.span, }); }; } @@ -161,10 +155,7 @@ impl Command for Touch { { return Err(ShellError::ChangeAccessTimeNotPossible { msg: format!("Failed to change the access time: {err}"), - span: call - .positional_nth(index) - .expect("already checked positional") - .span, + span: glob.span, }); }; } diff --git a/crates/nu-command/src/filters/compact.rs b/crates/nu-command/src/filters/compact.rs index d8872448be..9a4e968e9b 100644 --- a/crates/nu-command/src/filters/compact.rs +++ b/crates/nu-command/src/filters/compact.rs @@ -31,6 +31,10 @@ impl Command for Compact { "Creates a table with non-empty rows." } + fn search_terms(&self) -> Vec<&str> { + vec!["empty", "remove"] + } + fn run( &self, engine_state: &EngineState, diff --git a/crates/nu-command/src/filters/get.rs b/crates/nu-command/src/filters/get.rs index c30ad1f3a4..a481372db1 100644 --- a/crates/nu-command/src/filters/get.rs +++ b/crates/nu-command/src/filters/get.rs @@ -115,7 +115,7 @@ If multiple cell paths are given, this will produce a list of values."# }, Example { description: - "Extract the name of the 3rd record in a list (same as `ls | $in.name`)", + "Extract the name of the 3rd record in a list (same as `ls | $in.name.2`)", example: "ls | get name.2", result: None, }, diff --git a/crates/nu-command/src/platform/ansi/ansi_.rs b/crates/nu-command/src/platform/ansi/ansi_.rs index 427c7c3a73..9e0b0bba66 100644 --- a/crates/nu-command/src/platform/ansi/ansi_.rs +++ b/crates/nu-command/src/platform/ansi/ansi_.rs @@ -784,10 +784,7 @@ fn heavy_lifting(code: Value, escape: bool, osc: bool, call: &Call) -> Result { return Err(ShellError::TypeMismatch { err_message: String::from("Unknown ansi code"), - span: call - .positional_nth(0) - .expect("Unexpected missing argument") - .span, + span: code.span(), }) } } diff --git a/crates/nu-command/src/stor/insert.rs b/crates/nu-command/src/stor/insert.rs index 2aeb076d44..e0c0ad4d28 100644 --- a/crates/nu-command/src/stor/insert.rs +++ b/crates/nu-command/src/stor/insert.rs @@ -1,5 +1,6 @@ -use crate::database::{SQLiteDatabase, MEMORY_DB}; +use crate::database::{values_to_sql, SQLiteDatabase, MEMORY_DB}; use nu_engine::command_prelude::*; +use rusqlite::params_from_iter; #[derive(Clone)] pub struct StorInsert; @@ -57,86 +58,81 @@ impl Command for StorInsert { // let config = engine_state.get_config(); let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); - if table_name.is_none() { - return Err(ShellError::MissingParameter { - param_name: "requires at table name".into(), - span, - }); - } - let new_table_name = table_name.unwrap_or("table".into()); - if let Ok(conn) = db.open_connection() { - match columns { - Some(record) => { - let mut create_stmt = format!("INSERT INTO {} ( ", new_table_name); - let cols = record.columns(); - cols.for_each(|col| { - create_stmt.push_str(&format!("{}, ", col)); - }); - if create_stmt.ends_with(", ") { - create_stmt.pop(); - create_stmt.pop(); - } + process(table_name, span, &db, columns)?; - create_stmt.push_str(") VALUES ( "); - let vals = record.values(); - vals.for_each(|val| match val { - Value::Int { val, .. } => { - create_stmt.push_str(&format!("{}, ", val)); - } - Value::Float { val, .. } => { - create_stmt.push_str(&format!("{}, ", val)); - } - Value::String { val, .. } => { - create_stmt.push_str(&format!("'{}', ", val)); - } - Value::Date { val, .. } => { - create_stmt.push_str(&format!("'{}', ", val)); - } - Value::Bool { val, .. } => { - create_stmt.push_str(&format!("{}, ", val)); - } - _ => { - // return Err(ShellError::UnsupportedInput { - // msg: format!("{} is not a valid datepart, expected one of year, month, day, hour, minute, second, millisecond, microsecond, nanosecond", part.item), - // input: "value originates from here".to_string(), - // msg_span: span, - // input_span: val.span(), - // }); - } - }); - if create_stmt.ends_with(", ") { - create_stmt.pop(); - create_stmt.pop(); - } - - create_stmt.push(')'); - - // dbg!(&create_stmt); - - conn.execute(&create_stmt, []) - .map_err(|err| ShellError::GenericError { - error: "Failed to open SQLite connection in memory from insert".into(), - msg: err.to_string(), - span: Some(Span::test_data()), - help: None, - inner: vec![], - })?; - } - None => { - return Err(ShellError::MissingParameter { - param_name: "requires at least one column".into(), - span: call.head, - }); - } - }; - } - // dbg!(db.clone()); Ok(Value::custom(db, span).into_pipeline_data()) } } +fn process( + table_name: Option, + span: Span, + db: &SQLiteDatabase, + columns: Option, +) -> Result<(), ShellError> { + if table_name.is_none() { + return Err(ShellError::MissingParameter { + param_name: "requires at table name".into(), + span, + }); + } + let new_table_name = table_name.unwrap_or("table".into()); + if let Ok(conn) = db.open_connection() { + match columns { + Some(record) => { + let mut create_stmt = format!("INSERT INTO {} ( ", new_table_name); + let cols = record.columns(); + cols.for_each(|col| { + create_stmt.push_str(&format!("{}, ", col)); + }); + if create_stmt.ends_with(", ") { + create_stmt.pop(); + create_stmt.pop(); + } + + // Values are set as placeholders. + create_stmt.push_str(") VALUES ( "); + for (index, _) in record.columns().enumerate() { + create_stmt.push_str(&format!("?{}, ", index + 1)); + } + + if create_stmt.ends_with(", ") { + create_stmt.pop(); + create_stmt.pop(); + } + + create_stmt.push(')'); + + // dbg!(&create_stmt); + + // Get the params from the passed values + let params = values_to_sql(record.values().cloned())?; + + conn.execute(&create_stmt, params_from_iter(params)) + .map_err(|err| ShellError::GenericError { + error: "Failed to open SQLite connection in memory from insert".into(), + msg: err.to_string(), + span: Some(Span::test_data()), + help: None, + inner: vec![], + })?; + } + None => { + return Err(ShellError::MissingParameter { + param_name: "requires at least one column".into(), + span, + }); + } + }; + } + // dbg!(db.clone()); + Ok(()) +} + #[cfg(test)] mod test { + use chrono::DateTime; + use super::*; #[test] @@ -145,4 +141,160 @@ mod test { test_examples(StorInsert {}) } + + #[test] + fn test_process_with_simple_parameters() { + let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let create_stmt = "CREATE TABLE test_process_with_simple_parameters ( + int_column INTEGER, + real_column REAL, + str_column VARCHAR(255), + bool_column BOOLEAN, + date_column DATETIME DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) + )"; + + let conn = db + .open_connection() + .expect("Test was unable to open connection."); + conn.execute(create_stmt, []) + .expect("Failed to create table as part of test."); + let table_name = Some("test_process_with_simple_parameters".to_string()); + let span = Span::unknown(); + let mut columns = Record::new(); + columns.insert("int_column".to_string(), Value::test_int(42)); + columns.insert("real_column".to_string(), Value::test_float(3.1)); + columns.insert( + "str_column".to_string(), + Value::test_string("SimpleString".to_string()), + ); + columns.insert("bool_column".to_string(), Value::test_bool(true)); + columns.insert( + "date_column".to_string(), + Value::test_date( + DateTime::parse_from_str("2021-12-30 00:00:00 +0000", "%Y-%m-%d %H:%M:%S %z") + .expect("Date string should parse."), + ), + ); + + let result = process(table_name, span, &db, Some(columns)); + + assert!(result.is_ok()); + } + + #[test] + fn test_process_string_with_space() { + let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let create_stmt = "CREATE TABLE test_process_string_with_space ( + str_column VARCHAR(255) + )"; + + let conn = db + .open_connection() + .expect("Test was unable to open connection."); + conn.execute(create_stmt, []) + .expect("Failed to create table as part of test."); + let table_name = Some("test_process_string_with_space".to_string()); + let span = Span::unknown(); + let mut columns = Record::new(); + columns.insert( + "str_column".to_string(), + Value::test_string("String With Spaces".to_string()), + ); + + let result = process(table_name, span, &db, Some(columns)); + + assert!(result.is_ok()); + } + + #[test] + fn test_no_errors_when_string_too_long() { + let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let create_stmt = "CREATE TABLE test_errors_when_string_too_long ( + str_column VARCHAR(8) + )"; + + let conn = db + .open_connection() + .expect("Test was unable to open connection."); + conn.execute(create_stmt, []) + .expect("Failed to create table as part of test."); + let table_name = Some("test_errors_when_string_too_long".to_string()); + let span = Span::unknown(); + let mut columns = Record::new(); + columns.insert( + "str_column".to_string(), + Value::test_string("ThisIsALongString".to_string()), + ); + + let result = process(table_name, span, &db, Some(columns)); + // SQLite uses dynamic typing, making any length acceptable for a varchar column + assert!(result.is_ok()); + } + + #[test] + fn test_no_errors_when_param_is_wrong_type() { + let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let create_stmt = "CREATE TABLE test_errors_when_param_is_wrong_type ( + int_column INT + )"; + + let conn = db + .open_connection() + .expect("Test was unable to open connection."); + conn.execute(create_stmt, []) + .expect("Failed to create table as part of test."); + let table_name = Some("test_errors_when_param_is_wrong_type".to_string()); + let span = Span::unknown(); + let mut columns = Record::new(); + columns.insert( + "int_column".to_string(), + Value::test_string("ThisIsTheWrongType".to_string()), + ); + + let result = process(table_name, span, &db, Some(columns)); + // SQLite uses dynamic typing, making any type acceptable for a column + assert!(result.is_ok()); + } + + #[test] + fn test_errors_when_column_doesnt_exist() { + let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let create_stmt = "CREATE TABLE test_errors_when_column_doesnt_exist ( + int_column INT + )"; + + let conn = db + .open_connection() + .expect("Test was unable to open connection."); + conn.execute(create_stmt, []) + .expect("Failed to create table as part of test."); + let table_name = Some("test_errors_when_column_doesnt_exist".to_string()); + let span = Span::unknown(); + let mut columns = Record::new(); + columns.insert( + "not_a_column".to_string(), + Value::test_string("ThisIsALongString".to_string()), + ); + + let result = process(table_name, span, &db, Some(columns)); + + assert!(result.is_err()); + } + + #[test] + fn test_errors_when_table_doesnt_exist() { + let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + + let table_name = Some("test_errors_when_table_doesnt_exist".to_string()); + let span = Span::unknown(); + let mut columns = Record::new(); + columns.insert( + "str_column".to_string(), + Value::test_string("ThisIsALongString".to_string()), + ); + + let result = process(table_name, span, &db, Some(columns)); + + assert!(result.is_err()); + } } diff --git a/crates/nu-command/src/strings/char_.rs b/crates/nu-command/src/strings/char_.rs index e227f2667f..7d8d152b56 100644 --- a/crates/nu-command/src/strings/char_.rs +++ b/crates/nu-command/src/strings/char_.rs @@ -235,18 +235,18 @@ impl Command for Char { // handle -i flag if integer { - let int_args: Vec = call.rest_const(working_set, 0)?; - handle_integer_flag(int_args, call, call_span) + let int_args = call.rest_const(working_set, 0)?; + handle_integer_flag(int_args, call_span) } // handle -u flag else if unicode { - let string_args: Vec = call.rest_const(working_set, 0)?; - handle_unicode_flag(string_args, call, call_span) + let string_args = call.rest_const(working_set, 0)?; + handle_unicode_flag(string_args, call_span) } // handle the rest else { - let string_args: Vec = call.rest_const(working_set, 0)?; - handle_the_rest(string_args, call, call_span) + let string_args = call.rest_const(working_set, 0)?; + handle_the_rest(string_args, call_span) } } @@ -270,18 +270,18 @@ impl Command for Char { // handle -i flag if integer { - let int_args: Vec = call.rest(engine_state, stack, 0)?; - handle_integer_flag(int_args, call, call_span) + let int_args = call.rest(engine_state, stack, 0)?; + handle_integer_flag(int_args, call_span) } // handle -u flag else if unicode { - let string_args: Vec = call.rest(engine_state, stack, 0)?; - handle_unicode_flag(string_args, call, call_span) + let string_args = call.rest(engine_state, stack, 0)?; + handle_unicode_flag(string_args, call_span) } // handle the rest else { - let string_args: Vec = call.rest(engine_state, stack, 0)?; - handle_the_rest(string_args, call, call_span) + let string_args = call.rest(engine_state, stack, 0)?; + handle_the_rest(string_args, call_span) } } } @@ -309,8 +309,7 @@ fn generate_character_list(ctrlc: Option>, call_span: Span) -> P } fn handle_integer_flag( - int_args: Vec, - call: &Call, + int_args: Vec>, call_span: Span, ) -> Result { if int_args.is_empty() { @@ -319,20 +318,17 @@ fn handle_integer_flag( span: call_span, }); } - let mut multi_byte = String::new(); - for (i, &arg) in int_args.iter().enumerate() { - let span = call - .positional_nth(i) - .expect("Unexpected missing argument") - .span; - multi_byte.push(integer_to_unicode_char(arg, span)?) - } - Ok(Value::string(multi_byte, call_span).into_pipeline_data()) + + let str = int_args + .into_iter() + .map(integer_to_unicode_char) + .collect::>()?; + + Ok(Value::string(str, call_span).into_pipeline_data()) } fn handle_unicode_flag( - string_args: Vec, - call: &Call, + string_args: Vec>, call_span: Span, ) -> Result { if string_args.is_empty() { @@ -341,57 +337,53 @@ fn handle_unicode_flag( span: call_span, }); } - let mut multi_byte = String::new(); - for (i, arg) in string_args.iter().enumerate() { - let span = call - .positional_nth(i) - .expect("Unexpected missing argument") - .span; - multi_byte.push(string_to_unicode_char(arg, span)?) - } - Ok(Value::string(multi_byte, call_span).into_pipeline_data()) + + let str = string_args + .into_iter() + .map(string_to_unicode_char) + .collect::>()?; + + Ok(Value::string(str, call_span).into_pipeline_data()) } fn handle_the_rest( - string_args: Vec, - call: &Call, + string_args: Vec>, call_span: Span, ) -> Result { - if string_args.is_empty() { + let Some(s) = string_args.first() else { return Err(ShellError::MissingParameter { param_name: "missing name of the character".into(), span: call_span, }); - } - let special_character = str_to_character(&string_args[0]); + }; + + let special_character = str_to_character(&s.item); + if let Some(output) = special_character { Ok(Value::string(output, call_span).into_pipeline_data()) } else { Err(ShellError::TypeMismatch { err_message: "error finding named character".into(), - span: call - .positional_nth(0) - .expect("Unexpected missing argument") - .span, + span: s.span, }) } } -fn integer_to_unicode_char(value: i64, t: Span) -> Result { - let decoded_char = value.try_into().ok().and_then(std::char::from_u32); +fn integer_to_unicode_char(value: Spanned) -> Result { + let decoded_char = value.item.try_into().ok().and_then(std::char::from_u32); if let Some(ch) = decoded_char { Ok(ch) } else { Err(ShellError::TypeMismatch { err_message: "not a valid Unicode codepoint".into(), - span: t, + span: value.span, }) } } -fn string_to_unicode_char(s: &str, t: Span) -> Result { - let decoded_char = u32::from_str_radix(s, 16) +fn string_to_unicode_char(s: Spanned) -> Result { + let decoded_char = u32::from_str_radix(&s.item, 16) .ok() .and_then(std::char::from_u32); @@ -400,7 +392,7 @@ fn string_to_unicode_char(s: &str, t: Span) -> Result { } else { Err(ShellError::TypeMismatch { err_message: "error decoding Unicode character".into(), - span: t, + span: s.span, }) } } diff --git a/crates/nu-command/src/system/sys/mod.rs b/crates/nu-command/src/system/sys/mod.rs index 5e0467c251..fcb40fd37d 100644 --- a/crates/nu-command/src/system/sys/mod.rs +++ b/crates/nu-command/src/system/sys/mod.rs @@ -16,9 +16,8 @@ pub use sys_::Sys; pub use temp::SysTemp; pub use users::SysUsers; -use chrono::{DateTime, Local}; +use chrono::{DateTime, FixedOffset, Local}; use nu_protocol::{record, Record, Span, Value}; -use std::time::{Duration, UNIX_EPOCH}; use sysinfo::{ Components, CpuRefreshKind, Disks, Networks, System, Users, MINIMUM_CPU_UPDATE_INTERVAL, }; @@ -171,22 +170,31 @@ pub fn host(span: Span) -> Record { record.push("hostname", Value::string(trim_cstyle_null(hostname), span)); } - record.push( - "uptime", - Value::duration(1000000000 * System::uptime() as i64, span), - ); + let uptime = System::uptime() + .saturating_mul(1_000_000_000) + .try_into() + .unwrap_or(i64::MAX); - // Creates a new SystemTime from the specified number of whole seconds - let d = UNIX_EPOCH + Duration::from_secs(System::boot_time()); - // Create DateTime from SystemTime - let datetime = DateTime::::from(d); - // Convert to local time and then rfc3339 - let timestamp_str = datetime.with_timezone(datetime.offset()).to_rfc3339(); - record.push("boot_time", Value::string(timestamp_str, span)); + record.push("uptime", Value::duration(uptime, span)); + + let boot_time = boot_time() + .map(|time| Value::date(time, span)) + .unwrap_or(Value::nothing(span)); + + record.push("boot_time", boot_time); record } +fn boot_time() -> Option> { + // Broken systems can apparently return really high values. + // See: https://github.com/nushell/nushell/issues/10155 + // First, try to convert u64 to i64, and then try to create a `DateTime`. + let secs = System::boot_time().try_into().ok()?; + let time = DateTime::from_timestamp(secs, 0)?; + Some(time.with_timezone(&Local).fixed_offset()) +} + pub fn temp(span: Span) -> Value { let components = Components::new_with_refreshed_list() .iter() diff --git a/crates/nu-explore/src/views/record/mod.rs b/crates/nu-explore/src/views/record/mod.rs index 6534ba3589..0ed8e3e761 100644 --- a/crates/nu-explore/src/views/record/mod.rs +++ b/crates/nu-explore/src/views/record/mod.rs @@ -20,7 +20,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use nu_color_config::{get_color_map, StyleComputer}; use nu_protocol::{ engine::{EngineState, Stack}, - Record, Value, + Record, Span, Value, }; use ratatui::{layout::Rect, widgets::Block}; use std::{borrow::Cow, collections::HashMap}; @@ -180,7 +180,11 @@ impl<'a> RecordView<'a> { Orientation::Left => (column, row), }; - layer.records[row][column].clone() + if layer.records.len() > row && layer.records[row].len() > column { + layer.records[row][column].clone() + } else { + Value::nothing(Span::unknown()) + } } fn create_tablew(&'a self, cfg: ViewConfig<'a>) -> TableW<'a> { diff --git a/crates/nu-parser/src/lex.rs b/crates/nu-parser/src/lex.rs index 9560c5aec5..f67b8b0202 100644 --- a/crates/nu-parser/src/lex.rs +++ b/crates/nu-parser/src/lex.rs @@ -132,7 +132,7 @@ pub fn lex_item( }, Some(ParseError::UnexpectedEof( (start as char).to_string(), - Span::new(span.end, span.end), + Span::new(span.end - 1, span.end), )), ); } @@ -262,26 +262,10 @@ pub fn lex_item( let span = Span::new(span_offset + token_start, span_offset + *curr_offset); - // If there is still unclosed opening delimiters, remember they were missing - if let Some(block) = block_level.last() { - let delim = block.closing(); - let cause = - ParseError::UnexpectedEof((delim as char).to_string(), Span::new(span.end, span.end)); - - return ( - Token { - contents: TokenContents::Item, - span, - }, - Some(cause), - ); - } - if let Some(delim) = quote_start { // The non-lite parse trims quotes on both sides, so we add the expected quote so that // anyone wanting to consume this partial parse (e.g., completions) will be able to get // correct information from the non-lite parse. - return ( Token { contents: TokenContents::Item, @@ -289,11 +273,28 @@ pub fn lex_item( }, Some(ParseError::UnexpectedEof( (delim as char).to_string(), - Span::new(span.start, span.end), + Span::new(span.end - 1, span.end), )), ); } + // If there is still unclosed opening delimiters, remember they were missing + if let Some(block) = block_level.last() { + let delim = block.closing(); + let cause = ParseError::UnexpectedEof( + (delim as char).to_string(), + Span::new(span.end - 1, span.end), + ); + + return ( + Token { + contents: TokenContents::Item, + span, + }, + Some(cause), + ); + } + // If we didn't accumulate any characters, it's an unexpected error. if *curr_offset - token_start == 0 { return ( @@ -411,9 +412,11 @@ fn lex_raw_string( *curr_offset += 1 } if !matches { + let mut expected = '\''.to_string(); + expected.push_str(&"#".repeat(prefix_sharp_cnt)); return Err(ParseError::UnexpectedEof( - "#".to_string(), - Span::new(span_offset + *curr_offset, span_offset + *curr_offset), + expected, + Span::new(span_offset + *curr_offset - 1, span_offset + *curr_offset), )); } Ok(()) diff --git a/crates/nu-protocol/Cargo.toml b/crates/nu-protocol/Cargo.toml index e2420a6e83..be45738447 100644 --- a/crates/nu-protocol/Cargo.toml +++ b/crates/nu-protocol/Cargo.toml @@ -47,6 +47,7 @@ nu-test-support = { path = "../nu-test-support", version = "0.93.1" } pretty_assertions = { workspace = true } rstest = { workspace = true } tempfile = { workspace = true } +os_pipe = { workspace = true } [package.metadata.docs.rs] all-features = true diff --git a/crates/nu-protocol/src/ast/call.rs b/crates/nu-protocol/src/ast/call.rs index 6689bc3a55..9aaf17d5db 100644 --- a/crates/nu-protocol/src/ast/call.rs +++ b/crates/nu-protocol/src/ast/call.rs @@ -154,30 +154,10 @@ impl Call { }) } - pub fn positional_iter_mut(&mut self) -> impl Iterator { - self.arguments - .iter_mut() - .take_while(|arg| match arg { - Argument::Spread(_) => false, // Don't include positional arguments given to rest parameter - _ => true, - }) - .filter_map(|arg| match arg { - Argument::Named(_) => None, - Argument::Positional(positional) => Some(positional), - Argument::Unknown(unknown) => Some(unknown), - Argument::Spread(_) => None, - }) - } - pub fn positional_nth(&self, i: usize) -> Option<&Expression> { self.positional_iter().nth(i) } - // TODO this method is never used. Delete? - pub fn positional_nth_mut(&mut self, i: usize) -> Option<&mut Expression> { - self.positional_iter_mut().nth(i) - } - pub fn positional_len(&self) -> usize { self.positional_iter().count() } diff --git a/crates/nu-protocol/src/engine/stack.rs b/crates/nu-protocol/src/engine/stack.rs index b577d55270..19726db9c0 100644 --- a/crates/nu-protocol/src/engine/stack.rs +++ b/crates/nu-protocol/src/engine/stack.rs @@ -7,6 +7,7 @@ use crate::{ }; use std::{ collections::{HashMap, HashSet}, + fs::File, sync::Arc, }; @@ -74,26 +75,10 @@ impl Stack { } } - /// Unwrap a uniquely-owned stack. - /// - /// In debug mode, this panics if there are multiple references. - /// In production this will instead clone the underlying stack. - pub fn unwrap_unique(stack_arc: Arc) -> Stack { - // If you hit an error here, it's likely that you created an extra - // Arc pointing to the stack somewhere. Make sure that it gets dropped before - // getting here! - Arc::try_unwrap(stack_arc).unwrap_or_else(|arc| { - // in release mode, we clone the stack, but this can lead to - // major performance issues, so we should avoid it - debug_assert!(false, "More than one stack reference remaining!"); - (*arc).clone() - }) - } - /// Create a new child stack from a parent. /// /// Changes from this child can be merged back into the parent with - /// Stack::with_changes_from_child + /// [`Stack::with_changes_from_child`] pub fn with_parent(parent: Arc) -> Stack { Stack { // here we are still cloning environment variable-related information @@ -108,19 +93,17 @@ impl Stack { } } - /// Take an Arc of a parent (assumed to be unique), and a child, and apply - /// all the changes from a child back to the parent. + /// Take an [`Arc`] parent, and a child, and apply all the changes from a child back to the parent. /// - /// Here it is assumed that child was created with a call to Stack::with_parent - /// with parent + /// Here it is assumed that `child` was created by a call to [`Stack::with_parent`] with `parent`. + /// + /// For this to be performant and not clone `parent`, `child` should be the only other + /// referencer of `parent`. pub fn with_changes_from_child(parent: Arc, child: Stack) -> Stack { // we're going to drop the link to the parent stack on our new stack // so that we can unwrap the Arc as a unique reference - // - // This makes the new_stack be in a bit of a weird state, so we shouldn't call - // any structs drop(child.parent_stack); - let mut unique_stack = Stack::unwrap_unique(parent); + let mut unique_stack = Arc::unwrap_or_clone(parent); unique_stack .vars @@ -593,6 +576,57 @@ impl Stack { self } + /// Replaces the default stdout of the stack with a given file. + /// + /// This method configures the default stdout to redirect to a specified file. + /// It is primarily useful for applications using `nu` as a language, where the stdout of + /// external commands that are not explicitly piped can be redirected to a file. + /// + /// # Using Pipes + /// + /// For use in third-party applications pipes might be very useful as they allow using the + /// stdout of external commands for different uses. + /// For example the [`os_pipe`](https://docs.rs/os_pipe) crate provides a elegant way to to + /// access the stdout. + /// + /// ``` + /// # use std::{fs::File, io::{self, Read}, thread, error}; + /// # use nu_protocol::engine::Stack; + /// # + /// let (mut reader, writer) = os_pipe::pipe().unwrap(); + /// // Use a thread to avoid blocking the execution of the called command. + /// let reader = thread::spawn(move || { + /// let mut buf: Vec = Vec::new(); + /// reader.read_to_end(&mut buf)?; + /// Ok::<_, io::Error>(buf) + /// }); + /// + /// #[cfg(windows)] + /// let file = std::os::windows::io::OwnedHandle::from(writer).into(); + /// #[cfg(unix)] + /// let file = std::os::unix::io::OwnedFd::from(writer).into(); + /// + /// let stack = Stack::new().stdout_file(file); + /// + /// // Execute some nu code. + /// + /// drop(stack); // drop the stack so that the writer will be dropped too + /// let buf = reader.join().unwrap().unwrap(); + /// // Do with your buffer whatever you want. + /// ``` + pub fn stdout_file(mut self, file: File) -> Self { + self.out_dest.stdout = OutDest::File(Arc::new(file)); + self + } + + /// Replaces the default stderr of the stack with a given file. + /// + /// For more info, see [`stdout_file`](Self::stdout_file). + pub fn stderr_file(mut self, file: File) -> Self { + self.out_dest.stderr = OutDest::File(Arc::new(file)); + self + } + /// Set the PWD environment variable to `path`. /// /// This method accepts `path` with trailing slashes, but they're removed diff --git a/crates/nu-std/testing.nu b/crates/nu-std/testing.nu index ddae1d371e..489430e462 100644 --- a/crates/nu-std/testing.nu +++ b/crates/nu-std/testing.nu @@ -287,7 +287,7 @@ export def run-tests [ --list, # list the selected tests without running them. --threads: int@"nu-complete threads", # Amount of threads to use for parallel execution. Default: All threads are utilized ] { - let available_threads = (sys | get cpu | length) + let available_threads = (sys cpu | length) # Can't use pattern matching here due to https://github.com/nushell/nushell/issues/9198 let threads = (if $threads == null { diff --git a/crates/nu_plugin_polars/src/dataframe/eager/summary.rs b/crates/nu_plugin_polars/src/dataframe/eager/summary.rs index 8e5151837d..2b22d73bfb 100644 --- a/crates/nu_plugin_polars/src/dataframe/eager/summary.rs +++ b/crates/nu_plugin_polars/src/dataframe/eager/summary.rs @@ -38,7 +38,7 @@ impl PluginCommand for Summary { ) .named( "quantiles", - SyntaxShape::Table(vec![]), + SyntaxShape::List(Box::new(SyntaxShape::Float)), "provide optional quantiles", Some('q'), ) diff --git a/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/operations.rs b/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/operations.rs index 42d803b0e4..db5409aff2 100644 --- a/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/operations.rs +++ b/crates/nu_plugin_polars/src/dataframe/values/nu_dataframe/operations.rs @@ -27,7 +27,7 @@ impl NuDataFrame { let rhs_span = right.span(); match right { Value::Custom { .. } => { - let rhs = NuDataFrame::try_from_value(plugin, right)?; + let rhs = NuDataFrame::try_from_value_coerce(plugin, right, rhs_span)?; match (self.is_series(), rhs.is_series()) { (true, true) => { diff --git a/crates/nu_plugin_polars/src/dataframe/values/nu_lazyframe/custom_value.rs b/crates/nu_plugin_polars/src/dataframe/values/nu_lazyframe/custom_value.rs index 731d210dd8..b3cd9500dd 100644 --- a/crates/nu_plugin_polars/src/dataframe/values/nu_lazyframe/custom_value.rs +++ b/crates/nu_plugin_polars/src/dataframe/values/nu_lazyframe/custom_value.rs @@ -7,7 +7,7 @@ use uuid::Uuid; use crate::{ values::{CustomValueSupport, NuDataFrame, PolarsPluginCustomValue}, - PolarsPlugin, + Cacheable, PolarsPlugin, }; use super::NuLazyFrame; @@ -76,11 +76,49 @@ impl PolarsPluginCustomValue for NuLazyFrameCustomValue { _engine: &EngineInterface, other_value: Value, ) -> Result, ShellError> { - // to compare, we need to convert to NuDataframe - let df = NuLazyFrame::try_from_custom_value(plugin, self)?; - let df = df.collect(other_value.span())?; + let eager = NuLazyFrame::try_from_custom_value(plugin, self)?.collect(Span::unknown())?; let other = NuDataFrame::try_from_value_coerce(plugin, &other_value, other_value.span())?; - let res = df.is_equal(&other); + let res = eager.is_equal(&other); Ok(res) } + + fn custom_value_operation( + &self, + plugin: &PolarsPlugin, + engine: &EngineInterface, + lhs_span: Span, + operator: nu_protocol::Spanned, + right: Value, + ) -> Result { + let eager = NuLazyFrame::try_from_custom_value(plugin, self)?.collect(Span::unknown())?; + Ok(eager + .compute_with_value(plugin, lhs_span, operator.item, operator.span, &right)? + .cache(plugin, engine, lhs_span)? + .into_value(lhs_span)) + } + + fn custom_value_follow_path_int( + &self, + plugin: &PolarsPlugin, + _engine: &EngineInterface, + _self_span: Span, + index: nu_protocol::Spanned, + ) -> Result { + let eager = NuLazyFrame::try_from_custom_value(plugin, self)?.collect(Span::unknown())?; + eager.get_value(index.item, index.span) + } + + fn custom_value_follow_path_string( + &self, + plugin: &PolarsPlugin, + engine: &EngineInterface, + self_span: Span, + column_name: nu_protocol::Spanned, + ) -> Result { + let eager = NuLazyFrame::try_from_custom_value(plugin, self)?.collect(Span::unknown())?; + let column = eager.column(&column_name.item, self_span)?; + Ok(column + .cache(plugin, engine, self_span)? + .into_value(self_span)) + } } diff --git a/src/config_files.rs b/src/config_files.rs index ba8a3436a0..ec4511860f 100644 --- a/src/config_files.rs +++ b/src/config_files.rs @@ -1,4 +1,4 @@ -use log::{info, trace}; +use log::warn; #[cfg(feature = "plugin")] use nu_cli::read_plugin_file; use nu_cli::{eval_config_contents, eval_source}; @@ -27,7 +27,10 @@ pub(crate) fn read_config_file( config_file: Option>, is_env_config: bool, ) { - trace!("read_config_file {:?}", &config_file); + warn!( + "read_config_file() config_file_specified: {:?}, is_env_config: {is_env_config}", + &config_file + ); // Load config startup file if let Some(file) = config_file { let working_set = StateWorkingSet::new(engine_state); @@ -122,21 +125,27 @@ pub(crate) fn read_config_file( } pub(crate) fn read_loginshell_file(engine_state: &mut EngineState, stack: &mut Stack) { + warn!( + "read_loginshell_file() {}:{}:{}", + file!(), + line!(), + column!() + ); + // read and execute loginshell file if exists if let Some(mut config_path) = nu_path::config_dir() { config_path.push(NUSHELL_FOLDER); config_path.push(LOGINSHELL_FILE); + warn!("loginshell_file: {}", config_path.display()); + if config_path.exists() { eval_config_contents(config_path, engine_state, stack); } } - - info!("read_loginshell_file {}:{}:{}", file!(), line!(), column!()); } pub(crate) fn read_default_env_file(engine_state: &mut EngineState, stack: &mut Stack) { - trace!("read_default_env_file"); let config_file = get_default_env(); eval_source( engine_state, @@ -147,7 +156,13 @@ pub(crate) fn read_default_env_file(engine_state: &mut EngineState, stack: &mut false, ); - info!("read_config_file {}:{}:{}", file!(), line!(), column!()); + warn!( + "read_default_env_file() env_file_contents: {config_file} {}:{}:{}", + file!(), + line!(), + column!() + ); + // Merge the environment in case env vars changed in the config match engine_state.cwd(Some(stack)) { Ok(cwd) => { @@ -167,10 +182,9 @@ fn eval_default_config( config_file: &str, is_env_config: bool, ) { - trace!( - "eval_default_config: config_file: {:?}, is_env_config: {}", - &config_file, - is_env_config + warn!( + "eval_default_config() config_file_specified: {:?}, is_env_config: {}", + &config_file, is_env_config ); println!("Continuing without config file"); // Just use the contents of "default_config.nu" or "default_env.nu" @@ -208,11 +222,9 @@ pub(crate) fn setup_config( env_file: Option>, is_login_shell: bool, ) { - trace!( - "setup_config: config: {:?}, env: {:?}, login: {}", - &config_file, - &env_file, - is_login_shell + warn!( + "setup_config() config_file_specified: {:?}, env_file_specified: {:?}, login: {}", + &config_file, &env_file, is_login_shell ); let result = catch_unwind(AssertUnwindSafe(|| { #[cfg(feature = "plugin")] @@ -240,12 +252,9 @@ pub(crate) fn set_config_path( key: &str, config_file: Option<&Spanned>, ) { - trace!( - "set_config_path: cwd: {:?}, default_config: {}, key: {}, config_file: {:?}", - &cwd, - &default_config_name, - &key, - &config_file + warn!( + "set_config_path() cwd: {:?}, default_config: {}, key: {}, config_file_specified: {:?}", + &cwd, &default_config_name, &key, &config_file ); let config_path = match config_file { Some(s) => canonicalize_with(&s.item, cwd).ok(), diff --git a/src/main.rs b/src/main.rs index 714b932df8..db0c80d4f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,8 +7,6 @@ mod signals; #[cfg(unix)] mod terminal; mod test_bins; -#[cfg(test)] -mod tests; #[cfg(feature = "mimalloc")] #[global_allocator] diff --git a/tests/main.rs b/tests/main.rs index db44c4e019..6824f26f13 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -11,5 +11,6 @@ mod path; mod plugin_persistence; #[cfg(feature = "plugin")] mod plugins; +mod repl; mod scope; mod shell; diff --git a/tests/plugins/stream.rs b/tests/plugins/stream.rs index ee62703017..8530e5bc32 100644 --- a/tests/plugins/stream.rs +++ b/tests/plugins/stream.rs @@ -1,3 +1,5 @@ +use rstest::rstest; + use nu_test_support::nu_with_plugins; use pretty_assertions::assert_eq; @@ -190,3 +192,18 @@ fn generate_sequence() { assert_eq!(actual.out, "[0,2,4,6,8,10]"); } + +#[rstest] +#[timeout(std::time::Duration::from_secs(6))] +fn echo_interactivity_on_slow_pipelines() { + // This test works by putting 0 on the upstream immediately, followed by 1 after 10 seconds. + // If values aren't streamed to the plugin as they become available, `example echo` won't emit + // anything until both 0 and 1 are available. The desired behavior is that `example echo` gets + // the 0 immediately, which is consumed by `first`, allowing the pipeline to terminate early. + let actual = nu_with_plugins!( + cwd: "tests/fixtures/formats", + plugin: ("nu_plugin_example"), + r#"[1] | each { |n| sleep 10sec; $n } | prepend 0 | example echo | first"# + ); + assert_eq!(actual.out, "0"); +} diff --git a/tests/repl/mod.rs b/tests/repl/mod.rs new file mode 100644 index 0000000000..5ed7f29ba5 --- /dev/null +++ b/tests/repl/mod.rs @@ -0,0 +1,27 @@ +mod test_bits; +mod test_cell_path; +mod test_commandline; +mod test_conditionals; +mod test_config; +mod test_config_path; +mod test_converters; +mod test_custom_commands; +mod test_engine; +mod test_env; +mod test_help; +mod test_hiding; +mod test_ide; +mod test_iteration; +mod test_known_external; +mod test_math; +mod test_modules; +mod test_parser; +mod test_ranges; +mod test_regex; +mod test_signatures; +mod test_spread; +mod test_stdlib; +mod test_strings; +mod test_table_operations; +mod test_type_check; +mod tests; diff --git a/src/tests/test_bits.rs b/tests/repl/test_bits.rs similarity index 97% rename from src/tests/test_bits.rs rename to tests/repl/test_bits.rs index fdb672d023..410f48d83b 100644 --- a/src/tests/test_bits.rs +++ b/tests/repl/test_bits.rs @@ -1,4 +1,4 @@ -use crate::tests::{run_test, TestResult}; +use crate::repl::tests::{run_test, TestResult}; #[test] fn bits_and() -> TestResult { diff --git a/src/tests/test_cell_path.rs b/tests/repl/test_cell_path.rs similarity index 98% rename from src/tests/test_cell_path.rs rename to tests/repl/test_cell_path.rs index 56b6751638..c89c50259b 100644 --- a/src/tests/test_cell_path.rs +++ b/tests/repl/test_cell_path.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test, TestResult}; +use crate::repl::tests::{fail_test, run_test, TestResult}; // Tests for null / null / Value::Nothing #[test] diff --git a/src/tests/test_commandline.rs b/tests/repl/test_commandline.rs similarity index 98% rename from src/tests/test_commandline.rs rename to tests/repl/test_commandline.rs index 130faf933a..0443ee4d6c 100644 --- a/src/tests/test_commandline.rs +++ b/tests/repl/test_commandline.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test, TestResult}; +use crate::repl::tests::{fail_test, run_test, TestResult}; #[test] fn commandline_test_get_empty() -> TestResult { diff --git a/src/tests/test_conditionals.rs b/tests/repl/test_conditionals.rs similarity index 96% rename from src/tests/test_conditionals.rs rename to tests/repl/test_conditionals.rs index 20307ffe65..dd1e16c5a8 100644 --- a/src/tests/test_conditionals.rs +++ b/tests/repl/test_conditionals.rs @@ -1,4 +1,4 @@ -use crate::tests::{run_test, TestResult}; +use crate::repl::tests::{run_test, TestResult}; #[test] fn if_test1() -> TestResult { diff --git a/src/tests/test_config.rs b/tests/repl/test_config.rs similarity index 98% rename from src/tests/test_config.rs rename to tests/repl/test_config.rs index 96bd7f714d..005ae63171 100644 --- a/src/tests/test_config.rs +++ b/tests/repl/test_config.rs @@ -1,5 +1,4 @@ -use super::{fail_test, run_test, run_test_std}; -use crate::tests::TestResult; +use crate::repl::tests::{fail_test, run_test, run_test_std, TestResult}; #[test] fn mutate_nu_config() -> TestResult { diff --git a/src/tests/test_config_path.rs b/tests/repl/test_config_path.rs similarity index 100% rename from src/tests/test_config_path.rs rename to tests/repl/test_config_path.rs diff --git a/src/tests/test_converters.rs b/tests/repl/test_converters.rs similarity index 96% rename from src/tests/test_converters.rs rename to tests/repl/test_converters.rs index c41868539e..cfa165e95d 100644 --- a/src/tests/test_converters.rs +++ b/tests/repl/test_converters.rs @@ -1,4 +1,4 @@ -use crate::tests::{run_test, TestResult}; +use crate::repl::tests::{run_test, TestResult}; #[test] fn from_json_1() -> TestResult { diff --git a/src/tests/test_custom_commands.rs b/tests/repl/test_custom_commands.rs similarity index 98% rename from src/tests/test_custom_commands.rs rename to tests/repl/test_custom_commands.rs index 4cc81878e4..43a24fa250 100644 --- a/src/tests/test_custom_commands.rs +++ b/tests/repl/test_custom_commands.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test, run_test_contains, TestResult}; +use crate::repl::tests::{fail_test, run_test, run_test_contains, TestResult}; use nu_test_support::nu; use pretty_assertions::assert_eq; diff --git a/src/tests/test_engine.rs b/tests/repl/test_engine.rs similarity index 99% rename from src/tests/test_engine.rs rename to tests/repl/test_engine.rs index 9f5be84763..c1eb919afa 100644 --- a/src/tests/test_engine.rs +++ b/tests/repl/test_engine.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test, TestResult}; +use crate::repl::tests::{fail_test, run_test, TestResult}; use rstest::rstest; #[test] diff --git a/src/tests/test_env.rs b/tests/repl/test_env.rs similarity index 91% rename from src/tests/test_env.rs rename to tests/repl/test_env.rs index 159b3d0a20..7963f7580b 100644 --- a/src/tests/test_env.rs +++ b/tests/repl/test_env.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test, TestResult}; +use crate::repl::tests::{fail_test, run_test, TestResult}; use nu_test_support::nu; #[test] diff --git a/src/tests/test_help.rs b/tests/repl/test_help.rs similarity index 94% rename from src/tests/test_help.rs rename to tests/repl/test_help.rs index b529f7cd19..16d168587a 100644 --- a/src/tests/test_help.rs +++ b/tests/repl/test_help.rs @@ -1,4 +1,4 @@ -use crate::tests::{run_test, TestResult}; +use crate::repl::tests::{run_test, TestResult}; use rstest::rstest; #[rstest] diff --git a/src/tests/test_hiding.rs b/tests/repl/test_hiding.rs similarity index 99% rename from src/tests/test_hiding.rs rename to tests/repl/test_hiding.rs index 81484cc51e..ee2016c54d 100644 --- a/src/tests/test_hiding.rs +++ b/tests/repl/test_hiding.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test, TestResult}; +use crate::repl::tests::{fail_test, run_test, TestResult}; // TODO: Test the use/hide tests also as separate lines in REPL (i.e., with merging the delta in between) #[test] diff --git a/src/tests/test_ide.rs b/tests/repl/test_ide.rs similarity index 76% rename from src/tests/test_ide.rs rename to tests/repl/test_ide.rs index b5e40cfe8d..45cc87aa4d 100644 --- a/src/tests/test_ide.rs +++ b/tests/repl/test_ide.rs @@ -1,4 +1,4 @@ -use crate::tests::{test_ide_contains, TestResult}; +use crate::repl::tests::{test_ide_contains, TestResult}; #[test] fn parser_recovers() -> TestResult { diff --git a/src/tests/test_iteration.rs b/tests/repl/test_iteration.rs similarity index 94% rename from src/tests/test_iteration.rs rename to tests/repl/test_iteration.rs index da3eef9ce2..bcc21b9ef9 100644 --- a/src/tests/test_iteration.rs +++ b/tests/repl/test_iteration.rs @@ -1,4 +1,4 @@ -use crate::tests::{run_test, TestResult}; +use crate::repl::tests::{run_test, TestResult}; #[test] fn better_block_types() -> TestResult { diff --git a/src/tests/test_known_external.rs b/tests/repl/test_known_external.rs similarity index 97% rename from src/tests/test_known_external.rs rename to tests/repl/test_known_external.rs index 586ee40cd3..c140f4ee90 100644 --- a/src/tests/test_known_external.rs +++ b/tests/repl/test_known_external.rs @@ -1,7 +1,6 @@ +use crate::repl::tests::{fail_test, run_test, run_test_contains, TestResult}; use std::process::Command; -use crate::tests::{fail_test, run_test, run_test_contains, TestResult}; - // cargo version prints a string of the form: // cargo 1.60.0 (d1fd9fe2c 2022-03-01) diff --git a/src/tests/test_math.rs b/tests/repl/test_math.rs similarity index 98% rename from src/tests/test_math.rs rename to tests/repl/test_math.rs index ed372b198a..af44f8a857 100644 --- a/src/tests/test_math.rs +++ b/tests/repl/test_math.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test, TestResult}; +use crate::repl::tests::{fail_test, run_test, TestResult}; #[test] fn add_simple() -> TestResult { diff --git a/src/tests/test_modules.rs b/tests/repl/test_modules.rs similarity index 98% rename from src/tests/test_modules.rs rename to tests/repl/test_modules.rs index 26ca62bf49..dd62c74b77 100644 --- a/src/tests/test_modules.rs +++ b/tests/repl/test_modules.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test, TestResult}; +use crate::repl::tests::{fail_test, run_test, TestResult}; #[test] fn module_def_imports_1() -> TestResult { diff --git a/src/tests/test_parser.rs b/tests/repl/test_parser.rs similarity index 99% rename from src/tests/test_parser.rs rename to tests/repl/test_parser.rs index 3d19f05a9c..c0e7279b7b 100644 --- a/src/tests/test_parser.rs +++ b/tests/repl/test_parser.rs @@ -1,9 +1,7 @@ -use crate::tests::{fail_test, run_test, run_test_with_env, TestResult}; +use crate::repl::tests::{fail_test, run_test, run_test_contains, run_test_with_env, TestResult}; use nu_test_support::{nu, nu_repl_code}; use std::collections::HashMap; -use super::run_test_contains; - #[test] fn env_shorthand() -> TestResult { run_test("FOO=BAR if false { 3 } else { 4 }", "4") diff --git a/src/tests/test_ranges.rs b/tests/repl/test_ranges.rs similarity index 92% rename from src/tests/test_ranges.rs rename to tests/repl/test_ranges.rs index 3a007196e6..96f59eea84 100644 --- a/src/tests/test_ranges.rs +++ b/tests/repl/test_ranges.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test, TestResult}; +use crate::repl::tests::{fail_test, run_test, TestResult}; #[test] fn int_in_inc_range() -> TestResult { diff --git a/src/tests/test_regex.rs b/tests/repl/test_regex.rs similarity index 96% rename from src/tests/test_regex.rs rename to tests/repl/test_regex.rs index adb59cc4a7..6f9063c81a 100644 --- a/src/tests/test_regex.rs +++ b/tests/repl/test_regex.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test, TestResult}; +use crate::repl::tests::{fail_test, run_test, TestResult}; #[test] fn contains() -> TestResult { diff --git a/src/tests/test_signatures.rs b/tests/repl/test_signatures.rs similarity index 99% rename from src/tests/test_signatures.rs rename to tests/repl/test_signatures.rs index 1ca395c90f..5e265974d5 100644 --- a/src/tests/test_signatures.rs +++ b/tests/repl/test_signatures.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test, TestResult}; +use crate::repl::tests::{fail_test, run_test, TestResult}; #[test] fn list_annotations() -> TestResult { diff --git a/src/tests/test_spread.rs b/tests/repl/test_spread.rs similarity index 98% rename from src/tests/test_spread.rs rename to tests/repl/test_spread.rs index 3dceea7089..419188be33 100644 --- a/src/tests/test_spread.rs +++ b/tests/repl/test_spread.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test, TestResult}; +use crate::repl::tests::{fail_test, run_test, TestResult}; use nu_test_support::nu; #[test] diff --git a/src/tests/test_stdlib.rs b/tests/repl/test_stdlib.rs similarity index 86% rename from src/tests/test_stdlib.rs rename to tests/repl/test_stdlib.rs index 3be252ef77..401fd3c0aa 100644 --- a/src/tests/test_stdlib.rs +++ b/tests/repl/test_stdlib.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test_std, TestResult}; +use crate::repl::tests::{fail_test, run_test_std, TestResult}; #[test] fn library_loaded() -> TestResult { diff --git a/src/tests/test_strings.rs b/tests/repl/test_strings.rs similarity index 91% rename from src/tests/test_strings.rs rename to tests/repl/test_strings.rs index 7d2ae1de84..fa6c419e23 100644 --- a/src/tests/test_strings.rs +++ b/tests/repl/test_strings.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test, TestResult}; +use crate::repl::tests::{fail_test, run_test, TestResult}; #[test] fn cjk_in_substrings() -> TestResult { @@ -154,6 +154,18 @@ fn raw_string_inside_closure() -> TestResult { } #[test] -fn incomplete_raw_string() -> TestResult { - fail_test("r#abc", "expected '") +fn incomplete_string() -> TestResult { + fail_test("r#abc", "expected '")?; + fail_test("r#'bc", "expected closing '#")?; + fail_test("'ab\"", "expected closing '")?; + fail_test("\"ab'", "expected closing \"")?; + fail_test( + r#"def func [] { + { + "A": ""B" # <- the quote is bad + } +} +"#, + "expected closing \"", + ) } diff --git a/src/tests/test_table_operations.rs b/tests/repl/test_table_operations.rs similarity index 99% rename from src/tests/test_table_operations.rs rename to tests/repl/test_table_operations.rs index 9971261190..5d61e0d902 100644 --- a/src/tests/test_table_operations.rs +++ b/tests/repl/test_table_operations.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test, TestResult}; +use crate::repl::tests::{fail_test, run_test, TestResult}; #[test] fn illegal_column_duplication() -> TestResult { diff --git a/src/tests/test_type_check.rs b/tests/repl/test_type_check.rs similarity index 98% rename from src/tests/test_type_check.rs rename to tests/repl/test_type_check.rs index ed912f8fff..87216e6672 100644 --- a/src/tests/test_type_check.rs +++ b/tests/repl/test_type_check.rs @@ -1,4 +1,4 @@ -use crate::tests::{fail_test, run_test, TestResult}; +use crate::repl::tests::{fail_test, run_test, TestResult}; #[test] fn chained_operator_typecheck() -> TestResult { diff --git a/src/tests.rs b/tests/repl/tests.rs similarity index 89% rename from src/tests.rs rename to tests/repl/tests.rs index 487173b9b1..e3a5e89471 100644 --- a/src/tests.rs +++ b/tests/repl/tests.rs @@ -1,30 +1,3 @@ -mod test_bits; -mod test_cell_path; -mod test_commandline; -mod test_conditionals; -mod test_config; -mod test_config_path; -mod test_converters; -mod test_custom_commands; -mod test_engine; -mod test_env; -mod test_help; -mod test_hiding; -mod test_ide; -mod test_iteration; -mod test_known_external; -mod test_math; -mod test_modules; -mod test_parser; -mod test_ranges; -mod test_regex; -mod test_signatures; -mod test_spread; -mod test_stdlib; -mod test_strings; -mod test_table_operations; -mod test_type_check; - use assert_cmd::prelude::*; use pretty_assertions::assert_eq; use std::collections::HashMap;