From 4aa910252360745ccfce325fb4f95b0891e5284c Mon Sep 17 00:00:00 2001 From: Stefan Holderbach Date: Thu, 21 Sep 2023 20:11:56 +0200 Subject: [PATCH] Simplify `nu!` test macros. (#10403) # Description Unify the logic between `nu!` and `nu_with_std!`. The inner code actually does not contain any variadic components. So it can safely be abstracted into a function. Similarly simplify the variadic to an array in `nu_with_plugin!` This also seems to simplify the codegen for tests. Comparing the size of the `/target/debug` folder after running: ```sh cargo clean --profile dev cargo build --workspace --tests ``` With this branch a reduction from `8.9GB` to `8.7GB` # User-Facing Changes None # Tests + Formatting No changes necessary --- crates/nu-test-support/src/macros.rs | 425 +++++++++++---------------- 1 file changed, 164 insertions(+), 261 deletions(-) diff --git a/crates/nu-test-support/src/macros.rs b/crates/nu-test-support/src/macros.rs index 3dc2d1095..ca7012e55 100644 --- a/crates/nu-test-support/src/macros.rs +++ b/crates/nu-test-support/src/macros.rs @@ -96,7 +96,7 @@ macro_rules! nu { // Create the NuOpts struct from the `field => value ;` pairs (@nu_opts $( $field:ident => $value:expr ; )*) => { - NuOpts{ + $crate::macros::NuOpts{ $( $field: Some($value), )* @@ -115,97 +115,11 @@ macro_rules! nu { // Do the actual work. (@main $opts:expr, $path:expr) => {{ - pub use std::error::Error; - pub use std::io::prelude::*; - pub use std::process::{Command, Stdio}; - pub use $crate::NATIVE_PATH_ENV_VAR; - - pub fn escape_quote_string(input: String) -> String { - let mut output = String::with_capacity(input.len() + 2); - output.push('"'); - - for c in input.chars() { - if c == '"' || c == '\\' { - output.push('\\'); - } - output.push(c); - } - - output.push('"'); - output - } - - let test_bins = $crate::fs::binaries(); - - let cwd = std::env::current_dir().expect("Could not get current working directory."); - let test_bins = $crate::nu_path::canonicalize_with(&test_bins, cwd).unwrap_or_else(|e| { - panic!( - "Couldn't canonicalize dummy binaries path {}: {:?}", - test_bins.display(), - e - ) - }); - - let mut paths = $crate::shell_os_paths(); - paths.insert(0, test_bins); - - let path = $path.lines().collect::>().join("; "); - - let paths_joined = match std::env::join_paths(paths) { - Ok(all) => all, - Err(_) => panic!("Couldn't join paths for PATH var."), - }; - - let target_cwd = $opts.cwd.unwrap_or(".".to_string()); - let locale = $opts.locale.unwrap_or("en_US.UTF-8".to_string()); - let executable_path = $crate::fs::executable_path(); - - let mut command = $crate::macros::run_command(&executable_path, &target_cwd); - command - .env(nu_utils::locale::LOCALE_OVERRIDE_ENV_VAR, locale) - .env(NATIVE_PATH_ENV_VAR, paths_joined) - // TODO: consider adding custom plugin path for tests to - // not interfere with user local environment - // .arg("--skip-plugins") - // .arg("--no-history") - // .arg("--config-file") - // .arg($crate::fs::DisplayPath::display_path(&$crate::fs::fixtures().join("playground/config/default.toml"))) - .arg("--no-std-lib") - .arg(format!("-c {}", escape_quote_string(path))) - .stdout(Stdio::piped()) - // .stdin(Stdio::piped()) - .stderr(Stdio::piped()); - - let mut process = match command.spawn() - { - Ok(child) => child, - Err(why) => panic!("Can't run test {:?} {}", $crate::fs::executable_path(), why.to_string()), - }; - - // let stdin = process.stdin.as_mut().expect("couldn't open stdin"); - // stdin - // .write_all(b"exit\n") - // .expect("couldn't write to stdin"); - - let output = process - .wait_with_output() - .expect("couldn't read from stdout/stderr"); - - let out = $crate::macros::read_std(&output.stdout); - let err = String::from_utf8_lossy(&output.stderr); - - println!("=== stderr\n{}", err); - - $crate::Outcome::new(out,err.into_owned()) + $crate::macros::nu_run_test($opts, $path, false) }}; // This is the entrypoint for this macro. ($($token:tt)*) => {{ - #[derive(Default)] - struct NuOpts { - cwd: Option, - locale: Option, - } nu!(@options [ ] $($token)*) }}; @@ -258,7 +172,7 @@ macro_rules! nu_with_std { // Create the NuOpts struct from the `field => value ;` pairs (@nu_opts $( $field:ident => $value:expr ; )*) => { - NuOpts{ + $crate::macros::NuOpts{ $( $field: Some($value), )* @@ -277,201 +191,190 @@ macro_rules! nu_with_std { // Do the actual work. (@main $opts:expr, $path:expr) => {{ - pub use std::error::Error; - pub use std::io::prelude::*; - pub use std::process::Stdio; - pub use $crate::NATIVE_PATH_ENV_VAR; - - pub fn escape_quote_string(input: String) -> String { - let mut output = String::with_capacity(input.len() + 2); - output.push('"'); - - for c in input.chars() { - if c == '"' || c == '\\' { - output.push('\\'); - } - output.push(c); - } - - output.push('"'); - output - } - - let test_bins = $crate::fs::binaries(); - - let cwd = std::env::current_dir().expect("Could not get current working directory."); - let test_bins = $crate::nu_path::canonicalize_with(&test_bins, cwd).unwrap_or_else(|e| { - panic!( - "Couldn't canonicalize dummy binaries path {}: {:?}", - test_bins.display(), - e - ) - }); - - let mut paths = $crate::shell_os_paths(); - paths.insert(0, test_bins); - - let path = $path.lines().collect::>().join("; "); - - let paths_joined = match std::env::join_paths(paths) { - Ok(all) => all, - Err(_) => panic!("Couldn't join paths for PATH var."), - }; - - let target_cwd = $opts.cwd.unwrap_or(".".to_string()); - let locale = $opts.locale.unwrap_or("en_US.UTF-8".to_string()); - let executable_path = $crate::fs::executable_path(); - - let mut command = $crate::macros::run_command(&executable_path, &target_cwd); - command - .env(nu_utils::locale::LOCALE_OVERRIDE_ENV_VAR, locale) - .env(NATIVE_PATH_ENV_VAR, paths_joined) - // .arg("--skip-plugins") - // .arg("--no-history") - // .arg("--config-file") - // .arg($crate::fs::DisplayPath::display_path(&$crate::fs::fixtures().join("playground/config/default.toml"))) - .arg(format!("-c {}", escape_quote_string(path))) - .stdout(Stdio::piped()) - // .stdin(Stdio::piped()) - .stderr(Stdio::piped()); - - let mut process = match command.spawn() - { - Ok(child) => child, - Err(why) => panic!("Can't run test {:?} {}", $crate::fs::executable_path(), why.to_string()), - }; - - // let stdin = process.stdin.as_mut().expect("couldn't open stdin"); - // stdin - // .write_all(b"exit\n") - // .expect("couldn't write to stdin"); - - let output = process - .wait_with_output() - .expect("couldn't read from stdout/stderr"); - - let out = $crate::macros::read_std(&output.stdout); - let err = String::from_utf8_lossy(&output.stderr); - - println!("=== stderr\n{}", err); - - $crate::Outcome::new(out,err.into_owned()) + $crate::macros::nu_run_test($opts, $path, true) }}; // This is the entrypoint for this macro. ($($token:tt)*) => {{ - #[derive(Default)] - struct NuOpts { - cwd: Option, - locale: Option, - } - nu!(@options [ ] $($token)*) }}; } -#[macro_export] -macro_rules! with_exe { - ($name:literal) => {{ - #[cfg(windows)] - { - concat!($name, ".exe") - } - #[cfg(not(windows))] - { - $name - } - }}; -} - #[macro_export] macro_rules! nu_with_plugins { (cwd: $cwd:expr, plugins: [$(($plugin_name:expr)),+$(,)?], $command:expr) => {{ - nu_with_plugins!($cwd, [$(("", $plugin_name)),+], $command) + $crate::macros::nu_with_plugin_run_test($cwd, &[$($plugin_name),+], $command) }}; (cwd: $cwd:expr, plugin: ($plugin_name:expr), $command:expr) => {{ - nu_with_plugins!($cwd, [("", $plugin_name)], $command) + $crate::macros::nu_with_plugin_run_test($cwd, &[$plugin_name], $command) }}; - ($cwd:expr, [$(($format:expr, $plugin_name:expr)),+$(,)?], $command:expr) => {{ - pub use std::error::Error; - pub use std::io::prelude::*; - pub use std::process::Stdio; - pub use tempfile::tempdir; - pub use $crate::{NATIVE_PATH_ENV_VAR, with_exe}; - - let test_bins = $crate::fs::binaries(); - let test_bins = nu_path::canonicalize_with(&test_bins, ".").unwrap_or_else(|e| { - panic!( - "Couldn't canonicalize dummy binaries path {}: {:?}", - test_bins.display(), - e - ) - }); - - let temp = tempdir().expect("couldn't create a temporary directory"); - let temp_plugin_file = temp.path().join("plugin.nu"); - std::fs::File::create(&temp_plugin_file).expect("couldn't create temporary plugin file"); - - $crate::commands::ensure_plugins_built(); - - // TODO: the `$format` is a dummy empty string, but `plugin_name` is repeatable - // just keep it here for now. Need to find a way to remove it. - let registrations = format!( - concat!($(concat!("register ", $format, " {};")),+), - $( - nu_path::canonicalize_with(with_exe!($plugin_name), &test_bins) - .unwrap_or_else(|e| { - panic!("failed to canonicalize plugin {} path", $plugin_name) - }) - .display() - ),+ - ); - let commands = format!("{registrations}{}", $command); - - let target_cwd = $crate::fs::in_directory(&$cwd); - // In plugin testing, we need to use installed nushell to drive - // plugin commands. - let mut executable_path = $crate::fs::executable_path(); - if !executable_path.exists() { - executable_path = $crate::fs::installed_nu_path(); - } - let mut process = match $crate::macros::run_command(&executable_path, &target_cwd) - .arg("--commands") - .arg(commands) - .arg("--plugin-config") - .arg(temp_plugin_file) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - { - Ok(child) => child, - Err(why) => panic!("Can't run test {}", why.to_string()), - }; - - let output = process - .wait_with_output() - .expect("couldn't read from stdout/stderr"); - - let out = $crate::macros::read_std(&output.stdout); - let err = String::from_utf8_lossy(&output.stderr); - - println!("=== stderr\n{}", err); - - $crate::Outcome::new(out, err.into_owned()) - }}; } -pub fn read_std(std: &[u8]) -> String { +use crate::{Outcome, NATIVE_PATH_ENV_VAR}; +use std::{ + path::Path, + process::{Command, Stdio}, +}; +use tempfile::tempdir; + +#[derive(Default)] +pub struct NuOpts { + pub cwd: Option, + pub locale: Option, +} + +pub fn nu_run_test(opts: NuOpts, commands: impl AsRef, with_std: bool) -> Outcome { + let test_bins = crate::fs::binaries(); + + let cwd = std::env::current_dir().expect("Could not get current working directory."); + let test_bins = nu_path::canonicalize_with(&test_bins, cwd).unwrap_or_else(|e| { + panic!( + "Couldn't canonicalize dummy binaries path {}: {:?}", + test_bins.display(), + e + ) + }); + + let mut paths = crate::shell_os_paths(); + paths.insert(0, test_bins); + + let commands = commands.as_ref().lines().collect::>().join("; "); + + let paths_joined = match std::env::join_paths(paths) { + Ok(all) => all, + Err(_) => panic!("Couldn't join paths for PATH var."), + }; + + let target_cwd = opts.cwd.unwrap_or(".".to_string()); + let locale = opts.locale.unwrap_or("en_US.UTF-8".to_string()); + let executable_path = crate::fs::executable_path(); + + let mut command = setup_command(&executable_path, &target_cwd); + command + .env(nu_utils::locale::LOCALE_OVERRIDE_ENV_VAR, locale) + .env(NATIVE_PATH_ENV_VAR, paths_joined); + // TODO: consider adding custom plugin path for tests to + // not interfere with user local environment + if !with_std { + command.arg("--no-std-lib"); + } + command + .arg(format!("-c {}", escape_quote_string(commands))) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let process = match command.spawn() { + Ok(child) => child, + Err(why) => panic!("Can't run test {:?} {}", crate::fs::executable_path(), why), + }; + + let output = process + .wait_with_output() + .expect("couldn't read from stdout/stderr"); + + let out = collapse_output(&output.stdout); + let err = String::from_utf8_lossy(&output.stderr); + + println!("=== stderr\n{}", err); + + Outcome::new(out, err.into_owned()) +} + +pub fn nu_with_plugin_run_test(cwd: impl AsRef, plugins: &[&str], command: &str) -> Outcome { + let test_bins = crate::fs::binaries(); + let test_bins = nu_path::canonicalize_with(&test_bins, ".").unwrap_or_else(|e| { + panic!( + "Couldn't canonicalize dummy binaries path {}: {:?}", + test_bins.display(), + e + ) + }); + + let temp = tempdir().expect("couldn't create a temporary directory"); + let temp_plugin_file = temp.path().join("plugin.nu"); + std::fs::File::create(&temp_plugin_file).expect("couldn't create temporary plugin file"); + + crate::commands::ensure_plugins_built(); + + let registrations: String = plugins + .iter() + .map(|plugin_name| { + let plugin = with_exe(plugin_name); + let plugin_path = nu_path::canonicalize_with(&plugin, &test_bins) + .unwrap_or_else(|_| panic!("failed to canonicalize plugin {} path", &plugin)); + let plugin_path = plugin_path.to_string_lossy(); + format!("register {plugin_path};") + }) + .collect(); + let commands = format!("{registrations}{command}"); + + let target_cwd = crate::fs::in_directory(&cwd); + // In plugin testing, we need to use installed nushell to drive + // plugin commands. + let mut executable_path = crate::fs::executable_path(); + if !executable_path.exists() { + executable_path = crate::fs::installed_nu_path(); + } + let process = match setup_command(&executable_path, &target_cwd) + .arg("--commands") + .arg(commands) + .arg("--plugin-config") + .arg(temp_plugin_file) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + { + Ok(child) => child, + Err(why) => panic!("Can't run test {}", why), + }; + + let output = process + .wait_with_output() + .expect("couldn't read from stdout/stderr"); + + let out = collapse_output(&output.stdout); + let err = String::from_utf8_lossy(&output.stderr); + + println!("=== stderr\n{}", err); + + Outcome::new(out, err.into_owned()) +} + +fn escape_quote_string(input: String) -> String { + let mut output = String::with_capacity(input.len() + 2); + output.push('"'); + + for c in input.chars() { + if c == '"' || c == '\\' { + output.push('\\'); + } + output.push(c); + } + + output.push('"'); + output +} + +fn with_exe(name: &str) -> String { + #[cfg(windows)] + { + name.to_string() + ".exe" + } + #[cfg(not(windows))] + { + name.to_string() + } +} + +fn collapse_output(std: &[u8]) -> String { let out = String::from_utf8_lossy(std); let out = out.lines().collect::>().join("\n"); let out = out.replace("\r\n", ""); out.replace('\n', "") } -use std::{path::Path, process::Command}; - -pub fn run_command(executable_path: &Path, target_cwd: &str) -> Command { +fn setup_command(executable_path: &Path, target_cwd: &str) -> Command { let mut command = Command::new(executable_path); command