Skip to main content

nu_test_support/deprecated/
macros.rs

1use kitest::println;
2
3/// Run a command in nu and get its output
4///
5/// The `nu!` macro accepts a number of options like the `cwd` in which the
6/// command should be run. It is also possible to specify a different `locale`
7/// to test locale dependent commands.
8///
9/// Pass options as the first arguments in the form of `key_1: value_1, key_1:
10/// value_2, ...`. The options are defined in the `NuOpts` struct inside the
11/// `nu!` macro.
12///
13/// The command can be formatted using `{}` just like `println!` or `format!`.
14/// Pass the format arguments comma separated after the command itself.
15///
16/// # Examples
17///
18/// ```no_run
19/// # // NOTE: The `nu!` macro needs the `nu` binary to exist. The test are
20/// # //       therefore only compiled but not run (that's what the `no_run` at
21/// # //       the beginning of this code block is for).
22/// #
23/// use nu_test_support::nu;
24///
25/// let outcome = nu!(
26///     "date now | date to-record | get year"
27/// );
28///
29/// let dir = "/";
30/// let outcome = nu!(
31///     "ls {} | get name",
32///     dir,
33/// );
34///
35/// let outcome = nu!(
36///     cwd: "/",
37///     "ls | get name",
38/// );
39///
40/// let cell = "size";
41/// let outcome = nu!(
42///     locale: "de_DE.UTF-8",
43///     "ls | into int {}",
44///     cell,
45/// );
46///
47/// let decimals = 2;
48/// let outcome = nu!(
49///     locale: "de_DE.UTF-8",
50///     "10 | into string --decimals {}",
51///     decimals,
52/// );
53/// ```
54#[macro_export]
55macro_rules! nu {
56    // In the `@options` phase, we restructure all the
57    // `$field_1: $value_1, $field_2: $value_2, ...`
58    // pairs to a structure like
59    // `@options[ $field_1 => $value_1 ; $field_2 => $value_2 ; ... ]`.
60    // We do this to later distinguish the options from the `$path` and `$part`s.
61    // (See
62    //   https://users.rust-lang.org/t/i-dont-think-this-local-ambiguity-when-calling-macro-is-ambiguous/79401?u=x3ro
63    // )
64    //
65    // If there is any special treatment needed for the `$value`, we can just
66    // match for the specific `field` name.
67    (
68        @options [ $($options:tt)* ]
69        cwd: $value:expr,
70        $($rest:tt)*
71    ) => {
72        nu!(@options [ $($options)* cwd => $crate::fs::in_directory($value) ; ] $($rest)*)
73    };
74    // For all other options, we call `.into()` on the `$value` and hope for the best. ;)
75    (
76        @options [ $($options:tt)* ]
77        $field:ident : $value:expr,
78        $($rest:tt)*
79    ) => {
80        nu!(@options [ $($options)* $field => $value.into() ; ] $($rest)*)
81    };
82
83    // When the `$field: $value,` pairs are all parsed, the next tokens are the `$path` and any
84    // number of `$part`s, potentially followed by a trailing comma.
85    (
86        @options [ $($options:tt)* ]
87        $path:expr
88        $(, $part:expr)*
89        $(,)*
90    ) => {{
91        // Here we parse the options into a `NuOpts` struct
92        let opts = nu!(@nu_opts $($options)*);
93        // and format the `$path` using the `$part`s
94        let path = $path;
95        // Then finally we go to the `@main` phase, where the actual work is done.
96        nu!(@main opts, path)
97    }};
98
99    // Create the NuOpts struct from the `field => value ;` pairs
100    (@nu_opts $( $field:ident => $value:expr ; )*) => {
101        $crate::macros::NuOpts{
102            $(
103                $field: Some($value),
104            )*
105            ..Default::default()
106        }
107    };
108
109    // Do the actual work.
110    (@main $opts:expr, $path:expr) => {{
111        $crate::macros::nu_run_test($opts, $path, false)
112    }};
113
114    // This is the entrypoint for this macro.
115    ($($token:tt)*) => {{
116
117        nu!(@options [ ] $($token)*)
118    }};
119}
120
121#[macro_export]
122macro_rules! nu_with_std {
123    // In the `@options` phase, we restructure all the
124    // `$field_1: $value_1, $field_2: $value_2, ...`
125    // pairs to a structure like
126    // `@options[ $field_1 => $value_1 ; $field_2 => $value_2 ; ... ]`.
127    // We do this to later distinguish the options from the `$path` and `$part`s.
128    // (See
129    //   https://users.rust-lang.org/t/i-dont-think-this-local-ambiguity-when-calling-macro-is-ambiguous/79401?u=x3ro
130    // )
131    //
132    // If there is any special treatment needed for the `$value`, we can just
133    // match for the specific `field` name.
134    (
135        @options [ $($options:tt)* ]
136        cwd: $value:expr,
137        $($rest:tt)*
138    ) => {
139        nu_with_std!(@options [ $($options)* cwd => $crate::fs::in_directory($value) ; ] $($rest)*)
140    };
141    // For all other options, we call `.into()` on the `$value` and hope for the best. ;)
142    (
143        @options [ $($options:tt)* ]
144        $field:ident : $value:expr,
145        $($rest:tt)*
146    ) => {
147        nu_with_std!(@options [ $($options)* $field => $value.into() ; ] $($rest)*)
148    };
149
150    // When the `$field: $value,` pairs are all parsed, the next tokens are the `$path` and any
151    // number of `$part`s, potentially followed by a trailing comma.
152    (
153        @options [ $($options:tt)* ]
154        $path:expr
155        $(, $part:expr)*
156        $(,)*
157    ) => {{
158        // Here we parse the options into a `NuOpts` struct
159        let opts = nu_with_std!(@nu_opts $($options)*);
160        // and format the `$path` using the `$part`s
161        let path = nu_with_std!(@format_path $path, $($part),*);
162        // Then finally we go to the `@main` phase, where the actual work is done.
163        nu_with_std!(@main opts, path)
164    }};
165
166    // Create the NuOpts struct from the `field => value ;` pairs
167    (@nu_opts $( $field:ident => $value:expr ; )*) => {
168        $crate::macros::NuOpts{
169            $(
170                $field: Some($value),
171            )*
172            ..Default::default()
173        }
174    };
175
176    // Helper to format `$path`.
177    (@format_path $path:expr $(,)?) => {
178        // When there are no `$part`s, do not format anything
179        $path
180    };
181    (@format_path $path:expr, $($part:expr),* $(,)?) => {{
182        format!($path, $( $part ),*)
183    }};
184
185    // Do the actual work.
186    (@main $opts:expr, $path:expr) => {{
187        $crate::macros::nu_run_test($opts, $path, true)
188    }};
189
190    // This is the entrypoint for this macro.
191    ($($token:tt)*) => {{
192        nu_with_std!(@options [ ] $($token)*)
193    }};
194}
195
196#[macro_export]
197macro_rules! nu_with_plugins {
198    (cwd: $cwd:expr, plugins: [$(($plugin_name:expr)),*$(,)?], $command:expr) => {{
199        nu_with_plugins!(
200            cwd: $cwd,
201            envs: Vec::<(&str, &str)>::new(),
202            plugins: [$(($plugin_name)),*],
203            $command
204        )
205    }};
206    (cwd: $cwd:expr, plugin: ($plugin_name:expr), $command:expr) => {{
207        nu_with_plugins!(
208            cwd: $cwd,
209            envs: Vec::<(&str, &str)>::new(),
210            plugin: ($plugin_name),
211            $command
212        )
213    }};
214
215    (
216        cwd: $cwd:expr,
217        envs: $envs:expr,
218        plugins: [$(($plugin_name:expr)),*$(,)?],
219        $command:expr
220    ) => {{
221        $crate::macros::nu_with_plugin_run_test($cwd, $envs, &[$($plugin_name),*], $command)
222    }};
223    (cwd: $cwd:expr, envs: $envs:expr, plugin: ($plugin_name:expr), $command:expr) => {{
224        $crate::macros::nu_with_plugin_run_test($cwd, $envs, &[$plugin_name], $command)
225    }};
226
227}
228
229use crate::Outcome;
230use nu_path::{AbsolutePath, AbsolutePathBuf, Path, PathBuf};
231use nu_utils::consts::NATIVE_PATH_ENV_VAR;
232use std::{
233    ffi::OsStr,
234    process::{Command, Stdio},
235};
236use tempfile::tempdir;
237
238#[derive(Default)]
239pub struct NuOpts {
240    pub cwd: Option<AbsolutePathBuf>,
241    pub locale: Option<String>,
242    pub envs: Option<Vec<(String, String)>>,
243    pub experimental: Option<Vec<String>>,
244    pub collapse_output: Option<bool>,
245    // Note: At the time this was added, passing in a file path was more convenient. However,
246    // passing in file contents seems like a better API - consider this when adding new uses of
247    // this field.
248    pub env_config: Option<PathBuf>,
249}
250
251pub fn nu_run_test(opts: NuOpts, commands: impl AsRef<str>, with_std: bool) -> Outcome {
252    let test_bins = crate::fs::binaries()
253        .canonicalize()
254        .expect("Could not canonicalize dummy binaries path");
255
256    let mut paths = crate::shell_os_paths();
257    paths.insert(0, test_bins.into());
258
259    let commands = commands.as_ref();
260
261    let paths_joined = match std::env::join_paths(paths) {
262        Ok(all) => all,
263        Err(_) => panic!("Couldn't join paths for PATH var."),
264    };
265
266    let target_cwd = opts.cwd.unwrap_or_else(crate::fs::root);
267    let locale = opts.locale.unwrap_or("en_US.UTF-8".to_string());
268    let executable_path = crate::fs::executable_path();
269
270    let mut command = setup_command(&executable_path, &target_cwd);
271    command
272        .env(nu_utils::locale::LOCALE_OVERRIDE_ENV_VAR, locale)
273        .env(NATIVE_PATH_ENV_VAR, paths_joined);
274
275    if let Some(envs) = opts.envs {
276        command.envs(envs);
277    }
278
279    match opts.env_config {
280        Some(path) => command.arg("--env-config").arg(path),
281        // TODO: This seems unnecessary: the code that runs for integration tests
282        // (run_commands) loads startup configs only if they are specified via flags explicitly or
283        // the shell is started as logging shell (which it is not in this case).
284        None => command.arg("--no-config-file"),
285    };
286
287    if let Some(experimental_opts) = opts.experimental {
288        let opts = format!("[{}]", experimental_opts.join(","));
289        command.arg(format!("--experimental-options={opts}"));
290    }
291
292    if !with_std {
293        command.arg("--no-std-lib");
294    }
295    // Use plain errors to help make error text matching more consistent
296    command.args(["--error-style", "plain", "--commands", commands]);
297    command.stdout(Stdio::piped()).stderr(Stdio::piped());
298
299    // Uncomment to debug the command being run:
300    // println!("=== command\n{command:#?}\n");
301
302    let process = match command.spawn() {
303        Ok(child) => child,
304        Err(why) => panic!("Can't run test {:?} {}", crate::fs::executable_path(), why),
305    };
306
307    let output = process
308        .wait_with_output()
309        .expect("couldn't read from stdout/stderr");
310
311    let out = String::from_utf8_lossy(&output.stdout);
312    let err = String::from_utf8_lossy(&output.stderr);
313
314    let out = if opts.collapse_output.unwrap_or(true) {
315        collapse_output(&out)
316    } else {
317        out.into_owned()
318    };
319
320    println!("=== stderr\n{err}");
321
322    Outcome::new(out, err.into_owned(), output.status)
323}
324
325pub fn nu_with_plugin_run_test<E, K, V>(
326    cwd: impl AsRef<Path>,
327    envs: E,
328    plugins: &[&str],
329    command: &str,
330) -> Outcome
331where
332    E: IntoIterator<Item = (K, V)>,
333    K: AsRef<OsStr>,
334    V: AsRef<OsStr>,
335{
336    let test_bins = crate::fs::binaries();
337    let test_bins = nu_path::canonicalize_with(&test_bins, ".").unwrap_or_else(|e| {
338        panic!(
339            "Couldn't canonicalize dummy binaries path {}: {:?}",
340            test_bins.display(),
341            e
342        )
343    });
344
345    let temp = tempdir().expect("couldn't create a temporary directory");
346    let [temp_config_file, temp_env_config_file] = ["config.nu", "env.nu"].map(|name| {
347        let temp_file = temp.path().join(name);
348        std::fs::File::create(&temp_file).expect("couldn't create temporary config file");
349        temp_file
350    });
351
352    // We don't have to write the plugin registry file, it's ok for it to not exist
353    let temp_plugin_file = temp.path().join("plugin.msgpackz");
354
355    crate::commands::ensure_plugins_built();
356
357    let plugin_paths: Vec<std::path::PathBuf> = plugins
358        .iter()
359        .map(|plugin_name| {
360            let plugin = with_exe(plugin_name);
361            nu_path::canonicalize_with(&plugin, &test_bins)
362                .unwrap_or_else(|_| panic!("failed to canonicalize plugin {} path", &plugin))
363        })
364        .collect();
365
366    let target_cwd = crate::fs::in_directory(&cwd);
367    // In plugin testing, we need to use installed nushell to drive
368    // plugin commands.
369    let mut executable_path = crate::fs::executable_path();
370    if !executable_path.exists() {
371        executable_path = crate::fs::installed_nu_path();
372    }
373
374    let mut cmd = setup_command(&executable_path, &target_cwd);
375    cmd.envs(envs)
376        .arg("--commands")
377        .arg(command)
378        // Use plain errors to help make error text matching more consistent
379        .args(["--error-style", "plain"])
380        .arg("--config")
381        .arg(temp_config_file)
382        .arg("--env-config")
383        .arg(temp_env_config_file)
384        .arg("--plugin-config")
385        .arg(temp_plugin_file);
386
387    // Add each plugin path as a separate argument after --plugins
388    if !plugin_paths.is_empty() {
389        cmd.arg("--plugins");
390        for path in &plugin_paths {
391            cmd.arg(path);
392        }
393    }
394
395    let process = match cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn() {
396        Ok(child) => child,
397        Err(why) => panic!("Can't run test {why}"),
398    };
399
400    let output = process
401        .wait_with_output()
402        .expect("couldn't read from stdout/stderr");
403
404    let out = collapse_output(&String::from_utf8_lossy(&output.stdout));
405    let err = String::from_utf8_lossy(&output.stderr);
406
407    println!("=== stderr\n{err}");
408
409    Outcome::new(out, err.into_owned(), output.status)
410}
411
412fn with_exe(name: &str) -> String {
413    #[cfg(windows)]
414    {
415        name.to_string() + ".exe"
416    }
417    #[cfg(not(windows))]
418    {
419        name.to_string()
420    }
421}
422
423fn collapse_output(out: &str) -> String {
424    let out = out.lines().collect::<Vec<_>>().join("\n");
425    let out = out.replace("\r\n", "");
426    out.replace('\n', "")
427}
428
429fn setup_command(executable_path: &AbsolutePath, target_cwd: &AbsolutePath) -> Command {
430    let mut command = Command::new(executable_path);
431
432    command
433        .env_clear()
434        .current_dir(target_cwd)
435        .env_remove("FILE_PWD")
436        .env("PWD", target_cwd); // setting PWD is enough to set cwd;
437
438    // Need these extra environments from before the environment is cleared.
439    let envs: std::collections::HashMap<String, String> = std::env::vars()
440        .filter(|(n, _)| {
441            n.starts_with("System") // System variables for disks, paths, etc.
442                || n == "NUSHELL_CARGO_PROFILE" // Variable for crate::fs::binaries()
443                || n == "PATHEXT" // Needed for Windows translate `nu` to `.../nu.exe`
444                || n == "TMP"
445                || n == "TEMP"
446                || n == "USERPROFILE"
447                || n == "TMPDIR"
448                || n.starts_with("CARGO_")
449                || n.starts_with("RUSTUP_")
450        })
451        .collect();
452
453    #[cfg(windows)]
454    let mut envs = envs;
455
456    #[cfg(windows)]
457    if let Some(pathext) = envs.get_mut("PATHEXT")
458        && !pathext.to_uppercase().contains(".PS1")
459    {
460        pathext.push_str(";.PS1");
461    }
462
463    command.envs(envs);
464
465    command
466}