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 nu_utils::escape_quote_string;
230use std::{
231 ffi::OsStr,
232 process::{Command, Stdio},
233};
234use tempfile::tempdir;
235
236#[derive(Default)]
237pub struct NuOpts {
238 pub cwd: Option<AbsolutePathBuf>,
239 pub locale: Option<String>,
240 pub envs: Option<Vec<(String, String)>>,
241 pub experimental: Option<Vec<String>>,
242 pub collapse_output: Option<bool>,
243 pub env_config: Option<PathBuf>,
247}
248
249pub fn nu_run_test(opts: NuOpts, commands: impl AsRef<str>, with_std: bool) -> Outcome {
250 let test_bins = crate::fs::binaries()
251 .canonicalize()
252 .expect("Could not canonicalize dummy binaries path");
253
254 let mut paths = crate::shell_os_paths();
255 paths.insert(0, test_bins.into());
256
257 let commands = commands.as_ref();
258
259 let paths_joined = match std::env::join_paths(paths) {
260 Ok(all) => all,
261 Err(_) => panic!("Couldn't join paths for PATH var."),
262 };
263
264 let target_cwd = opts.cwd.unwrap_or_else(crate::fs::root);
265 let locale = opts.locale.unwrap_or("en_US.UTF-8".to_string());
266 let executable_path = crate::fs::executable_path();
267
268 let mut command = setup_command(&executable_path, &target_cwd);
269 command
270 .env(nu_utils::locale::LOCALE_OVERRIDE_ENV_VAR, locale)
271 .env(NATIVE_PATH_ENV_VAR, paths_joined);
272
273 if let Some(envs) = opts.envs {
274 command.envs(envs);
275 }
276
277 match opts.env_config {
278 Some(path) => command.arg("--env-config").arg(path),
279 None => command.arg("--no-config-file"),
283 };
284
285 if let Some(experimental_opts) = opts.experimental {
286 let opts = format!("[{}]", experimental_opts.join(","));
287 command.arg(format!("--experimental-options={opts}"));
288 }
289
290 if !with_std {
291 command.arg("--no-std-lib");
292 }
293 command.args(["--error-style", "plain", "--commands", commands]);
295 command.stdout(Stdio::piped()).stderr(Stdio::piped());
296
297 let process = match command.spawn() {
301 Ok(child) => child,
302 Err(why) => panic!("Can't run test {:?} {}", crate::fs::executable_path(), why),
303 };
304
305 let output = process
306 .wait_with_output()
307 .expect("couldn't read from stdout/stderr");
308
309 let out = String::from_utf8_lossy(&output.stdout);
310 let err = String::from_utf8_lossy(&output.stderr);
311
312 let out = if opts.collapse_output.unwrap_or(true) {
313 collapse_output(&out)
314 } else {
315 out.into_owned()
316 };
317
318 println!("=== stderr\n{err}");
319
320 Outcome::new(out, err.into_owned(), output.status)
321}
322
323pub fn nu_with_plugin_run_test<E, K, V>(
324 cwd: impl AsRef<Path>,
325 envs: E,
326 plugins: &[&str],
327 command: &str,
328) -> Outcome
329where
330 E: IntoIterator<Item = (K, V)>,
331 K: AsRef<OsStr>,
332 V: AsRef<OsStr>,
333{
334 let test_bins = crate::fs::binaries();
335 let test_bins = nu_path::canonicalize_with(&test_bins, ".").unwrap_or_else(|e| {
336 panic!(
337 "Couldn't canonicalize dummy binaries path {}: {:?}",
338 test_bins.display(),
339 e
340 )
341 });
342
343 let temp = tempdir().expect("couldn't create a temporary directory");
344 let [temp_config_file, temp_env_config_file] = ["config.nu", "env.nu"].map(|name| {
345 let temp_file = temp.path().join(name);
346 std::fs::File::create(&temp_file).expect("couldn't create temporary config file");
347 temp_file
348 });
349
350 let temp_plugin_file = temp.path().join("plugin.msgpackz");
352
353 crate::commands::ensure_plugins_built();
354
355 let plugin_paths_quoted: Vec<String> = plugins
356 .iter()
357 .map(|plugin_name| {
358 let plugin = with_exe(plugin_name);
359 let plugin_path = nu_path::canonicalize_with(&plugin, &test_bins)
360 .unwrap_or_else(|_| panic!("failed to canonicalize plugin {} path", &plugin));
361 let plugin_path = plugin_path.to_string_lossy();
362 escape_quote_string(&plugin_path)
363 })
364 .collect();
365 let plugins_arg = format!("[{}]", plugin_paths_quoted.join(","));
366
367 let target_cwd = crate::fs::in_directory(&cwd);
368 let mut executable_path = crate::fs::executable_path();
371 if !executable_path.exists() {
372 executable_path = crate::fs::installed_nu_path();
373 }
374
375 let process = match setup_command(&executable_path, &target_cwd)
376 .envs(envs)
377 .arg("--commands")
378 .arg(command)
379 .args(["--error-style", "plain"])
381 .arg("--config")
382 .arg(temp_config_file)
383 .arg("--env-config")
384 .arg(temp_env_config_file)
385 .arg("--plugin-config")
386 .arg(temp_plugin_file)
387 .arg("--plugins")
388 .arg(plugins_arg)
389 .stdout(Stdio::piped())
390 .stderr(Stdio::piped())
391 .spawn()
392 {
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 .current_dir(target_cwd)
431 .env_remove("FILE_PWD")
432 .env("PWD", target_cwd); command
435}