1#[macro_export]
53macro_rules! nu {
54 (
66 @options [ $($options:tt)* ]
67 cwd: $value:expr,
68 $($rest:tt)*
69 ) => {
70 nu!(@options [ $($options)* cwd => $crate::fs::in_directory($value) ; ] $($rest)*)
71 };
72 (
74 @options [ $($options:tt)* ]
75 $field:ident : $value:expr,
76 $($rest:tt)*
77 ) => {
78 nu!(@options [ $($options)* $field => $value.into() ; ] $($rest)*)
79 };
80
81 (
84 @options [ $($options:tt)* ]
85 $path:expr
86 $(, $part:expr)*
87 $(,)*
88 ) => {{
89 let opts = nu!(@nu_opts $($options)*);
91 let path = $path;
93 nu!(@main opts, path)
95 }};
96
97 (@nu_opts $( $field:ident => $value:expr ; )*) => {
99 $crate::macros::NuOpts{
100 $(
101 $field: Some($value),
102 )*
103 ..Default::default()
104 }
105 };
106
107 (@main $opts:expr, $path:expr) => {{
109 $crate::macros::nu_run_test($opts, $path, false)
110 }};
111
112 ($($token:tt)*) => {{
114
115 nu!(@options [ ] $($token)*)
116 }};
117}
118
119#[macro_export]
120macro_rules! nu_with_std {
121 (
133 @options [ $($options:tt)* ]
134 cwd: $value:expr,
135 $($rest:tt)*
136 ) => {
137 nu_with_std!(@options [ $($options)* cwd => $crate::fs::in_directory($value) ; ] $($rest)*)
138 };
139 (
141 @options [ $($options:tt)* ]
142 $field:ident : $value:expr,
143 $($rest:tt)*
144 ) => {
145 nu_with_std!(@options [ $($options)* $field => $value.into() ; ] $($rest)*)
146 };
147
148 (
151 @options [ $($options:tt)* ]
152 $path:expr
153 $(, $part:expr)*
154 $(,)*
155 ) => {{
156 let opts = nu_with_std!(@nu_opts $($options)*);
158 let path = nu_with_std!(@format_path $path, $($part),*);
160 nu_with_std!(@main opts, path)
162 }};
163
164 (@nu_opts $( $field:ident => $value:expr ; )*) => {
166 $crate::macros::NuOpts{
167 $(
168 $field: Some($value),
169 )*
170 ..Default::default()
171 }
172 };
173
174 (@format_path $path:expr $(,)?) => {
176 $path
178 };
179 (@format_path $path:expr, $($part:expr),* $(,)?) => {{
180 format!($path, $( $part ),*)
181 }};
182
183 (@main $opts:expr, $path:expr) => {{
185 $crate::macros::nu_run_test($opts, $path, true)
186 }};
187
188 ($($token:tt)*) => {{
190 nu_with_std!(@options [ ] $($token)*)
191 }};
192}
193
194#[macro_export]
195macro_rules! nu_with_plugins {
196 (cwd: $cwd:expr, plugins: [$(($plugin_name:expr)),*$(,)?], $command:expr) => {{
197 nu_with_plugins!(
198 cwd: $cwd,
199 envs: Vec::<(&str, &str)>::new(),
200 plugins: [$(($plugin_name)),*],
201 $command
202 )
203 }};
204 (cwd: $cwd:expr, plugin: ($plugin_name:expr), $command:expr) => {{
205 nu_with_plugins!(
206 cwd: $cwd,
207 envs: Vec::<(&str, &str)>::new(),
208 plugin: ($plugin_name),
209 $command
210 )
211 }};
212
213 (
214 cwd: $cwd:expr,
215 envs: $envs:expr,
216 plugins: [$(($plugin_name:expr)),*$(,)?],
217 $command:expr
218 ) => {{
219 $crate::macros::nu_with_plugin_run_test($cwd, $envs, &[$($plugin_name),*], $command)
220 }};
221 (cwd: $cwd:expr, envs: $envs:expr, plugin: ($plugin_name:expr), $command:expr) => {{
222 $crate::macros::nu_with_plugin_run_test($cwd, $envs, &[$plugin_name], $command)
223 }};
224
225}
226
227use crate::{NATIVE_PATH_ENV_VAR, Outcome};
228use nu_path::{AbsolutePath, AbsolutePathBuf, Path, PathBuf};
229use std::{
230 ffi::OsStr,
231 process::{Command, Stdio},
232};
233use tempfile::tempdir;
234
235#[derive(Default)]
236pub struct NuOpts {
237 pub cwd: Option<AbsolutePathBuf>,
238 pub locale: Option<String>,
239 pub envs: Option<Vec<(String, String)>>,
240 pub experimental: Option<Vec<String>>,
241 pub collapse_output: Option<bool>,
242 pub env_config: Option<PathBuf>,
246}
247
248pub fn nu_run_test(opts: NuOpts, commands: impl AsRef<str>, with_std: bool) -> Outcome {
249 let test_bins = crate::fs::binaries()
250 .canonicalize()
251 .expect("Could not canonicalize dummy binaries path");
252
253 let mut paths = crate::shell_os_paths();
254 paths.insert(0, test_bins.into());
255
256 let commands = commands.as_ref();
257
258 let paths_joined = match std::env::join_paths(paths) {
259 Ok(all) => all,
260 Err(_) => panic!("Couldn't join paths for PATH var."),
261 };
262
263 let target_cwd = opts.cwd.unwrap_or_else(crate::fs::root);
264 let locale = opts.locale.unwrap_or("en_US.UTF-8".to_string());
265 let executable_path = crate::fs::executable_path();
266
267 let mut command = setup_command(&executable_path, &target_cwd);
268 command
269 .env(nu_utils::locale::LOCALE_OVERRIDE_ENV_VAR, locale)
270 .env(NATIVE_PATH_ENV_VAR, paths_joined);
271
272 if let Some(envs) = opts.envs {
273 command.envs(envs);
274 }
275
276 match opts.env_config {
277 Some(path) => command.arg("--env-config").arg(path),
278 None => command.arg("--no-config-file"),
282 };
283
284 if let Some(experimental_opts) = opts.experimental {
285 let opts = format!("[{}]", experimental_opts.join(","));
286 command.arg(format!("--experimental-options={opts}"));
287 }
288
289 if !with_std {
290 command.arg("--no-std-lib");
291 }
292 command.args(["--error-style", "plain", "--commands", commands]);
294 command.stdout(Stdio::piped()).stderr(Stdio::piped());
295
296 let process = match command.spawn() {
300 Ok(child) => child,
301 Err(why) => panic!("Can't run test {:?} {}", crate::fs::executable_path(), why),
302 };
303
304 let output = process
305 .wait_with_output()
306 .expect("couldn't read from stdout/stderr");
307
308 let out = String::from_utf8_lossy(&output.stdout);
309 let err = String::from_utf8_lossy(&output.stderr);
310
311 let out = if opts.collapse_output.unwrap_or(true) {
312 collapse_output(&out)
313 } else {
314 out.into_owned()
315 };
316
317 println!("=== stderr\n{err}");
318
319 Outcome::new(out, err.into_owned(), output.status)
320}
321
322pub fn nu_with_plugin_run_test<E, K, V>(
323 cwd: impl AsRef<Path>,
324 envs: E,
325 plugins: &[&str],
326 command: &str,
327) -> Outcome
328where
329 E: IntoIterator<Item = (K, V)>,
330 K: AsRef<OsStr>,
331 V: AsRef<OsStr>,
332{
333 let test_bins = crate::fs::binaries();
334 let test_bins = nu_path::canonicalize_with(&test_bins, ".").unwrap_or_else(|e| {
335 panic!(
336 "Couldn't canonicalize dummy binaries path {}: {:?}",
337 test_bins.display(),
338 e
339 )
340 });
341
342 let temp = tempdir().expect("couldn't create a temporary directory");
343 let [temp_config_file, temp_env_config_file] = ["config.nu", "env.nu"].map(|name| {
344 let temp_file = temp.path().join(name);
345 std::fs::File::create(&temp_file).expect("couldn't create temporary config file");
346 temp_file
347 });
348
349 let temp_plugin_file = temp.path().join("plugin.msgpackz");
351
352 crate::commands::ensure_plugins_built();
353
354 let plugin_paths: Vec<std::path::PathBuf> = plugins
355 .iter()
356 .map(|plugin_name| {
357 let plugin = with_exe(plugin_name);
358 nu_path::canonicalize_with(&plugin, &test_bins)
359 .unwrap_or_else(|_| panic!("failed to canonicalize plugin {} path", &plugin))
360 })
361 .collect();
362
363 let target_cwd = crate::fs::in_directory(&cwd);
364 let mut executable_path = crate::fs::executable_path();
367 if !executable_path.exists() {
368 executable_path = crate::fs::installed_nu_path();
369 }
370
371 let mut cmd = setup_command(&executable_path, &target_cwd);
372 cmd.envs(envs)
373 .arg("--commands")
374 .arg(command)
375 .args(["--error-style", "plain"])
377 .arg("--config")
378 .arg(temp_config_file)
379 .arg("--env-config")
380 .arg(temp_env_config_file)
381 .arg("--plugin-config")
382 .arg(temp_plugin_file);
383
384 if !plugin_paths.is_empty() {
386 cmd.arg("--plugins");
387 for path in &plugin_paths {
388 cmd.arg(path);
389 }
390 }
391
392 let process = match cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn() {
393 Ok(child) => child,
394 Err(why) => panic!("Can't run test {why}"),
395 };
396
397 let output = process
398 .wait_with_output()
399 .expect("couldn't read from stdout/stderr");
400
401 let out = collapse_output(&String::from_utf8_lossy(&output.stdout));
402 let err = String::from_utf8_lossy(&output.stderr);
403
404 println!("=== stderr\n{err}");
405
406 Outcome::new(out, err.into_owned(), output.status)
407}
408
409fn with_exe(name: &str) -> String {
410 #[cfg(windows)]
411 {
412 name.to_string() + ".exe"
413 }
414 #[cfg(not(windows))]
415 {
416 name.to_string()
417 }
418}
419
420fn collapse_output(out: &str) -> String {
421 let out = out.lines().collect::<Vec<_>>().join("\n");
422 let out = out.replace("\r\n", "");
423 out.replace('\n', "")
424}
425
426fn setup_command(executable_path: &AbsolutePath, target_cwd: &AbsolutePath) -> Command {
427 let mut command = Command::new(executable_path);
428
429 command
430 .env_clear()
431 .current_dir(target_cwd)
432 .env_remove("FILE_PWD")
433 .env("PWD", target_cwd); let envs: std::collections::HashMap<String, String> = std::env::vars()
437 .filter(|(n, _)| {
438 n.starts_with("System") || n == "NUSHELL_CARGO_PROFILE" || n == "PATHEXT" || n == "TMP"
442 || n == "TEMP"
443 || n == "USERPROFILE"
444 || n == "TMPDIR"
445 || n.starts_with("CARGO_")
446 || n.starts_with("RUSTUP_")
447 })
448 .collect();
449
450 #[cfg(windows)]
451 let mut envs = envs;
452
453 #[cfg(windows)]
454 if let Some(pathext) = envs.get_mut("PATHEXT")
455 && !pathext.to_uppercase().contains(".PS1")
456 {
457 pathext.push_str(";.PS1");
458 }
459
460 command.envs(envs);
461
462 command
463}