nu_test_support/lib.rs
1#![expect(clippy::test_attr_in_doctest)]
2
3//! Test support for the Nushell crates.
4//!
5//! This crate provides tools for testing Nushell crates, including support for both unit and
6//! integration testing.
7//! It offers a [custom test harness](#custom-test-harness) to control the environment tests run in, along with
8//! [filesystem sandboxing](playground), utilities for
9//! [executing and asserting nushell scripts](tester), and additional general helper functionality.
10//!
11//! # Custom Test Harness
12//!
13//! Running tests in specific environments is difficult with the default built-in test harness,
14//! especially when it comes to serial execution, setting environment variables, or configuring
15//! global state.
16//! This crate provides a [custom test harness](harness) based on [kitest] to address these issues.
17//! It works for both unit and integration tests, and most crates in nushell are already set up to
18//! use it.
19//! The harness behaves similarly to the regular test harness, so getting started does not require
20//! special knowledge.
21//!
22//! ## Setup for Unit Tests
23//!
24//! In Cargo.toml of the crate:
25//! ```custom,{class=language-toml}
26//! [lib]
27//! harness = false # important part
28//! ```
29//! This disables the built-in test harness in your library and requires a `main` function to
30//! execute tests.
31//! You can simply import the provided entry point:
32//! ```
33//! #[cfg(test)]
34//! use nu_test_support::harness::main;
35//! ```
36//!
37//! ## Setup for Integration Tests
38//!
39//! In Cargo.toml of the crate:
40//! ```custom,{class=language-toml}
41//! [package]
42//! autotests = false # disable automatically found tests
43//!
44//! [[test]]
45//! name = "tests" # whatever name fits here
46//! path = "tests/main.rs" # path to the main file
47//! harness = false # disable the test harness
48//! ```
49//! This disables autotests, so all integration tests must be defined manually.
50//! All tests should live in the defined test binary as modules, and since the default harness is
51//! disabled, the provided harness must be used.
52//!
53//! ## Using `#[test]` Macro
54//!
55//! To use the provided test harness and have it discover tests, a new test macro setup is required.
56//! In the `main.rs` of the test:
57//! ```
58//! #[cfg(test)] // for unit tests, not required for integration tests
59//! #[macro_use]
60//! extern crate nu_test_support;
61//! ```
62//! This overrides the prelude macros with those from this crate, in particular the
63//! [`test`](harness::macros::test) macro.
64//! This allows test writers to keep using `#[test]` on test functions as usual.
65//!
66//! ## Configuring Test Environment
67//!
68//! When using the test harness, additional attributes are available that can be used together with
69//! `#[test]` to control how tests are executed.
70//!
71//! - `#[serial]`
72//! Runs tests sequentially. This is useful when tests require significant
73//! resources or interfere with each other when executed in parallel.
74//!
75//! - `#[env(FOO = "bar")]`
76//! Sets environment variables for a specific test. The harness still
77//! inherits the existing environment, but this allows overriding or adding
78//! variables for individual tests.
79//!
80//! - `#[exp(nu_experimental::EXAMPLE)]`
81//! Enables a specific [experimental option](nu_experimental) for a test.
82//! It can also be explicitly disabled with
83//! `#[exp(nu_experimental::EXAMPLE = false)]`.
84//!
85//! Tests with matching environment configurations or experimental settings are grouped together,
86//! allowing them to run in parallel where possible.
87//!
88//! # Writing Integration Tests
89//!
90//! This crate provides the [`NuTester`](tester::NuTester) struct, which makes it easy to write
91//! integration tests that execute Nushell scripts.
92//! The main entry point is the [`test()`] function, which returns a `NuTester` instance
93//! preconfigured with all commands, relevant environment variables, and the standard library.
94//!
95//! Each execution group within the test harness receives its own freshly created tester instance.
96//! This ensures that environment variables and experimental options are properly isolated between
97//! tests.
98//!
99//! By running tests in-process instead of spawning a separate `nu` binary, tests can be
100//! significantly faster.
101//! This also improves iteration speed, since the binary does not need to be rebuilt before each
102//! run. Additionally, the initial `NuTester` setup is performed once and then cloned, reducing
103//! overhead across multiple tests.
104//!
105//! When writing integration tests, it is recommended to always import the [`prelude`] to avoid
106//! repeatedly importing common utilities.
107//! Input and output handling relies heavily on the [`IntoValue`](nu_protocol::IntoValue)
108//! and [`FromValue`](nu_protocol::FromValue) traits, making it easy to pass data into
109//! Nushell and extract values for assertions in a natural way.
110//!
111//! ## Simple Test Execution and Equality Assertion
112//!
113//! A basic pattern is to run a Nushell snippet and assert its output:
114//!
115//! ```
116//! # #[macro_use] extern crate nu_test_support;
117//! use nu_test_support::prelude::*;
118//!
119//! #[test]
120//! fn short_example() -> Result {
121//! # unimplemented!()
122//! # }
123//! #
124//! # fn main() -> Result {
125//! test()
126//! .run("version | get version")
127//! .expect_value_eq(env!("CARGO_PKG_VERSION"))
128//! }
129//! ```
130//!
131//! For improved readability, especially with longer pipelines, it can be
132//! helpful to store the script in a variable:
133//!
134//! ```
135//! # #[macro_use] extern crate nu_test_support;
136//! use nu_test_support::prelude::*;
137//!
138//! #[test]
139//! fn longer_example() -> Result {
140//! # unimplemented!()
141//! # }
142//! #
143//! # fn main() -> Result {
144//! let code = r#"
145//! [a [b c]]
146//! | flatten
147//! | str join " "
148//! "#;
149//!
150//! test().run(code).expect_value_eq("a b c")
151//! }
152//! ```
153//!
154//! ## Pulling Data out of Test Run
155//!
156//! The [`run`](tester::NuTester::run) method of [`NuTester`](tester::NuTester) is commonly used
157//! together with [`expect_value_eq`](tester::TestResultExt::expect_value_eq) to compare the
158//! result of a script with a value that implements [`IntoValue`](nu_protocol::IntoValue).
159//!
160//! In cases where direct comparison is not convenient, `run` can also return values by converting
161//! them into a type that implements [`FromValue`](nu_protocol::FromValue).
162//! This makes it easy to extract data from Nushell and work with it in Rust.
163//!
164//! ```
165//! # #[macro_use] extern crate nu_test_support;
166//! use nu_test_support::prelude::*;
167//!
168//! #[test]
169//! fn pull_value_out() -> Result {
170//! # unimplemented!()
171//! # }
172//! #
173//! # fn main() -> Result {
174//! let num: f64 = test().run("12.34 + 2")?;
175//! assert_eq!(num.floor(), 14.0);
176//! Ok(())
177//! }
178//! ```
179//!
180//! ## Running Multiple Snippets on a Single Tester
181//!
182//! Some tests require executing multiple snippets instead of a single pipeline.
183//! Running them sequentially can also improve readability, especially for commands that return
184//! [`Nothing`](nu_protocol::Value::Nothing).
185//!
186//! A single tester instance can be reused to execute multiple snippets in order, allowing state to
187//! be built up step by step:
188//!
189//! ```
190//! # #[macro_use] extern crate nu_test_support;
191//! use nu_test_support::prelude::*;
192//!
193//! #[test]
194//! fn multiple_statements() -> Result {
195//! # unimplemented!()
196//! # }
197//! #
198//! # fn main() -> Result {
199//! let mut tester = test();
200//! let () = tester.run("def parrot [] { '🦜' }")?;
201//! let () = tester.run("def duck [] { '🦆' }")?;
202//! tester
203//! .run("(parrot) + 🤝 + (duck)")
204//! .expect_value_eq("🦜🤝🦆")
205//! }
206//! ```
207//!
208//! ## Inserting Data
209//!
210//! In some cases, it is more convenient to pass data into a pipeline directly
211//! instead of constructing it in Nushell code. The
212//! [`run_with_data`](tester::NuTester::run_with_data) method supports this by
213//! accepting a value that implements [`IntoValue`](nu_protocol::IntoValue).
214//!
215//! This is also useful to avoid using [`format!`], which can make tests harder
216//! to read or reason about.
217//!
218//! ```
219//! # #[macro_use] extern crate nu_test_support;
220//! use bytes::Bytes;
221//! use nu_test_support::prelude::*;
222//!
223//! #[test]
224//! fn decode_bytes() -> Result {
225//! # unimplemented!()
226//! # }
227//! #
228//! # fn main() -> Result {
229//! test()
230//! .run_with_data("$in | decode", Bytes::from("hello world"))
231//! .expect_value_eq("hello world")
232//! }
233//! ```
234//!
235//! Since both [`IntoValue`](nu_protocol::IntoValue) and [`FromValue`](nu_protocol::FromValue) can
236//! be derived, custom Rust types can be passed into Nushell and asserted directly.
237//! This keeps tests type safe and expressive.
238//!
239//! ```
240//! # #[macro_use] extern crate nu_test_support;
241//! use nu_test_support::prelude::*;
242//!
243//! #[derive(Debug, PartialEq, Eq, Clone, IntoValue, FromValue)]
244//! struct Sample {
245//! a: String,
246//! b: u32,
247//! }
248//!
249//! #[test]
250//! fn in_and_out() -> Result {
251//! # unimplemented!()
252//! # }
253//! #
254//! # fn main() -> Result {
255//! let sample = Sample {
256//! a: "🐳".into(),
257//! b: 52,
258//! };
259//!
260//! test()
261//! .run_with_data("$in | to nuon | from nuon", sample.clone())
262//! .expect_value_eq(sample)
263//! }
264//! ```
265//!
266//! ## Working with Metadata or Streams
267//!
268//! Some tests need access to metadata or streaming data.
269//! In these cases, [`run`](tester::NuTester::run) is not sufficient, since it returns a
270//! [`Value`](nu_protocol::Value).
271//!
272//! To work with lower level details, the raw [`PipelineData`](nu_protocol::PipelineData)
273//! can be obtained using [`run_raw`](tester::NuTester::run_raw) or
274//! [`run_raw_with_data`](tester::NuTester::run_raw_with_data).
275//!
276//! ```
277//! # #[macro_use] extern crate nu_test_support;
278//! use nu_test_support::prelude::*;
279//!
280//! #[test]
281//! fn check_metadata() -> Result {
282//! # unimplemented!()
283//! # }
284//! #
285//! # fn main() -> Result {
286//! let mut pipeline_data = test().run_raw("version | to nuon")?.body;
287//! let metadata = pipeline_data.take_metadata().expect("should have metadata");
288//! let content_type = metadata.content_type.expect("should have a content type");
289//! assert_eq!(content_type, "application/x-nuon");
290//! Ok(())
291//! }
292//! ```
293//!
294//! ## Configuring the Tester
295//!
296//! By default, the tester only includes Nushell builtins, the standard library,
297//! the `$nu` constant, and a minimal set of environment variables.
298//! For example, `$env.PATH` is unset to keep tests deterministic.
299//! When needed, the tester can be configured through a set of convenience methods.
300//!
301//! ### Setting the Working Directory
302//!
303//! The [`cwd`](tester::NuTester::cwd) method sets the current working directory (`$env.PWD`).
304//! This is useful when tests rely on filesystem access relative to a specific location.
305//!
306//! ```
307//! # #[macro_use] extern crate nu_test_support;
308//! use nu_test_support::prelude::*;
309//!
310//! #[test]
311//! fn cwd() -> Result {
312//! # unimplemented!()
313//! # }
314//! #
315//! # fn main() -> Result {
316//! test()
317//! .cwd("./crates/nu-test-support")
318//! .run("open Cargo.toml | get package.name")
319//! .expect_value_eq("nu-test-support")
320//! }
321//! ```
322//!
323//! ### Configuring the Locale
324//!
325//! The [`locale`](tester::NuTester::locale) method overrides the locale, while
326//! [`locale_en`](tester::NuTester::locale_en) provides a convenient way to force English output.
327//! This is helpful when testing locale dependent behavior.
328//!
329//! ```
330//! # #[macro_use] extern crate nu_test_support;
331//! use nu_test_support::prelude::*;
332//!
333//! #[test]
334//! fn locale() -> Result {
335//! # unimplemented!()
336//! # }
337//! #
338//! # fn main() -> Result {
339//! let code = r#""2021-10-22 20:00:12 +01:00" | format date "%c""#;
340//! let en: String = test().locale_en().run(&code)?;
341//! let de: String = test().locale("de_DE").run(&code)?;
342//! assert_ne!(en, de);
343//! Ok(())
344//! }
345//! ```
346//!
347//! ### Inheriting the System PATH
348//!
349//! By default, external commands are not available since `$env.PATH` is unset.
350//! The [`inherit_path`](tester::NuTester::inherit_path) method restores access to the system PATH,
351//! allowing tests to call external binaries.
352//!
353//! ```
354//! # #[macro_use] extern crate nu_test_support;
355//! use nu_test_support::prelude::*;
356//!
357//! #[cfg(windows)]
358//! #[test]
359//! fn echo() -> Result {
360//! # unimplemented!()
361//! # }
362//! # #[cfg(windows)]
363//! # fn main() -> Result {
364//! test()
365//! .inherit_path()
366//! .run(r#"cmd.exe /c "echo abc""#)
367//! .expect_value_eq("abc")
368//! }
369//!
370//! #[cfg(unix)]
371//! #[test]
372//! fn echo() -> Result {
373//! # unimplemented!()
374//! # }
375//! # #[cfg(unix)]
376//! # fn main() -> Result {
377//! test()
378//! .inherit_path()
379//! .run(r#"sh -c "echo abc""#)
380//! .expect_value_eq("abc")
381//! }
382//! ```
383//!
384//! ### Using the Rust Toolchain
385//!
386//! The [`inherit_rust_toolchain_env`](tester::NuTester::inherit_rust_toolchain_env)
387//! method makes Rust tooling such as `cargo` or `rustc` available inside tests.
388//!
389//! ```
390//! # #[macro_use] extern crate nu_test_support;
391//! use nu_test_support::prelude::*;
392//!
393//! #[test]
394//! fn check_cargo_version() -> Result {
395//! # unimplemented!()
396//! # }
397//! #
398//! # fn main() -> Result {
399//! let code = r#"cargo --version | split row " " | get 0"#;
400//! test()
401//! .inherit_rust_toolchain_env()
402//! .run(code)
403//! .expect_value_eq("cargo")
404//! }
405//! ```
406//!
407//! ### Running the `nu` Binary
408//!
409//! The [`add_nu_to_path`](tester::NuTester::add_nu_to_path) method adds the compiled `nu` binary
410//! from the `target` directory to the PATH.
411//! This allows invoking `nu` itself from within tests.
412//! This approach requires rebuilding when behavior changes and should generally be avoided unless
413//! necessary.
414//!
415//! ```
416//! # #[macro_use] extern crate nu_test_support;
417//! use nu_test_support::prelude::*;
418//!
419//! #[test]
420//! fn cococo() -> Result {
421//! # unimplemented!()
422//! # }
423//! #
424//! # fn main() -> Result {
425//! test()
426//! .add_nu_to_path()
427//! .run("nu --testbin cococo")
428//! .expect_value_eq("cococo")
429//! }
430//! ```
431//!
432//! ### Setting Environment Variables
433//!
434//! The [`env`](tester::NuTester::env) method sets environment variables for the tester itself.
435//! Unlike the `#[env]` attribute, this configures the tester instance directly rather than the
436//! test harness.
437//!
438//! ```
439//! # #[macro_use] extern crate nu_test_support;
440//! use nu_test_support::prelude::*;
441//!
442//! #[test]
443//! fn hey() -> Result {
444//! # unimplemented!()
445//! # }
446//! #
447//! # fn main() -> Result {
448//! test()
449//! .env("HEY", "👋")
450//! .run("$env.HEY")
451//! .expect_value_eq("👋")
452//! }
453//! ```
454//!
455//! ## Using the Playground
456//!
457//! The [`Playground`](playground::Playground) provides a sandboxed filesystem
458//! environment for tests. This is especially useful when testing commands
459//! that modify the filesystem, such as creating or removing files.
460//!
461//! Tests typically combine the playground with [`cwd`](tester::NuTester::cwd)
462//! to point the tester to the sandboxed directory.
463//!
464//! ```
465//! # #[macro_use] extern crate nu_test_support;
466//! use nu_test_support::{fs::Stub::EmptyFile, prelude::*};
467//!
468//! #[test]
469//! fn rm_in_playground() -> Result {
470//! # unimplemented!()
471//! # }
472//! #
473//! # fn main() -> Result {
474//! Playground::setup("rm_in_doctest", |dirs, sandbox| {
475//! sandbox.with_files(&[EmptyFile("i_will_be_deleted.txt")]);
476//! test()
477//! .cwd(dirs.test())
478//! .run("rm i_will_be_deleted.txt")
479//! .expect_value_eq(())
480//! })
481//! }
482//! ```
483//!
484//! ## Configuring Experimental Options
485//!
486//! Experimental features can be enabled or disabled per test using the
487//! `#[exp]` attribute provided by the custom test harness.
488//!
489//! ```no_run
490//! # // this is a no_run as we cannot set experimental options safely during a doctest run
491//! # #[macro_use] extern crate nu_test_support;
492//! use nu_experimental::EXAMPLE;
493//! use nu_test_support::prelude::*;
494//!
495//! #[test]
496//! #[exp(EXAMPLE)]
497//! fn example_experimental_option() -> Result {
498//! # unimplemented!()
499//! # }
500//! #
501//! # fn main() -> Result {
502//! let code = "debug experimental-options | where identifier == example | get enabled.0";
503//! test().run(code).expect_value_eq(true)
504//! }
505//! ```
506//!
507//! ## Using `rstest`
508//!
509//! The `rstest` crate provides support for fixtures and parameterized test cases, which can
510//! significantly reduce boilerplate.
511//! It is especially useful when testing the same logic with multiple inputs.
512//!
513//! It works out of the box with the custom test harness, but requires careful ordering when
514//! combined with additional test attributes.
515//!
516//! ```
517//! # #[macro_use] extern crate nu_test_support;
518//! use nu_test_support::prelude::*;
519//! use rstest::rstest;
520//!
521//! #[rstest]
522//! #[case("a", "a🦜a")]
523//! #[case("🦜", "🦜🦜🦜")]
524//! fn simple_case(#[case] pre_and_suffix: &str, #[case] result: &str) -> Result {
525//! # unimplemented!()
526//! # }
527//! #
528//! # fn main() -> Result {
529//! # let pre_and_suffix = "a";
530//! # let result = "a🦜a";
531//! test()
532//! .run_with_data("$in + 🦜 + $in", pre_and_suffix)
533//! .expect_value_eq(result)
534//! }
535//! ```
536//!
537//! When combining `rstest` with the custom test harness attributes, the order of attributes
538//! becomes important.
539//! The harness attribute must be explicitly specified to ensure the test is picked up correctly.
540//!
541//! ```
542//! # #[macro_use] extern crate nu_test_support;
543//! use nu_test_support::prelude::*;
544//! use rstest::rstest;
545//!
546//! #[rstest]
547//! #[case(1)]
548//! #[case(-1)]
549//! #[nu_test_support::test]
550//! #[env(QUICK_MATHS = "true")]
551//! fn math_abs(#[case] input: i32) -> Result {
552//! # unimplemented!()
553//! # }
554//! #
555//! # fn main() -> Result {
556//! # let input: i32 = 1;
557//! test()
558//! .run_with_data("$in | math abs", input)
559//! .expect_value_eq(1)
560//! }
561//! ```
562
563pub mod assertions;
564pub mod fs;
565pub mod harness;
566pub mod net;
567pub mod playground;
568
569pub mod deprecated;
570#[doc(no_inline)]
571pub use deprecated::*;
572
573pub mod tester;
574pub use tester::{Result, ShellErrorExt, TestError as Error, TestResultExt, test};
575
576/// Prelude for writing tests.
577pub mod prelude {
578 #[doc(no_inline)]
579 pub use super::{
580 Outcome,
581 assertions::*,
582 nu,
583 playground::Playground,
584 tester::{Result, ShellErrorExt, TestError as Error, TestResultExt, test},
585 };
586
587 #[doc(no_inline)]
588 pub use nu_protocol::{CompileError, FromValue, IntoValue, ParseError, ShellError, Value};
589}
590
591// Expose macros to be used for the test harness.
592pub use harness::macros::*;
593
594// Needs to be reexported for `nu!` macro
595pub use nu_path;
596
597// Export json macro to allow writing json values easily.
598#[doc(no_inline)]
599pub use serde_json::json;
600
601/// Build a [`CellPath`](nu_protocol::ast::CellPath) in Rust using the familiar cell path syntax.
602///
603/// This macro lets you write cell paths the same way you do in Nushell.
604/// It also supports inline variables or expressions by wrapping them in a group (parentheses).
605///
606/// # Examples
607///
608/// ```rust
609/// use nu_test_support::test_cell_path;
610///
611/// let simple = test_cell_path!(foo.bar);
612/// assert_eq!(simple.to_string(), "$.foo.bar");
613///
614/// let with_modifiers = test_cell_path!(foo?.bar!);
615/// assert_eq!(with_modifiers.to_string(), "$.foo?.bar!");
616///
617/// let with_literal = test_cell_path!(foo."bar baz".3);
618/// assert_eq!(with_literal.to_string(), r#"$.foo."bar baz".3"#);
619///
620/// let column = "foo";
621/// let index = 3;
622/// let from_vars = test_cell_path!((column).(index));
623/// assert_eq!(from_vars.to_string(), "$.foo.3");
624/// ```
625#[doc(inline)]
626pub use nu_test_support_macros::test_cell_path;