Skip to main content

nu_test_support/tester/
mod.rs

1use std::{
2    env,
3    error::Error,
4    fmt::{Debug, Display},
5    io,
6    panic::Location,
7    path::{self, Path, PathBuf},
8    sync::{Arc, LazyLock},
9};
10
11use miette::Diagnostic;
12use nu_cmd_base::hook::eval_repl_hooks;
13use nu_protocol::{
14    CompileError, Config, FromValue, IntoValue, LabeledError, ParseError, PipelineData,
15    PipelineExecutionData, ShellError, Span, Value,
16    ast::Block,
17    debugger::WithoutDebug,
18    engine::{Command, EngineState, Stack, StateDelta, StateWorkingSet},
19    shell_error::{io::IoError, network::NetworkError},
20};
21use nu_utils::{consts::ENV_PATH_SEPARATOR_CHAR, sync::KeyedLazyLock};
22use parking_lot::{RwLock, const_rwlock};
23
24use crate::harness::group::GroupKey;
25
26#[cfg(feature = "plugin")]
27use nu_plugin_engine::{GetPlugin, PersistentPlugin, PluginDeclaration};
28#[cfg(feature = "plugin")]
29use nu_protocol::{PluginIdentity, PluginSignature, RegisteredPlugin};
30
31/// Workspace root.
32///
33/// Default starting cwd for [`test()`].
34pub static WORKSPACE_ROOT: LazyLock<PathBuf> = LazyLock::new(|| {
35    path::absolute(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
36        .expect("could not absolutize root")
37});
38
39// By using different engine states depending on the group key, we can ensure that behavior from
40// experimental options or environment variables take proper effect in the setup of an engine state.
41static INITIAL_ENGINE_STATES: KeyedLazyLock<GroupKey, EngineState> = KeyedLazyLock::new(|_| {
42    // Some modules below are commented out because they don't depend on nu-test-support
43    // Copied from `nu::command_context::add_command_context`
44    let engine_state = nu_cmd_lang::create_default_context();
45    #[cfg(feature = "plugin")]
46    let engine_state = nu_cmd_plugin::add_plugin_command_context(engine_state);
47    let engine_state = nu_command::add_shell_command_context(engine_state);
48    let engine_state = nu_cmd_extra::add_extra_command_context(engine_state);
49    #[cfg(feature = "os")]
50    let engine_state = nu_cli::add_cli_context(engine_state);
51    // let engine_state = nu_explore::add_explore_context(engine_state);
52
53    // Make `engine_state` mutable without fiddling with features
54    let mut engine_state = engine_state;
55
56    engine_state.generate_nu_constant();
57    [
58        ("PWD", Value::test_string(WORKSPACE_ROOT.to_string_lossy())),
59        ("config", Config::default().into_value(Span::unknown())),
60        ("NO_COLOR", Value::test_bool(true)),
61    ]
62    .into_iter()
63    .for_each(|(key, val)| engine_state.add_env_var(key.into(), val));
64
65    // Should this be inherited or do we want tighter testing?
66    #[cfg(windows)]
67    if let Ok(path_ext) = env::var("PATHEXT") {
68        engine_state.add_env_var("PATHEXT".into(), Value::test_string(path_ext));
69    }
70
71    nu_std::load_standard_library(&mut engine_state).expect("could not load standard library");
72
73    engine_state
74});
75
76/// Plugin auto loader for [`PLUGIN_AUTO_LOAD`].
77#[cfg(feature = "plugin")]
78#[derive(Debug, Clone)]
79pub struct PluginAutoLoader {
80    pub identity: Arc<PluginIdentity>,
81    pub plugin: Option<Arc<PersistentPlugin>>,
82    pub signatures: Option<Arc<[PluginSignature]>>,
83}
84
85/// Paths to be loaded into the PATH env variable of a [`NuTester`].
86pub static PATH_ENV_AUTO_LOAD: RwLock<Vec<PathBuf>> = const_rwlock(Vec::new());
87
88/// Plugins to be automatically loaded into a [`NuTester`].
89#[cfg(feature = "plugin")]
90pub static PLUGIN_AUTO_LOAD: RwLock<Vec<PluginAutoLoader>> = const_rwlock(Vec::new());
91
92/// Create a [`NuTester`] for running Nushell snippets in tests.
93///
94/// Prefer this helper over the `nu!` macro for most tests.
95/// It runs snippets in-process instead of shelling out to a subprocess, which makes tests faster
96/// and lets you pass and read values directly without inferring from stdout or stderr.
97/// The `nu!` macro executes the `nu` binary, and changes in a single crate might not trigger a
98/// rebuild of that binary, so tests can run against stale behavior unless you run `cargo build`
99/// first.
100/// Using this helper avoids that by executing against the in-process engine components.
101///
102/// The tester starts from a default [`EngineState`] with the standard library loaded, and a fresh
103/// [`Stack`].
104/// Use the returned value to configure environment variables or the working directory before
105/// running code.
106///
107/// # Environment behavior
108///
109/// - This tester does not inherit process environment variables.
110/// - Any variables you want available to the engine must be added explicitly via
111///   [`NuTester::env`] (or convenience helpers like [`NuTester::locale`]).
112/// - Experimental options and other external environment settings are respected
113///   when constructing the underlying engine state for the current test group.
114///
115/// # Auto loaders
116///
117/// The `*_AUTO_LOAD` statics automatically prepare the test [`EngineState`].
118///
119/// [`PATH_ENV_AUTO_LOAD`] loads paths into the tester's `PATH` environment variable, allowing
120/// binaries to be found without adding them manually in each [`test()`].
121///
122/// When the `plugin` feature is enabled,
123#[cfg_attr(feature = "plugin", doc = "[`PLUGIN_AUTO_LOAD`]")]
124#[cfg_attr(not(feature = "plugin"), doc = "`PLUGIN_AUTO_LOAD`")]
125/// loads plugins into the tester so they can be called during tests.
126///
127/// # Examples
128///
129/// ```rust
130/// use nu_test_support::prelude::*;
131///
132/// let code = "use std/util ellie; ellie | ansi strip";
133/// let value: String = test().run(code)?;
134/// assert_eq!(value, r#"
135///      __  ,
136///  .--()°'.'
137/// '|, . ,'
138///  !_-(_\
139/// "#.trim_matches('\n'));
140/// # Ok::<(), nu_test_support::tester::TestError>(())
141/// ```
142///
143/// ```rust
144/// use nu_test_support::prelude::*;
145///
146/// let mut tester = test()
147///     .env("FOO", "bar")
148///     .cwd("crates/nu-test-support");
149///
150/// let value: String = tester.run("$env.FOO")?;
151/// assert_eq!(value, "bar");
152/// # Ok::<(), nu_test_support::tester::TestError>(())
153/// ```
154pub fn test() -> NuTester {
155    let mut engine_state = INITIAL_ENGINE_STATES.get(&GroupKey::current()).clone();
156    engine_state.make_session_state_unique();
157
158    let tester = NuTester {
159        engine_state,
160        stack: Stack::new().collect_value(),
161        fname_counter: Counter::default(),
162    };
163
164    let tester = tester.append_path(&*PATH_ENV_AUTO_LOAD.read());
165
166    #[cfg(feature = "plugin")]
167    let tester = tester.auto_load_plugins();
168
169    tester
170}
171
172/// Helper for running Nushell code in tests.
173///
174/// `NuTester` owns an [`EngineState`] and [`Stack`] that are reused across invocations.
175/// Configuration methods update the engine state before execution.
176#[derive(Clone)]
177#[non_exhaustive] // Ensure this type is only generated using `test()`, `new()` or `default()`.
178pub struct NuTester {
179    pub engine_state: EngineState,
180    pub stack: Stack,
181
182    /// Counter that is used for parsing source code with different "file names".
183    fname_counter: Counter,
184}
185
186#[derive(Default, Clone)]
187struct Counter(u64);
188
189impl Counter {
190    pub fn get(&mut self) -> u64 {
191        let value = self.0;
192        self.0 += 1;
193        value
194    }
195}
196
197impl Default for NuTester {
198    /// Create a default tester.
199    ///
200    /// Prefer [`test()`] for a shorter entry point that avoids naming [`NuTester`].
201    fn default() -> Self {
202        test()
203    }
204}
205
206#[cfg(feature = "plugin")]
207impl NuTester {
208    /// Load the plugins from [`PLUGIN_AUTO_LOAD`] into the [`NuTester`].
209    ///
210    /// Called in [`test`], do not call somewhere else again.
211    fn auto_load_plugins(self) -> Self {
212        let mut tester = self;
213        let auto_loaders = PLUGIN_AUTO_LOAD.read();
214        if auto_loaders.is_empty() {
215            return tester;
216        }
217
218        let mut working_set = StateWorkingSet::new(&tester.engine_state);
219        for auto_loader in auto_loaders.iter() {
220            let plugin = working_set.find_or_create_plugin(&auto_loader.identity, || {
221                auto_loader
222                    .plugin
223                    .as_ref()
224                    .map(|plugin| plugin.clone())
225                    .unwrap_or_else(|| {
226                        Arc::new(PersistentPlugin::new(
227                            (*auto_loader.identity).clone(),
228                            Default::default(),
229                        ))
230                    })
231            });
232
233            let plugin: Arc<PersistentPlugin> = plugin
234                .as_any()
235                .downcast()
236                .expect("could not downcast to persistent plugin");
237
238            // if preloaded by our test harness, we don't need to construct a plugin
239            // interface here
240            let mut interface = None;
241
242            // our test harness also sets metadata, so we don't have to do again
243            if plugin.metadata().is_none() {
244                let interface = interface.get_or_insert_with(|| {
245                    plugin
246                        .clone()
247                        .get_plugin(None)
248                        .expect("could not get plugin")
249                });
250
251                plugin.set_metadata(Some(
252                    interface
253                        .get_metadata()
254                        .expect("could not get plugin metadata"),
255                ));
256            }
257
258            // our test harness also preloads signatures, assuming they don't change
259            let signatures = auto_loader
260                .signatures
261                .as_deref()
262                .map(|signatures| signatures.to_owned())
263                .unwrap_or_else(|| {
264                    let interface = interface.get_or_insert_with(|| {
265                        plugin
266                            .clone()
267                            .get_plugin(None)
268                            .expect("could not get plugin")
269                    });
270                    interface
271                        .get_signature()
272                        .expect("could not get plugin signatures")
273                });
274
275            for signature in signatures {
276                let decl = PluginDeclaration::new(plugin.clone(), signature);
277                working_set.add_decl(Box::new(decl));
278            }
279        }
280
281        tester
282            .engine_state
283            .merge_delta(working_set.render())
284            .expect("could not merge plugin working set");
285
286        tester
287    }
288}
289
290impl NuTester {
291    /// Create a default tester with the standard engine state.
292    ///
293    /// Prefer [`test()`] for a shorter entry point that avoids naming [`NuTester`].
294    pub fn new() -> Self {
295        test()
296    }
297
298    /// Set the working directory used for evaluation.
299    ///
300    /// Relative paths are resolved from the repository root and canonicalized.
301    pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
302        let cwd = cwd.into();
303
304        let cwd = match cwd.is_absolute() {
305            true => cwd,
306            false => WORKSPACE_ROOT
307                .join(cwd)
308                .canonicalize()
309                .expect("could not canonicalize path"),
310        };
311
312        self.engine_state
313            .add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
314        self
315    }
316
317    /// Set the locale used by tests via `NU_TEST_LOCALE_OVERRIDE`.
318    pub fn locale(mut self, locale: impl Into<String>) -> Self {
319        self.engine_state.add_env_var(
320            "NU_TEST_LOCALE_OVERRIDE".into(),
321            Value::test_string(locale.into()),
322        );
323        self
324    }
325
326    /// Set the locale to `en_US.utf8`.
327    pub fn locale_en(self) -> Self {
328        self.locale("en_US.utf8")
329    }
330
331    /// Get the current path env.
332    fn path(&self) -> Vec<Value> {
333        match self.engine_state.get_env_var("PATH") {
334            None => Vec::new(),
335            Some(Value::List { vals, .. }) => vals.to_vec(),
336            Some(Value::String { val, .. }) => val
337                .split(ENV_PATH_SEPARATOR_CHAR)
338                .map(Value::test_string)
339                .collect(),
340            Some(v) => panic!("PATH is neither a list nor a string, is {}", v.get_type()),
341        }
342    }
343
344    /// Prepend entries to the PATH.
345    pub fn prepend_path(self, entries: impl IntoIterator<Item = impl AsRef<Path>>) -> Self {
346        let path = entries
347            .into_iter()
348            .map(|item| Value::test_string(item.as_ref().to_string_lossy()))
349            .chain(self.path())
350            .collect();
351        self.env("PATH", Value::test_list(path))
352    }
353
354    /// Append entries to the PATH.
355    pub fn append_path(self, entries: impl IntoIterator<Item = impl AsRef<Path>>) -> Self {
356        let path = self
357            .path()
358            .into_iter()
359            .chain(
360                entries
361                    .into_iter()
362                    .map(|item| Value::test_string(item.as_ref().to_string_lossy())),
363            )
364            .collect();
365        self.env("PATH", Value::test_list(path))
366    }
367
368    /// Inherit the `PATH` environment variable from the running process by appending it.
369    ///
370    /// This is useful for tests that spawn external commands and should resolve
371    /// binaries the same way as the parent test process.
372    ///
373    /// Panics if `PATH` is not set in the current process environment.
374    pub fn inherit_path(self) -> Self {
375        let path = env::var("PATH").expect("PATH not available in env");
376        self.append_path(path.split(ENV_PATH_SEPARATOR_CHAR))
377    }
378
379    /// Inherit an environment variable from the running process, but only if it is set.
380    ///
381    /// This is useful for optional variables whose absence should not cause a panic.
382    pub fn inherit_env_if_set(self, key: impl AsRef<str>) -> Self {
383        let key = key.as_ref();
384        match env::var(key) {
385            Ok(val) => self.env(key, val),
386            Err(_) => self,
387        }
388    }
389
390    /// Inherit Rust toolchain related environment variables from the running process,
391    /// but only when they are set.
392    ///
393    /// This helps tests that spawn `cargo`, `rustc`, or `rustup` behave more like
394    /// the parent process, especially when the active toolchain or install location
395    /// is configured through environment variables.
396    ///
397    /// The following variables are inherited when present:
398    /// - `PATH`
399    /// - `CARGO_HOME`
400    /// - `RUSTUP_HOME`
401    /// - `RUSTUP_TOOLCHAIN`
402    /// - `RUSTUP_DIST_SERVER`
403    /// - `RUSTUP_UPDATE_ROOT`
404    ///
405    /// Proxy variables are also inherited when present since `rustup` may need them
406    /// to download or resolve toolchain metadata:
407    /// - `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`
408    /// - `http_proxy`, `https_proxy`, `no_proxy`
409    ///
410    /// This does not guarantee identical behavior to an interactive shell since the
411    /// current working directory can still affect rustup toolchain resolution.
412    pub fn inherit_rust_toolchain_env(self) -> Self {
413        self.inherit_path()
414            .inherit_env_if_set("PATH")
415            .inherit_env_if_set("CARGO_HOME")
416            .inherit_env_if_set("RUSTUP_HOME")
417            .inherit_env_if_set("RUSTUP_TOOLCHAIN")
418            .inherit_env_if_set("RUSTUP_DIST_SERVER")
419            .inherit_env_if_set("RUSTUP_UPDATE_ROOT")
420            .inherit_env_if_set("HTTP_PROXY")
421            .inherit_env_if_set("HTTPS_PROXY")
422            .inherit_env_if_set("NO_PROXY")
423            .inherit_env_if_set("http_proxy")
424            .inherit_env_if_set("https_proxy")
425            .inherit_env_if_set("no_proxy")
426    }
427
428    /// Adds the "nu" binary for testing to the path.
429    ///
430    /// Calling [`inherit_path`](Self::inherit_path) after this methods removes the path entry.
431    #[deprecated(note = "use `#[deps(NU)]` instead")]
432    pub fn add_nu_to_path(self) -> Self {
433        let nu_home = crate::fs::binaries();
434        let path = self.engine_state.get_env_var("PATH");
435        let path = match path {
436            None => nu_home.display().to_string(),
437            Some(path) => format!(
438                "{nu}{sep}{prev}",
439                nu = nu_home.display(),
440                sep = ENV_PATH_SEPARATOR_CHAR,
441                prev = path.as_str().expect("PATH should always be a string")
442            ),
443        };
444        self.env("PATH", path)
445    }
446
447    /// Add a custom environment variable to the engine state.
448    pub fn env(mut self, key: impl Into<String>, val: impl IntoValue) -> Self {
449        self.engine_state
450            .add_env_var(key.into(), val.into_value(Span::test_data()));
451        self
452    }
453
454    /// Run Nushell code and extract the value into `T`.
455    ///
456    /// Parsing, compilation, or evaluation failures are returned as [`TestError`].
457    #[track_caller]
458    pub fn run<T: FromValue>(&mut self, code: impl AsRef<str>) -> Result<T> {
459        Self::extract_value(self.run_raw(code)?)
460    }
461
462    /// Run Nushell code with input data and extract the value into `T`.
463    ///
464    /// The input value is converted into `PipelineData` using [`IntoValue`].
465    #[track_caller]
466    pub fn run_with_data<T: FromValue>(
467        &mut self,
468        code: impl AsRef<str>,
469        data: impl IntoValue,
470    ) -> Result<T> {
471        let input = PipelineData::value(data.into_value(Span::test_data()), None);
472        Self::extract_value(self.run_raw_with_data(code, input)?)
473    }
474
475    /// Run multiple Nushell command pipelines after each other and extract the value into `T`.
476    ///
477    /// This shortcircuits if any pipeline fails.
478    #[track_caller]
479    pub fn run_multiple<T: FromValue>(
480        &mut self,
481        pipelines: impl IntoIterator<Item = impl AsRef<str>>,
482    ) -> Result<T> {
483        let last = pipelines
484            .into_iter()
485            .map(|pipeline| self.run(pipeline))
486            .try_fold(Value::test_nothing(), |_, value| value)?;
487        Ok(T::from_value(last)?)
488    }
489
490    /// Run Nushell code after evaluating the REPL hook checkpoints for that source.
491    ///
492    /// This is for behavior that specifically depends on `pre_prompt`, `env_change`, or
493    /// `pre_execution` hooks.
494    /// For ordinary shared-state tests, prefer repeated [`run`](Self::run) calls or
495    /// [`run_multiple`](Self::run_multiple).
496    #[track_caller]
497    pub fn run_with_hooks<T: FromValue>(&mut self, code: impl AsRef<str>) -> Result<T> {
498        let location = TestLocation(Location::caller());
499        let code = code.as_ref();
500
501        eval_repl_hooks(&mut self.engine_state, &mut self.stack, code)
502            .map_err(|err| TestError {
503                location,
504                kind: TestErrorKind::Shell(err),
505            })
506            .and_then(|()| self.run(code))
507    }
508
509    /// Run Nushell code and return the raw [`PipelineExecutionData`].
510    #[track_caller]
511    pub fn run_raw(&mut self, code: impl AsRef<str>) -> Result<PipelineExecutionData> {
512        self.run_raw_with_data(code, PipelineData::empty())
513    }
514
515    /// Run Nushell code with input data and return the raw execution results.
516    ///
517    /// This parses, compiles, and evaluates the code against the current engine state.
518    #[track_caller]
519    pub fn run_raw_with_data(
520        &mut self,
521        code: impl AsRef<str>,
522        data: PipelineData,
523    ) -> Result<PipelineExecutionData> {
524        let location = TestLocation(Location::caller());
525        let (delta, block) = self.parse_and_compile(code)?;
526        self.engine_state.merge_delta(delta)?;
527        nu_engine::eval_block::<WithoutDebug>(&self.engine_state, &mut self.stack, &block, data)
528            .map_err(|err| TestError {
529                location,
530                kind: TestErrorKind::Shell(err),
531            })
532    }
533
534    #[track_caller]
535    pub fn parse_and_compile(&mut self, code: impl AsRef<str>) -> Result<(StateDelta, Arc<Block>)> {
536        let location = TestLocation(Location::caller());
537        let code = code.as_ref().as_bytes();
538
539        let mut working_set = StateWorkingSet::new(&self.engine_state);
540        let fname = format!("nu-tester-{}", self.fname_counter.get());
541        let block = nu_parser::parse(&mut working_set, Some(&fname), code, false);
542
543        if let Some(err) = working_set.parse_errors.into_iter().next() {
544            return Err(TestError {
545                location,
546                kind: TestErrorKind::Parse(err),
547            });
548        }
549
550        if let Some(err) = working_set.compile_errors.into_iter().next() {
551            return Err(TestError {
552                location,
553                kind: TestErrorKind::Compile(err),
554            });
555        }
556
557        Ok((working_set.delta, block))
558    }
559
560    #[track_caller]
561    fn extract_value<T: FromValue>(
562        pipeline_execution_data: PipelineExecutionData,
563    ) -> Result<T, TestError> {
564        let pipeline_data = pipeline_execution_data.body;
565        let value = pipeline_data.into_value(Span::test_data())?;
566        let value = T::from_value(value)?;
567        Ok(value)
568    }
569
570    /// Test examples of a command.
571    #[track_caller]
572    pub fn examples(&mut self, command: impl Command + 'static) -> Result {
573        let location = TestLocation(Location::caller());
574        for example in command.examples() {
575            match example.result {
576                None => self
577                    .parse_and_compile(example.example)
578                    .map(|_| ())
579                    .map_err(|err| TestError {
580                        location,
581                        kind: TestErrorKind::ExampleFailed {
582                            command: command.name().to_string(),
583                            description: example.description.to_string(),
584                            code: example.example.to_string(),
585                            err: Box::new(err.kind),
586                        },
587                    })?,
588                Some(expected) => {
589                    let got = self.clone().run(example.example)?;
590                    if got != expected {
591                        return Err(TestError {
592                            location,
593                            kind: TestErrorKind::ExampleFailed {
594                                command: command.name().to_string(),
595                                description: example.description.to_string(),
596                                code: example.example.to_string(),
597                                err: Box::new(TestErrorKind::UnexpectedValue { expected, got }),
598                            },
599                        });
600                    }
601                }
602            }
603        }
604
605        Ok(())
606    }
607}
608
609#[derive(Debug, Clone, PartialEq)]
610pub struct TestError {
611    location: TestLocation,
612    kind: TestErrorKind,
613}
614
615#[derive(Clone, Copy, PartialEq, derive_more::Debug)]
616#[debug("{_0}")]
617pub struct TestLocation(&'static Location<'static>);
618
619/// Errors emitted by `NuTester` when parsing, compiling, or evaluating code.
620///
621/// This enum is marked as non-exhaustive to allow adding new variants.
622#[non_exhaustive]
623#[derive(Debug, Clone, PartialEq)]
624pub enum TestErrorKind {
625    Parse(ParseError),
626    Compile(CompileError),
627    Shell(ShellError),
628    GotValue {
629        got: Value,
630    },
631    NoInner,
632    MultipleInner {
633        count: usize,
634    },
635    UnexpectedErrorKind {
636        expected: &'static str,
637        got: ShellError,
638    },
639    UnexpectedValue {
640        expected: Value,
641        got: Value,
642    },
643    NoCode {
644        expected: String,
645    },
646    UnexpectedCode {
647        expected: String,
648        got: String,
649    },
650    ExampleFailed {
651        command: String,
652        description: String,
653        code: String,
654        err: Box<TestErrorKind>,
655    },
656    Io {
657        message: String,
658        kind: io::ErrorKind,
659    },
660}
661
662impl Display for TestError {
663    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
664        write!(f, "{self:#?}")
665    }
666}
667
668impl Error for TestError {}
669
670impl From<ShellError> for TestError {
671    #[track_caller]
672    fn from(err: ShellError) -> Self {
673        Self {
674            location: TestLocation(Location::caller()),
675            kind: TestErrorKind::Shell(err),
676        }
677    }
678}
679
680impl From<ParseError> for TestError {
681    #[track_caller]
682    fn from(err: ParseError) -> Self {
683        Self {
684            location: TestLocation(Location::caller()),
685            kind: TestErrorKind::Parse(err),
686        }
687    }
688}
689
690impl From<io::Error> for TestError {
691    #[track_caller]
692    fn from(value: io::Error) -> Self {
693        Self {
694            location: TestLocation(Location::caller()),
695            kind: TestErrorKind::Io {
696                message: value.to_string(),
697                kind: value.kind(),
698            },
699        }
700    }
701}
702
703impl TestError {
704    /// Convert this error into a [`ParseError`], if it is one.
705    pub fn parse(self) -> Result<ParseError, TestError> {
706        match self.kind {
707            TestErrorKind::Parse(err) => Ok(err),
708            _ => Err(self),
709        }
710    }
711
712    /// Convert this error into a [`CompileError`], if it is one.
713    pub fn compile(self) -> Result<CompileError, TestError> {
714        match self.kind {
715            TestErrorKind::Compile(err) => Ok(err),
716            _ => Err(self),
717        }
718    }
719
720    /// Convert this error into a [`ShellError`], if it is one.
721    pub fn shell(self) -> Result<ShellError, TestError> {
722        match self.kind {
723            TestErrorKind::Shell(err) => Ok(err),
724            _ => Err(self),
725        }
726    }
727
728    /// Update it's inner location with the call site of this function.
729    #[track_caller]
730    pub fn update_location(self) -> Self {
731        Self {
732            location: TestLocation(Location::caller()),
733            ..self
734        }
735    }
736}
737
738/// Convenience result type for test helpers.
739pub type Result<T = (), E = TestError> = std::result::Result<T, E>;
740
741/// Extensions for asserting error kinds from test helpers.
742pub trait TestResultExt: Sized {
743    /// Expect the result to be a `Value` equal to the provided input.
744    fn expect_value_eq<T: IntoValue>(self, value: T) -> Result;
745
746    /// Expect the result to be an error with a specific [`code`](miette::Diagnostic::code).
747    fn expect_error_code_eq(self, code: impl AsRef<str>) -> Result;
748
749    /// Expect the result to be a [`ShellError`].
750    fn expect_shell_error(self) -> Result<ShellError>;
751    /// Expect the result to be a [`ParseError`].
752    fn expect_parse_error(self) -> Result<ParseError>;
753    /// Expect the result to be a [`CompileError`].
754    fn expect_compile_error(self) -> Result<CompileError>;
755
756    /// Expect the result to be a [`ShellError::Io`].
757    fn expect_io_error(self) -> Result<IoError>;
758    /// Expect the result to be a [`ShellError::Network`].
759    fn expect_network_error(self) -> Result<NetworkError>;
760    /// Expect the result to be a [`ShellError::LabeledError`].
761    fn expect_labeled_error(self) -> Result<LabeledError>;
762
763    /// Expect the result to be a [`ShellError`].
764    #[track_caller]
765    fn expect_error(self) -> Result<ShellError> {
766        self.expect_shell_error()
767    }
768}
769
770impl TestResultExt for Result<Value> {
771    #[track_caller]
772    fn expect_value_eq<T: IntoValue>(self, expected: T) -> Result {
773        let expected = expected.into_value(Span::test_data());
774        match self {
775            Err(err) => Err(err.update_location()),
776            Ok(actual) if actual == expected => Ok(()),
777            Ok(actual) => Err(TestError {
778                location: TestLocation(Location::caller()),
779                kind: TestErrorKind::UnexpectedValue {
780                    expected,
781                    got: actual,
782                },
783            }),
784        }
785    }
786
787    #[track_caller]
788    fn expect_error_code_eq(self, code: impl AsRef<str>) -> Result {
789        let expected = code.as_ref();
790        let got = match self {
791            Ok(got) => {
792                return Err(TestError {
793                    location: TestLocation(Location::caller()),
794                    kind: TestErrorKind::GotValue { got },
795                });
796            }
797            Err(TestError {
798                kind: TestErrorKind::Shell(ref err),
799                ..
800            }) => err.code(),
801            Err(TestError {
802                kind: TestErrorKind::Compile(ref err),
803                ..
804            }) => err.code(),
805            Err(TestError {
806                kind: TestErrorKind::Parse(ref err),
807                ..
808            }) => err.code(),
809            Err(err) => return Err(err.update_location()),
810        };
811
812        let Some(got) = got else {
813            return Err(TestError {
814                location: TestLocation(Location::caller()),
815                kind: TestErrorKind::NoCode {
816                    expected: expected.to_string(),
817                },
818            });
819        };
820
821        let got = got.to_string();
822        match got == expected {
823            true => Ok(()),
824            false => Err(TestError {
825                location: TestLocation(Location::caller()),
826                kind: TestErrorKind::UnexpectedCode {
827                    expected: expected.to_string(),
828                    got,
829                },
830            }),
831        }
832    }
833
834    #[track_caller]
835    fn expect_shell_error(self) -> Result<ShellError> {
836        match self {
837            Ok(got) => Err(TestError {
838                location: TestLocation(Location::caller()),
839                kind: TestErrorKind::GotValue { got },
840            }),
841            Err(TestError {
842                kind: TestErrorKind::Shell(err),
843                ..
844            }) => Ok(err),
845            Err(err) => Err(err.update_location()),
846        }
847    }
848
849    #[track_caller]
850    fn expect_parse_error(self) -> Result<ParseError> {
851        match self {
852            Ok(got) => Err(TestError {
853                location: TestLocation(Location::caller()),
854                kind: TestErrorKind::GotValue { got },
855            }),
856            Err(TestError {
857                kind: TestErrorKind::Parse(err),
858                ..
859            }) => Ok(err),
860            Err(err) => Err(err.update_location()),
861        }
862    }
863
864    #[track_caller]
865    fn expect_compile_error(self) -> Result<CompileError> {
866        match self {
867            Ok(got) => Err(TestError {
868                location: TestLocation(Location::caller()),
869                kind: TestErrorKind::GotValue { got },
870            }),
871            Err(TestError {
872                kind: TestErrorKind::Compile(err),
873                ..
874            }) => Ok(err),
875            Err(err) => Err(err.update_location()),
876        }
877    }
878
879    #[track_caller]
880    fn expect_io_error(self) -> Result<IoError> {
881        match self {
882            Ok(got) => Err(TestError {
883                location: TestLocation(Location::caller()),
884                kind: TestErrorKind::GotValue { got },
885            }),
886            Err(TestError {
887                kind: TestErrorKind::Shell(ShellError::Io(err)),
888                ..
889            }) => Ok(err),
890            Err(err) => Err(err.update_location()),
891        }
892    }
893
894    #[track_caller]
895    fn expect_network_error(self) -> Result<NetworkError> {
896        match self {
897            Ok(got) => Err(TestError {
898                location: TestLocation(Location::caller()),
899                kind: TestErrorKind::GotValue { got },
900            }),
901            Err(TestError {
902                kind: TestErrorKind::Shell(ShellError::Network(err)),
903                ..
904            }) => Ok(err),
905            Err(err) => Err(err.update_location()),
906        }
907    }
908
909    #[track_caller]
910    fn expect_labeled_error(self) -> Result<LabeledError> {
911        match self {
912            Ok(got) => Err(TestError {
913                location: TestLocation(Location::caller()),
914                kind: TestErrorKind::GotValue { got },
915            }),
916            Err(TestError {
917                kind: TestErrorKind::Shell(ShellError::LabeledError(err)),
918                ..
919            }) => Ok(*err),
920            Err(err) => Err(err.update_location()),
921        }
922    }
923}
924
925/// Extensions for interrogating [`ShellError`] values in tests.
926pub trait ShellErrorExt {
927    /// Tries to convert into an inner value from a [`ShellError`].
928    ///
929    /// Useful if the error is expected to be a generic error that contains an inner error or a
930    /// chained error that chained another error.
931    ///
932    /// However, this function returns [`TestErrorKind::NoInner`]
933    /// - if `inner` of [`ShellError::Generic`] is empty
934    /// - if `sources` of [`ShellError::ChainedError`] is empty
935    /// - if `sources` of [`ShellError::EvalBlockWithInput`] is empty
936    /// - the error is none of the above types
937    ///
938    /// Also if multiple inner values are found a [`TestErrorKind::MultipleInner`] is returned.
939    fn into_inner(self) -> Result<ShellError>;
940
941    /// Extract the [`LabeledError`] from [`ShellError::LabeledError`], if it is one.
942    fn into_labeled(self) -> Result<LabeledError>;
943
944    /// Extract the iterator on the sources of the [`ChainedError`] from
945    /// [`ShellError::ChainedError`], it it is one.
946    fn into_chained_iter(self) -> Result<impl Iterator<Item = ShellError>>;
947
948    /// Extract the error field from [`ShellError::Generic`], if it is one.
949    fn generic_error(self) -> Result<String>;
950
951    /// Extract the message field from [`ShellError::Generic`], if it is one.
952    fn generic_msg(self) -> Result<String>;
953}
954
955impl ShellErrorExt for ShellError {
956    #[track_caller]
957    fn into_inner(self) -> Result<ShellError> {
958        let no_inner = TestError {
959            location: TestLocation(Location::caller()),
960            kind: TestErrorKind::NoInner,
961        };
962
963        let iter: &mut dyn Iterator<Item = ShellError> = match self {
964            ShellError::Generic(err) => &mut err.inner.into_iter(),
965            ShellError::ChainedError(err) => &mut err.sources_iter(),
966            ShellError::EvalBlockWithInput { sources, .. } => &mut sources.into_iter(),
967            _ => return Err(no_inner),
968        };
969
970        let Some(inner) = iter.next() else {
971            return Err(no_inner);
972        };
973
974        let rest = iter.count();
975        if rest != 0 {
976            return Err(TestError {
977                location: TestLocation(Location::caller()),
978                kind: TestErrorKind::MultipleInner { count: rest + 1 },
979            });
980        }
981
982        Ok(inner)
983    }
984
985    #[track_caller]
986    fn into_labeled(self) -> Result<LabeledError> {
987        match self {
988            ShellError::LabeledError(err) => Ok(*err),
989            got => Err(TestError {
990                location: TestLocation(Location::caller()),
991                kind: TestErrorKind::UnexpectedErrorKind {
992                    expected: "Labeled",
993                    got,
994                },
995            }),
996        }
997    }
998
999    #[track_caller]
1000    fn into_chained_iter(self) -> Result<impl Iterator<Item = ShellError>> {
1001        match self {
1002            ShellError::ChainedError(err) => Ok(err.sources_iter()),
1003            got => Err(TestError {
1004                location: TestLocation(Location::caller()),
1005                kind: TestErrorKind::UnexpectedErrorKind {
1006                    expected: "Chained",
1007                    got,
1008                },
1009            }),
1010        }
1011    }
1012
1013    #[track_caller]
1014    fn generic_error(self) -> Result<String> {
1015        match self {
1016            ShellError::Generic(err) => Ok(err.error.into_owned()),
1017            got => Err(TestError {
1018                location: TestLocation(Location::caller()),
1019                kind: TestErrorKind::UnexpectedErrorKind {
1020                    expected: "Generic",
1021                    got,
1022                },
1023            }),
1024        }
1025    }
1026
1027    #[track_caller]
1028    fn generic_msg(self) -> Result<String> {
1029        match self {
1030            ShellError::Generic(err) => Ok(err.msg.into_owned()),
1031            got => Err(TestError {
1032                location: TestLocation(Location::caller()),
1033                kind: TestErrorKind::UnexpectedErrorKind {
1034                    expected: "Generic",
1035                    got,
1036                },
1037            }),
1038        }
1039    }
1040}