1use std::{
2 env,
3 error::Error,
4 fmt::{Debug, Display},
5 panic::Location,
6 path::PathBuf,
7 sync::{Arc, LazyLock},
8};
9
10use nu_protocol::{
11 CompileError, Config, FromValue, IntoValue, LabeledError, ParseError, PipelineData,
12 PipelineExecutionData, ShellError, Span, Value,
13 ast::Block,
14 debugger::WithoutDebug,
15 engine::{Command, EngineState, Stack, StateDelta, StateWorkingSet},
16 shell_error::{io::IoError, network::NetworkError},
17};
18use nu_utils::{consts::ENV_PATH_SEPARATOR_CHAR, sync::KeyedLazyLock};
19
20use crate::harness::group::GroupKey;
21
22static ROOT: LazyLock<PathBuf> = LazyLock::new(|| {
23 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
24 .join("../..")
25 .canonicalize()
26 .expect("could not canonicalize root")
27});
28
29static INITIAL_ENGINE_STATES: KeyedLazyLock<GroupKey, EngineState> = KeyedLazyLock::new(|_| {
32 let engine_state = nu_cmd_lang::create_default_context();
35 let engine_state = nu_command::add_shell_command_context(engine_state);
38 let engine_state = nu_cmd_extra::add_extra_command_context(engine_state);
39 #[cfg(feature = "os")]
40 let engine_state = nu_cli::add_cli_context(engine_state);
41 let mut engine_state = engine_state;
45
46 engine_state.generate_nu_constant();
47 [
48 ("PWD", Value::test_string(ROOT.to_string_lossy())),
49 ("config", Config::default().into_value(Span::unknown())),
50 ("NO_COLOR", Value::test_bool(true)),
51 ]
52 .into_iter()
53 .for_each(|(key, val)| engine_state.add_env_var(key.into(), val));
54
55 nu_std::load_standard_library(&mut engine_state).expect("could not load standard library");
56
57 engine_state
58});
59
60pub fn test() -> NuTester {
111 NuTester::default()
112}
113
114#[derive(Clone)]
119#[non_exhaustive] pub struct NuTester {
121 pub engine_state: EngineState,
122 pub stack: Stack,
123}
124
125impl Default for NuTester {
126 fn default() -> Self {
130 Self {
131 engine_state: INITIAL_ENGINE_STATES.get(&GroupKey::current()).clone(),
132 stack: Stack::new().collect_value(),
133 }
134 }
135}
136
137impl NuTester {
138 pub fn new() -> Self {
142 Self::default()
143 }
144
145 pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
149 let cwd = cwd.into();
150
151 let cwd = match cwd.is_absolute() {
152 true => cwd,
153 false => ROOT
154 .join(cwd)
155 .canonicalize()
156 .expect("could not canonicalize path"),
157 };
158
159 self.engine_state
160 .add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
161 self
162 }
163
164 pub fn locale(mut self, locale: impl Into<String>) -> Self {
166 self.engine_state.add_env_var(
167 "NU_TEST_LOCALE_OVERRIDE".into(),
168 Value::test_string(locale.into()),
169 );
170 self
171 }
172
173 pub fn locale_en(self) -> Self {
175 self.locale("en_US.utf8")
176 }
177
178 pub fn inherit_path(self) -> Self {
185 let path = env::var("PATH").expect("PATH not available in env");
186 self.env("PATH", path)
187 }
188
189 pub fn inherit_env_if_set(self, key: impl AsRef<str>) -> Self {
193 let key = key.as_ref();
194 match env::var(key) {
195 Ok(val) => self.env(key, val),
196 Err(_) => self,
197 }
198 }
199
200 pub fn inherit_rust_toolchain_env(self) -> Self {
223 self.inherit_env_if_set("PATH")
224 .inherit_env_if_set("CARGO_HOME")
225 .inherit_env_if_set("RUSTUP_HOME")
226 .inherit_env_if_set("RUSTUP_TOOLCHAIN")
227 .inherit_env_if_set("RUSTUP_DIST_SERVER")
228 .inherit_env_if_set("RUSTUP_UPDATE_ROOT")
229 .inherit_env_if_set("HTTP_PROXY")
230 .inherit_env_if_set("HTTPS_PROXY")
231 .inherit_env_if_set("NO_PROXY")
232 .inherit_env_if_set("http_proxy")
233 .inherit_env_if_set("https_proxy")
234 .inherit_env_if_set("no_proxy")
235 }
236
237 pub fn add_nu_to_path(self) -> Self {
241 let nu_home = crate::fs::binaries();
242 let path = self.engine_state.get_env_var("PATH");
243 let path = match path {
244 None => nu_home.display().to_string(),
245 Some(path) => format!(
246 "{nu}{sep}{prev}",
247 nu = nu_home.display(),
248 sep = ENV_PATH_SEPARATOR_CHAR,
249 prev = path.as_str().expect("PATH should always be a string")
250 ),
251 };
252 self.env("PATH", path)
253 }
254
255 pub fn env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
257 self.engine_state
258 .add_env_var(key.into(), Value::test_string(val.into()));
259 self
260 }
261
262 #[track_caller]
266 pub fn run<T: FromValue>(&mut self, code: impl AsRef<str>) -> Result<T> {
267 Self::extract_value(self.run_raw(code)?)
268 }
269
270 #[track_caller]
274 pub fn run_with_data<T: FromValue>(
275 &mut self,
276 code: impl AsRef<str>,
277 data: impl IntoValue,
278 ) -> Result<T> {
279 let input = PipelineData::value(data.into_value(Span::test_data()), None);
280 Self::extract_value(self.run_raw_with_data(code, input)?)
281 }
282
283 #[track_caller]
285 pub fn run_raw(&mut self, code: impl AsRef<str>) -> Result<PipelineExecutionData> {
286 self.run_raw_with_data(code, PipelineData::empty())
287 }
288
289 #[track_caller]
293 pub fn run_raw_with_data(
294 &mut self,
295 code: impl AsRef<str>,
296 data: PipelineData,
297 ) -> Result<PipelineExecutionData> {
298 let location = TestLocation(Location::caller());
299 let (delta, block) = self.parse_and_compile(code)?;
300 self.engine_state.merge_delta(delta)?;
301 nu_engine::eval_block::<WithoutDebug>(&self.engine_state, &mut self.stack, &block, data)
302 .map_err(|err| TestError {
303 location,
304 kind: TestErrorKind::Shell(err),
305 })
306 }
307
308 #[track_caller]
309 pub fn parse_and_compile(&self, code: impl AsRef<str>) -> Result<(StateDelta, Arc<Block>)> {
310 let location = TestLocation(Location::caller());
311 let code = code.as_ref().as_bytes();
312
313 let mut working_set = StateWorkingSet::new(&self.engine_state);
314 let block = nu_parser::parse(&mut working_set, None, code, false);
315
316 if let Some(err) = working_set.parse_errors.into_iter().next() {
317 return Err(TestError {
318 location,
319 kind: TestErrorKind::Parse(err),
320 });
321 }
322
323 if let Some(err) = working_set.compile_errors.into_iter().next() {
324 return Err(TestError {
325 location,
326 kind: TestErrorKind::Compile(err),
327 });
328 }
329
330 Ok((working_set.delta, block))
331 }
332
333 #[track_caller]
334 fn extract_value<T: FromValue>(
335 pipeline_execution_data: PipelineExecutionData,
336 ) -> Result<T, TestError> {
337 let pipeline_data = pipeline_execution_data.body;
338 let value = pipeline_data.into_value(Span::test_data())?;
339 let value = T::from_value(value)?;
340 Ok(value)
341 }
342
343 #[track_caller]
345 pub fn examples(&self, command: impl Command + 'static) -> Result {
346 let location = TestLocation(Location::caller());
347 for example in command.examples() {
348 match example.result {
349 None => self
350 .parse_and_compile(example.example)
351 .map(|_| ())
352 .map_err(|err| TestError {
353 location,
354 kind: TestErrorKind::ExampleFailed {
355 command: command.name().to_string(),
356 description: example.description.to_string(),
357 code: example.example.to_string(),
358 err: Box::new(err.kind),
359 },
360 })?,
361 Some(expected) => {
362 let got = self.clone().run(example.example)?;
363 if got != expected {
364 return Err(TestError {
365 location,
366 kind: TestErrorKind::ExampleFailed {
367 command: command.name().to_string(),
368 description: example.description.to_string(),
369 code: example.example.to_string(),
370 err: Box::new(TestErrorKind::UnexpectedValue { expected, got }),
371 },
372 });
373 }
374 }
375 }
376 }
377
378 Ok(())
379 }
380}
381
382#[derive(Debug, Clone, PartialEq)]
383pub struct TestError {
384 location: TestLocation,
385 kind: TestErrorKind,
386}
387
388#[derive(Clone, Copy, PartialEq, derive_more::Debug)]
389#[debug("{_0}")]
390pub struct TestLocation(&'static Location<'static>);
391
392#[non_exhaustive]
396#[derive(Debug, Clone, PartialEq)]
397pub enum TestErrorKind {
398 Parse(ParseError),
399 Compile(CompileError),
400 Shell(ShellError),
401 GotValue {
402 got: Value,
403 },
404 NoInner,
405 UnexpectedErrorKind {
406 expected: &'static str,
407 got: ShellError,
408 },
409 UnexpectedValue {
410 expected: Value,
411 got: Value,
412 },
413 ExampleFailed {
414 command: String,
415 description: String,
416 code: String,
417 err: Box<TestErrorKind>,
418 },
419}
420
421impl Display for TestError {
422 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
423 write!(f, "{self:#?}")
424 }
425}
426
427impl Error for TestError {}
428
429impl From<ShellError> for TestError {
430 #[track_caller]
431 fn from(err: ShellError) -> Self {
432 Self {
433 location: TestLocation(Location::caller()),
434 kind: TestErrorKind::Shell(err),
435 }
436 }
437}
438
439impl From<ParseError> for TestError {
440 #[track_caller]
441 fn from(err: ParseError) -> Self {
442 Self {
443 location: TestLocation(Location::caller()),
444 kind: TestErrorKind::Parse(err),
445 }
446 }
447}
448
449impl TestError {
450 pub fn parse(self) -> Result<ParseError, TestError> {
452 match self.kind {
453 TestErrorKind::Parse(err) => Ok(err),
454 _ => Err(self),
455 }
456 }
457
458 pub fn compile(self) -> Result<CompileError, TestError> {
460 match self.kind {
461 TestErrorKind::Compile(err) => Ok(err),
462 _ => Err(self),
463 }
464 }
465
466 pub fn shell(self) -> Result<ShellError, TestError> {
468 match self.kind {
469 TestErrorKind::Shell(err) => Ok(err),
470 _ => Err(self),
471 }
472 }
473
474 #[track_caller]
476 pub fn update_location(self) -> Self {
477 Self {
478 location: TestLocation(Location::caller()),
479 ..self
480 }
481 }
482}
483
484pub type Result<T = (), E = TestError> = std::result::Result<T, E>;
486
487pub trait TestResultExt: Sized {
489 fn expect_value_eq<T: IntoValue>(self, value: T) -> Result;
491
492 fn expect_shell_error(self) -> Result<ShellError>;
494 fn expect_parse_error(self) -> Result<ParseError>;
496 fn expect_compile_error(self) -> Result<CompileError>;
498
499 fn expect_io_error(self) -> Result<IoError>;
501 fn expect_network_error(self) -> Result<NetworkError>;
503 fn expect_labeled_error(self) -> Result<LabeledError>;
505
506 #[track_caller]
508 fn expect_error(self) -> Result<ShellError> {
509 self.expect_shell_error()
510 }
511}
512
513impl TestResultExt for Result<Value> {
514 #[track_caller]
515 fn expect_value_eq<T: IntoValue>(self, expected: T) -> Result {
516 let expected = expected.into_value(Span::test_data());
517 match self {
518 Err(err) => Err(err.update_location()),
519 Ok(actual) if actual == expected => Ok(()),
520 Ok(actual) => Err(TestError {
521 location: TestLocation(Location::caller()),
522 kind: TestErrorKind::UnexpectedValue {
523 expected,
524 got: actual,
525 },
526 }),
527 }
528 }
529
530 #[track_caller]
531 fn expect_shell_error(self) -> Result<ShellError> {
532 match self {
533 Ok(got) => Err(TestError {
534 location: TestLocation(Location::caller()),
535 kind: TestErrorKind::GotValue { got },
536 }),
537 Err(TestError {
538 kind: TestErrorKind::Shell(err),
539 ..
540 }) => Ok(err),
541 Err(err) => Err(err.update_location()),
542 }
543 }
544
545 #[track_caller]
546 fn expect_parse_error(self) -> Result<ParseError> {
547 match self {
548 Ok(got) => Err(TestError {
549 location: TestLocation(Location::caller()),
550 kind: TestErrorKind::GotValue { got },
551 }),
552 Err(TestError {
553 kind: TestErrorKind::Parse(err),
554 ..
555 }) => Ok(err),
556 Err(err) => Err(err.update_location()),
557 }
558 }
559
560 #[track_caller]
561 fn expect_compile_error(self) -> Result<CompileError> {
562 match self {
563 Ok(got) => Err(TestError {
564 location: TestLocation(Location::caller()),
565 kind: TestErrorKind::GotValue { got },
566 }),
567 Err(TestError {
568 kind: TestErrorKind::Compile(err),
569 ..
570 }) => Ok(err),
571 Err(err) => Err(err.update_location()),
572 }
573 }
574
575 #[track_caller]
576 fn expect_io_error(self) -> Result<IoError> {
577 match self {
578 Ok(got) => Err(TestError {
579 location: TestLocation(Location::caller()),
580 kind: TestErrorKind::GotValue { got },
581 }),
582 Err(TestError {
583 kind: TestErrorKind::Shell(ShellError::Io(err)),
584 ..
585 }) => Ok(err),
586 Err(err) => Err(err.update_location()),
587 }
588 }
589
590 #[track_caller]
591 fn expect_network_error(self) -> Result<NetworkError> {
592 match self {
593 Ok(got) => Err(TestError {
594 location: TestLocation(Location::caller()),
595 kind: TestErrorKind::GotValue { got },
596 }),
597 Err(TestError {
598 kind: TestErrorKind::Shell(ShellError::Network(err)),
599 ..
600 }) => Ok(err),
601 Err(err) => Err(err.update_location()),
602 }
603 }
604
605 #[track_caller]
606 fn expect_labeled_error(self) -> Result<LabeledError> {
607 match self {
608 Ok(got) => Err(TestError {
609 location: TestLocation(Location::caller()),
610 kind: TestErrorKind::GotValue { got },
611 }),
612 Err(TestError {
613 kind: TestErrorKind::Shell(ShellError::LabeledError(err)),
614 ..
615 }) => Ok(*err),
616 Err(err) => Err(err.update_location()),
617 }
618 }
619}
620
621pub trait ShellErrorExt {
623 fn into_inner(self) -> Result<ShellError>;
635
636 fn into_labeled(self) -> Result<LabeledError>;
638
639 fn generic_error(self) -> Result<String>;
641
642 fn generic_msg(self) -> Result<String>;
644}
645
646impl ShellErrorExt for ShellError {
647 #[track_caller]
648 fn into_inner(self) -> Result<ShellError> {
649 let no_inner = TestError {
650 location: TestLocation(Location::caller()),
651 kind: TestErrorKind::NoInner,
652 };
653 match self {
654 ShellError::Generic(err) => err.inner.into_iter().next().ok_or(no_inner),
655 ShellError::ChainedError(err) => err.sources_iter().next().ok_or(no_inner),
656 _ => Err(no_inner),
657 }
658 }
659
660 #[track_caller]
661 fn into_labeled(self) -> Result<LabeledError> {
662 match self {
663 ShellError::LabeledError(err) => Ok(*err),
664 got => Err(TestError {
665 location: TestLocation(Location::caller()),
666 kind: TestErrorKind::UnexpectedErrorKind {
667 expected: "Labeled",
668 got,
669 },
670 }),
671 }
672 }
673
674 #[track_caller]
675 fn generic_error(self) -> Result<String> {
676 match self {
677 ShellError::Generic(err) => Ok(err.error.into_owned()),
678 got => Err(TestError {
679 location: TestLocation(Location::caller()),
680 kind: TestErrorKind::UnexpectedErrorKind {
681 expected: "Generic",
682 got,
683 },
684 }),
685 }
686 }
687
688 #[track_caller]
689 fn generic_msg(self) -> Result<String> {
690 match self {
691 ShellError::Generic(err) => Ok(err.msg.into_owned()),
692 got => Err(TestError {
693 location: TestLocation(Location::caller()),
694 kind: TestErrorKind::UnexpectedErrorKind {
695 expected: "Generic",
696 got,
697 },
698 }),
699 }
700 }
701}