use std::{
env,
error::Error,
fmt::{Debug, Display},
io,
panic::Location,
path::{self, Path, PathBuf},
sync::{Arc, LazyLock},
};
use miette::Diagnostic;
use nu_cmd_base::hook::eval_repl_hooks;
use nu_protocol::{
CompileError, Config, FromValue, IntoValue, LabeledError, ParseError, PipelineData,
PipelineExecutionData, ShellError, Span, Value,
ast::Block,
debugger::WithoutDebug,
engine::{Command, EngineState, Stack, StateDelta, StateWorkingSet},
shell_error::{io::IoError, network::NetworkError},
};
use nu_utils::{consts::ENV_PATH_SEPARATOR_CHAR, sync::KeyedLazyLock};
use parking_lot::{RwLock, const_rwlock};
use crate::harness::group::GroupKey;
#[cfg(feature = "plugin")]
use nu_plugin_engine::{GetPlugin, PersistentPlugin, PluginDeclaration};
#[cfg(feature = "plugin")]
use nu_protocol::{PluginIdentity, PluginSignature, RegisteredPlugin};
pub static WORKSPACE_ROOT: LazyLock<PathBuf> = LazyLock::new(|| {
path::absolute(concat!(env!("CARGO_MANIFEST_DIR"), "/../.."))
.expect("could not absolutize root")
});
static INITIAL_ENGINE_STATES: KeyedLazyLock<GroupKey, EngineState> = KeyedLazyLock::new(|_| {
let engine_state = nu_cmd_lang::create_default_context();
#[cfg(feature = "plugin")]
let engine_state = nu_cmd_plugin::add_plugin_command_context(engine_state);
let engine_state = nu_command::add_shell_command_context(engine_state);
let engine_state = nu_cmd_extra::add_extra_command_context(engine_state);
#[cfg(feature = "os")]
let engine_state = nu_cli::add_cli_context(engine_state);
let mut engine_state = engine_state;
engine_state.generate_nu_constant();
[
("PWD", Value::test_string(WORKSPACE_ROOT.to_string_lossy())),
("config", Config::default().into_value(Span::unknown())),
("NO_COLOR", Value::test_bool(true)),
]
.into_iter()
.for_each(|(key, val)| engine_state.add_env_var(key.into(), val));
#[cfg(windows)]
if let Ok(path_ext) = env::var("PATHEXT") {
engine_state.add_env_var("PATHEXT".into(), Value::test_string(path_ext));
}
nu_std::load_standard_library(&mut engine_state).expect("could not load standard library");
engine_state
});
#[cfg(feature = "plugin")]
#[derive(Debug, Clone)]
pub struct PluginAutoLoader {
pub identity: Arc<PluginIdentity>,
pub plugin: Option<Arc<PersistentPlugin>>,
pub signatures: Option<Arc<[PluginSignature]>>,
}
pub static PATH_ENV_AUTO_LOAD: RwLock<Vec<PathBuf>> = const_rwlock(Vec::new());
#[cfg(feature = "plugin")]
pub static PLUGIN_AUTO_LOAD: RwLock<Vec<PluginAutoLoader>> = const_rwlock(Vec::new());
#[cfg_attr(feature = "plugin", doc = "[`PLUGIN_AUTO_LOAD`]")]
#[cfg_attr(not(feature = "plugin"), doc = "`PLUGIN_AUTO_LOAD`")]
pub fn test() -> NuTester {
let mut engine_state = INITIAL_ENGINE_STATES.get(&GroupKey::current()).clone();
engine_state.make_session_state_unique();
let tester = NuTester {
engine_state,
stack: Stack::new().collect_value(),
fname_counter: Counter::default(),
};
let tester = tester.append_path(&*PATH_ENV_AUTO_LOAD.read());
#[cfg(feature = "plugin")]
let tester = tester.auto_load_plugins();
tester
}
#[derive(Clone)]
#[non_exhaustive] pub struct NuTester {
pub engine_state: EngineState,
pub stack: Stack,
fname_counter: Counter,
}
#[derive(Default, Clone)]
struct Counter(u64);
impl Counter {
pub fn get(&mut self) -> u64 {
let value = self.0;
self.0 += 1;
value
}
}
impl Default for NuTester {
fn default() -> Self {
test()
}
}
#[cfg(feature = "plugin")]
impl NuTester {
fn auto_load_plugins(self) -> Self {
let mut tester = self;
let auto_loaders = PLUGIN_AUTO_LOAD.read();
if auto_loaders.is_empty() {
return tester;
}
let mut working_set = StateWorkingSet::new(&tester.engine_state);
for auto_loader in auto_loaders.iter() {
let plugin = working_set.find_or_create_plugin(&auto_loader.identity, || {
auto_loader
.plugin
.as_ref()
.map(|plugin| plugin.clone())
.unwrap_or_else(|| {
Arc::new(PersistentPlugin::new(
(*auto_loader.identity).clone(),
Default::default(),
))
})
});
let plugin: Arc<PersistentPlugin> = plugin
.as_any()
.downcast()
.expect("could not downcast to persistent plugin");
let mut interface = None;
if plugin.metadata().is_none() {
let interface = interface.get_or_insert_with(|| {
plugin
.clone()
.get_plugin(None)
.expect("could not get plugin")
});
plugin.set_metadata(Some(
interface
.get_metadata()
.expect("could not get plugin metadata"),
));
}
let signatures = auto_loader
.signatures
.as_deref()
.map(|signatures| signatures.to_owned())
.unwrap_or_else(|| {
let interface = interface.get_or_insert_with(|| {
plugin
.clone()
.get_plugin(None)
.expect("could not get plugin")
});
interface
.get_signature()
.expect("could not get plugin signatures")
});
for signature in signatures {
let decl = PluginDeclaration::new(plugin.clone(), signature);
working_set.add_decl(Box::new(decl));
}
}
tester
.engine_state
.merge_delta(working_set.render())
.expect("could not merge plugin working set");
tester
}
}
impl NuTester {
pub fn new() -> Self {
test()
}
pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
let cwd = cwd.into();
let cwd = match cwd.is_absolute() {
true => cwd,
false => WORKSPACE_ROOT
.join(cwd)
.canonicalize()
.expect("could not canonicalize path"),
};
self.engine_state
.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
self
}
pub fn locale(mut self, locale: impl Into<String>) -> Self {
self.engine_state.add_env_var(
"NU_TEST_LOCALE_OVERRIDE".into(),
Value::test_string(locale.into()),
);
self
}
pub fn locale_en(self) -> Self {
self.locale("en_US.utf8")
}
fn path(&self) -> Vec<Value> {
match self.engine_state.get_env_var("PATH") {
None => Vec::new(),
Some(Value::List { vals, .. }) => vals.to_vec(),
Some(Value::String { val, .. }) => val
.split(ENV_PATH_SEPARATOR_CHAR)
.map(Value::test_string)
.collect(),
Some(v) => panic!("PATH is neither a list nor a string, is {}", v.get_type()),
}
}
pub fn prepend_path(self, entries: impl IntoIterator<Item = impl AsRef<Path>>) -> Self {
let path = entries
.into_iter()
.map(|item| Value::test_string(item.as_ref().to_string_lossy()))
.chain(self.path())
.collect();
self.env("PATH", Value::test_list(path))
}
pub fn append_path(self, entries: impl IntoIterator<Item = impl AsRef<Path>>) -> Self {
let path = self
.path()
.into_iter()
.chain(
entries
.into_iter()
.map(|item| Value::test_string(item.as_ref().to_string_lossy())),
)
.collect();
self.env("PATH", Value::test_list(path))
}
pub fn inherit_path(self) -> Self {
let path = env::var("PATH").expect("PATH not available in env");
self.append_path(path.split(ENV_PATH_SEPARATOR_CHAR))
}
pub fn inherit_env_if_set(self, key: impl AsRef<str>) -> Self {
let key = key.as_ref();
match env::var(key) {
Ok(val) => self.env(key, val),
Err(_) => self,
}
}
pub fn inherit_rust_toolchain_env(self) -> Self {
self.inherit_path()
.inherit_env_if_set("PATH")
.inherit_env_if_set("CARGO_HOME")
.inherit_env_if_set("RUSTUP_HOME")
.inherit_env_if_set("RUSTUP_TOOLCHAIN")
.inherit_env_if_set("RUSTUP_DIST_SERVER")
.inherit_env_if_set("RUSTUP_UPDATE_ROOT")
.inherit_env_if_set("HTTP_PROXY")
.inherit_env_if_set("HTTPS_PROXY")
.inherit_env_if_set("NO_PROXY")
.inherit_env_if_set("http_proxy")
.inherit_env_if_set("https_proxy")
.inherit_env_if_set("no_proxy")
}
#[deprecated(note = "use `#[deps(NU)]` instead")]
pub fn add_nu_to_path(self) -> Self {
let nu_home = crate::fs::binaries();
let path = self.engine_state.get_env_var("PATH");
let path = match path {
None => nu_home.display().to_string(),
Some(path) => format!(
"{nu}{sep}{prev}",
nu = nu_home.display(),
sep = ENV_PATH_SEPARATOR_CHAR,
prev = path.as_str().expect("PATH should always be a string")
),
};
self.env("PATH", path)
}
pub fn env(mut self, key: impl Into<String>, val: impl IntoValue) -> Self {
self.engine_state
.add_env_var(key.into(), val.into_value(Span::test_data()));
self
}
#[track_caller]
pub fn run<T: FromValue>(&mut self, code: impl AsRef<str>) -> Result<T> {
Self::extract_value(self.run_raw(code)?)
}
#[track_caller]
pub fn run_with_data<T: FromValue>(
&mut self,
code: impl AsRef<str>,
data: impl IntoValue,
) -> Result<T> {
let input = PipelineData::value(data.into_value(Span::test_data()), None);
Self::extract_value(self.run_raw_with_data(code, input)?)
}
#[track_caller]
pub fn run_multiple<T: FromValue>(
&mut self,
pipelines: impl IntoIterator<Item = impl AsRef<str>>,
) -> Result<T> {
let last = pipelines
.into_iter()
.map(|pipeline| self.run(pipeline))
.try_fold(Value::test_nothing(), |_, value| value)?;
Ok(T::from_value(last)?)
}
#[track_caller]
pub fn run_with_hooks<T: FromValue>(&mut self, code: impl AsRef<str>) -> Result<T> {
let location = TestLocation(Location::caller());
let code = code.as_ref();
eval_repl_hooks(&mut self.engine_state, &mut self.stack, code)
.map_err(|err| TestError {
location,
kind: TestErrorKind::Shell(err),
})
.and_then(|()| self.run(code))
}
#[track_caller]
pub fn run_raw(&mut self, code: impl AsRef<str>) -> Result<PipelineExecutionData> {
self.run_raw_with_data(code, PipelineData::empty())
}
#[track_caller]
pub fn run_raw_with_data(
&mut self,
code: impl AsRef<str>,
data: PipelineData,
) -> Result<PipelineExecutionData> {
let location = TestLocation(Location::caller());
let (delta, block) = self.parse_and_compile(code)?;
self.engine_state.merge_delta(delta)?;
nu_engine::eval_block::<WithoutDebug>(&self.engine_state, &mut self.stack, &block, data)
.map_err(|err| TestError {
location,
kind: TestErrorKind::Shell(err),
})
}
#[track_caller]
pub fn parse_and_compile(&mut self, code: impl AsRef<str>) -> Result<(StateDelta, Arc<Block>)> {
let location = TestLocation(Location::caller());
let code = code.as_ref().as_bytes();
let mut working_set = StateWorkingSet::new(&self.engine_state);
let fname = format!("nu-tester-{}", self.fname_counter.get());
let block = nu_parser::parse(&mut working_set, Some(&fname), code, false);
if let Some(err) = working_set.parse_errors.into_iter().next() {
return Err(TestError {
location,
kind: TestErrorKind::Parse(err),
});
}
if let Some(err) = working_set.compile_errors.into_iter().next() {
return Err(TestError {
location,
kind: TestErrorKind::Compile(err),
});
}
Ok((working_set.delta, block))
}
#[track_caller]
fn extract_value<T: FromValue>(
pipeline_execution_data: PipelineExecutionData,
) -> Result<T, TestError> {
let pipeline_data = pipeline_execution_data.body;
let value = pipeline_data.into_value(Span::test_data())?;
let value = T::from_value(value)?;
Ok(value)
}
#[track_caller]
pub fn examples(&mut self, command: impl Command + 'static) -> Result {
let location = TestLocation(Location::caller());
for example in command.examples() {
match example.result {
None => self
.parse_and_compile(example.example)
.map(|_| ())
.map_err(|err| TestError {
location,
kind: TestErrorKind::ExampleFailed {
command: command.name().to_string(),
description: example.description.to_string(),
code: example.example.to_string(),
err: Box::new(err.kind),
},
})?,
Some(expected) => {
let got = self.clone().run(example.example)?;
if got != expected {
return Err(TestError {
location,
kind: TestErrorKind::ExampleFailed {
command: command.name().to_string(),
description: example.description.to_string(),
code: example.example.to_string(),
err: Box::new(TestErrorKind::UnexpectedValue { expected, got }),
},
});
}
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TestError {
location: TestLocation,
kind: TestErrorKind,
}
#[derive(Clone, Copy, PartialEq, derive_more::Debug)]
#[debug("{_0}")]
pub struct TestLocation(&'static Location<'static>);
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum TestErrorKind {
Parse(ParseError),
Compile(CompileError),
Shell(ShellError),
GotValue {
got: Value,
},
NoInner,
MultipleInner {
count: usize,
},
UnexpectedErrorKind {
expected: &'static str,
got: ShellError,
},
UnexpectedValue {
expected: Value,
got: Value,
},
NoCode {
expected: String,
},
UnexpectedCode {
expected: String,
got: String,
},
ExampleFailed {
command: String,
description: String,
code: String,
err: Box<TestErrorKind>,
},
Io {
message: String,
kind: io::ErrorKind,
},
}
impl Display for TestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:#?}")
}
}
impl Error for TestError {}
impl From<ShellError> for TestError {
#[track_caller]
fn from(err: ShellError) -> Self {
Self {
location: TestLocation(Location::caller()),
kind: TestErrorKind::Shell(err),
}
}
}
impl From<ParseError> for TestError {
#[track_caller]
fn from(err: ParseError) -> Self {
Self {
location: TestLocation(Location::caller()),
kind: TestErrorKind::Parse(err),
}
}
}
impl From<io::Error> for TestError {
#[track_caller]
fn from(value: io::Error) -> Self {
Self {
location: TestLocation(Location::caller()),
kind: TestErrorKind::Io {
message: value.to_string(),
kind: value.kind(),
},
}
}
}
impl TestError {
pub fn parse(self) -> Result<ParseError, TestError> {
match self.kind {
TestErrorKind::Parse(err) => Ok(err),
_ => Err(self),
}
}
pub fn compile(self) -> Result<CompileError, TestError> {
match self.kind {
TestErrorKind::Compile(err) => Ok(err),
_ => Err(self),
}
}
pub fn shell(self) -> Result<ShellError, TestError> {
match self.kind {
TestErrorKind::Shell(err) => Ok(err),
_ => Err(self),
}
}
#[track_caller]
pub fn update_location(self) -> Self {
Self {
location: TestLocation(Location::caller()),
..self
}
}
}
pub type Result<T = (), E = TestError> = std::result::Result<T, E>;
pub trait TestResultExt: Sized {
fn expect_value_eq<T: IntoValue>(self, value: T) -> Result;
fn expect_error_code_eq(self, code: impl AsRef<str>) -> Result;
fn expect_shell_error(self) -> Result<ShellError>;
fn expect_parse_error(self) -> Result<ParseError>;
fn expect_compile_error(self) -> Result<CompileError>;
fn expect_io_error(self) -> Result<IoError>;
fn expect_network_error(self) -> Result<NetworkError>;
fn expect_labeled_error(self) -> Result<LabeledError>;
#[track_caller]
fn expect_error(self) -> Result<ShellError> {
self.expect_shell_error()
}
}
impl TestResultExt for Result<Value> {
#[track_caller]
fn expect_value_eq<T: IntoValue>(self, expected: T) -> Result {
let expected = expected.into_value(Span::test_data());
match self {
Err(err) => Err(err.update_location()),
Ok(actual) if actual == expected => Ok(()),
Ok(actual) => Err(TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::UnexpectedValue {
expected,
got: actual,
},
}),
}
}
#[track_caller]
fn expect_error_code_eq(self, code: impl AsRef<str>) -> Result {
let expected = code.as_ref();
let got = match self {
Ok(got) => {
return Err(TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::GotValue { got },
});
}
Err(TestError {
kind: TestErrorKind::Shell(ref err),
..
}) => err.code(),
Err(TestError {
kind: TestErrorKind::Compile(ref err),
..
}) => err.code(),
Err(TestError {
kind: TestErrorKind::Parse(ref err),
..
}) => err.code(),
Err(err) => return Err(err.update_location()),
};
let Some(got) = got else {
return Err(TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::NoCode {
expected: expected.to_string(),
},
});
};
let got = got.to_string();
match got == expected {
true => Ok(()),
false => Err(TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::UnexpectedCode {
expected: expected.to_string(),
got,
},
}),
}
}
#[track_caller]
fn expect_shell_error(self) -> Result<ShellError> {
match self {
Ok(got) => Err(TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::GotValue { got },
}),
Err(TestError {
kind: TestErrorKind::Shell(err),
..
}) => Ok(err),
Err(err) => Err(err.update_location()),
}
}
#[track_caller]
fn expect_parse_error(self) -> Result<ParseError> {
match self {
Ok(got) => Err(TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::GotValue { got },
}),
Err(TestError {
kind: TestErrorKind::Parse(err),
..
}) => Ok(err),
Err(err) => Err(err.update_location()),
}
}
#[track_caller]
fn expect_compile_error(self) -> Result<CompileError> {
match self {
Ok(got) => Err(TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::GotValue { got },
}),
Err(TestError {
kind: TestErrorKind::Compile(err),
..
}) => Ok(err),
Err(err) => Err(err.update_location()),
}
}
#[track_caller]
fn expect_io_error(self) -> Result<IoError> {
match self {
Ok(got) => Err(TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::GotValue { got },
}),
Err(TestError {
kind: TestErrorKind::Shell(ShellError::Io(err)),
..
}) => Ok(err),
Err(err) => Err(err.update_location()),
}
}
#[track_caller]
fn expect_network_error(self) -> Result<NetworkError> {
match self {
Ok(got) => Err(TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::GotValue { got },
}),
Err(TestError {
kind: TestErrorKind::Shell(ShellError::Network(err)),
..
}) => Ok(err),
Err(err) => Err(err.update_location()),
}
}
#[track_caller]
fn expect_labeled_error(self) -> Result<LabeledError> {
match self {
Ok(got) => Err(TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::GotValue { got },
}),
Err(TestError {
kind: TestErrorKind::Shell(ShellError::LabeledError(err)),
..
}) => Ok(*err),
Err(err) => Err(err.update_location()),
}
}
}
pub trait ShellErrorExt {
fn into_inner(self) -> Result<ShellError>;
fn into_labeled(self) -> Result<LabeledError>;
fn into_chained_iter(self) -> Result<impl Iterator<Item = ShellError>>;
fn generic_error(self) -> Result<String>;
fn generic_msg(self) -> Result<String>;
}
impl ShellErrorExt for ShellError {
#[track_caller]
fn into_inner(self) -> Result<ShellError> {
let no_inner = TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::NoInner,
};
let iter: &mut dyn Iterator<Item = ShellError> = match self {
ShellError::Generic(err) => &mut err.inner.into_iter(),
ShellError::ChainedError(err) => &mut err.sources_iter(),
ShellError::EvalBlockWithInput { sources, .. } => &mut sources.into_iter(),
_ => return Err(no_inner),
};
let Some(inner) = iter.next() else {
return Err(no_inner);
};
let rest = iter.count();
if rest != 0 {
return Err(TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::MultipleInner { count: rest + 1 },
});
}
Ok(inner)
}
#[track_caller]
fn into_labeled(self) -> Result<LabeledError> {
match self {
ShellError::LabeledError(err) => Ok(*err),
got => Err(TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::UnexpectedErrorKind {
expected: "Labeled",
got,
},
}),
}
}
#[track_caller]
fn into_chained_iter(self) -> Result<impl Iterator<Item = ShellError>> {
match self {
ShellError::ChainedError(err) => Ok(err.sources_iter()),
got => Err(TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::UnexpectedErrorKind {
expected: "Chained",
got,
},
}),
}
}
#[track_caller]
fn generic_error(self) -> Result<String> {
match self {
ShellError::Generic(err) => Ok(err.error.into_owned()),
got => Err(TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::UnexpectedErrorKind {
expected: "Generic",
got,
},
}),
}
}
#[track_caller]
fn generic_msg(self) -> Result<String> {
match self {
ShellError::Generic(err) => Ok(err.msg.into_owned()),
got => Err(TestError {
location: TestLocation(Location::caller()),
kind: TestErrorKind::UnexpectedErrorKind {
expected: "Generic",
got,
},
}),
}
}
}