mirror of
https://github.com/nushell/nushell.git
synced 2025-03-26 15:26:51 +01:00
Enable nushell error with backtrace (#14945)
# Description After this pr, nushell is able to raise errors with a backtrace, which should make users easier to debug. To enable the feature, users need to set env variable via `$env.NU_BACKTRACE = 1`. But yeah it might not work perfectly, there are some corner cases which might not be handled. I think it should close #13379 in another way. ### About the change The implementation mostly contained with 2 parts: 1. introduce a new `ChainedError` struct as well as a new `ShellError::ChainedError` variant. If `eval_instruction` returned an error, it converts the error to `ShellError::ChainedError`. `ChainedError` struct is responsable to display errors properly. It needs to handle the following 2 cases: - if we run a function which runs `error make` internally, it needs to display the error itself along with caller span. - if we run a `error make` directly, or some commands directly returns an error, we just want nushell raise an error about `error make`. 2. Attach caller spans to `ListStream` and `ByteStream`, because they are lazy streams, and *only* contains the span that runs it directly(like `^false`, for example), so nushell needs to add all caller spans to the stream. For example: in `def a [] { ^false }; def b [] { a; 33 }; b`, when we run `b`, which runs `a`, which runs `^false`, the `ByteStream` only contains the span of `^false`, we need to make it contains the span of `a`, so nushell is able to get all spans if something bad happened. This behavior is happened after running `Instruction::Call`, if it returns a `ByteStream` and `ListStream`, it will call `push_caller_span` method to attach call spans. # User-Facing Changes It's better to demostrate how it works by examples, given the following definition: ```nushell > $env.NU_BACKTRACE = 1 > def a [x] { if $x == 3 { error make {msg: 'a custom error'}}} > def a_2 [x] { if $x == 3 { ^false } else { $x } } > def a_3 [x] { if $x == 3 { [1 2 3] | each {error make {msg: 'a custom error inside list stream'} } } } > def b [--list-stream --external] { if $external == true { # error with non-zero exit code, which is generated from external command. a_2 1; a_2 3; a_2 2 } else if $list_stream == true { # error generated by list-stream a_3 1; a_3 3; a_3 2 } else { # error generated by command directly a 1; a 2; a 3 } } ``` Run `b` directly shows the following error: <details> ```nushell Error: chained_error × oops ╭─[entry #27:1:1] 1 │ b · ┬ · ╰── error happened when running this ╰──── Error: chained_error × oops ╭─[entry #26:10:19] 9 │ # error generated by command directly 10 │ a 1; a 2; a 3 · ┬ · ╰── error happened when running this 11 │ } ╰──── Error: × a custom error ╭─[entry #6:1:26] 1 │ def a [x] { if $x == 3 { error make {msg: 'a custom error'}}} · ─────┬──── · ╰── originates from here ╰──── ``` </details> Run `b --list-stream` shows the following error <details> ```nushell Error: chained_error × oops ╭─[entry #28:1:1] 1 │ b --list-stream · ┬ · ╰── error happened when running this ╰──── Error: nu:🐚:eval_block_with_input × Eval block failed with pipeline input ╭─[entry #26:7:16] 6 │ # error generated by list-stream 7 │ a_3 1; a_3 3; a_3 2 · ─┬─ · ╰── source value 8 │ } else { ╰──── Error: nu:🐚:eval_block_with_input × Eval block failed with pipeline input ╭─[entry #23:1:29] 1 │ def a_3 [x] { if $x == 3 { [1 2 3] | each {error make {msg: 'a custom error inside list stream'} } } } · ┬ · ╰── source value ╰──── Error: × a custom error inside list stream ╭─[entry #23:1:44] 1 │ def a_3 [x] { if $x == 3 { [1 2 3] | each {error make {msg: 'a custom error inside list stream'} } } } · ─────┬──── · ╰── originates from here ╰──── ``` </details> Run `b --external` shows the following error: <details> ```nushell Error: chained_error × oops ╭─[entry #29:1:1] 1 │ b --external · ┬ · ╰── error happened when running this ╰──── Error: nu:🐚:eval_block_with_input × Eval block failed with pipeline input ╭─[entry #26:4:16] 3 │ # error with non-zero exit code, which is generated from external command. 4 │ a_2 1; a_2 3; a_2 2 · ─┬─ · ╰── source value 5 │ } else if $list_stream == true { ╰──── Error: nu:🐚:non_zero_exit_code × External command had a non-zero exit code ╭─[entry #7:1:29] 1 │ def a_2 [x] { if $x == 3 { ^false } else { $x } } · ──┬── · ╰── exited with code 1 ╰──── ``` </details> It also added a message to guide the usage of NU_BACKTRACE, see the last line in the following example: ```shell ls asdfasd Error: nu:🐚:io::not_found × I/O error ╰─▶ × Entity not found ╭─[entry #17:1:4] 1 │ ls asdfasd · ───┬─── · ╰── Entity not found ╰──── help: The error occurred at '/home/windsoilder/projects/nushell/asdfasd' set the `NU_BACKTRACE=1` environment variable to display a backtrace. ``` # Tests + Formatting Added some tests for the behavior. # After Submitting
This commit is contained in:
parent
bdc767bf23
commit
2f18b9c856
@ -187,6 +187,7 @@ fn eval_ir_block_impl<D: DebugContext>(
|
||||
|
||||
// Program counter, starts at zero.
|
||||
let mut pc = 0;
|
||||
let need_backtrace = ctx.engine_state.get_env_var("NU_BACKTRACE").is_some();
|
||||
|
||||
while pc < ir_block.instructions.len() {
|
||||
let instruction = &ir_block.instructions[pc];
|
||||
@ -195,7 +196,7 @@ fn eval_ir_block_impl<D: DebugContext>(
|
||||
|
||||
D::enter_instruction(ctx.engine_state, ir_block, pc, ctx.registers);
|
||||
|
||||
let result = eval_instruction::<D>(ctx, instruction, span, ast);
|
||||
let result = eval_instruction::<D>(ctx, instruction, span, ast, need_backtrace);
|
||||
|
||||
D::leave_instruction(
|
||||
ctx.engine_state,
|
||||
@ -228,8 +229,10 @@ fn eval_ir_block_impl<D: DebugContext>(
|
||||
// If an error handler is set, branch there
|
||||
prepare_error_handler(ctx, error_handler, Some(err.into_spanned(*span)));
|
||||
pc = error_handler.handler_index;
|
||||
} else if need_backtrace {
|
||||
let err = ShellError::into_chainned(err, *span);
|
||||
return Err(err);
|
||||
} else {
|
||||
// If not, exit the block with the error
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
@ -285,6 +288,7 @@ fn eval_instruction<D: DebugContext>(
|
||||
instruction: &Instruction,
|
||||
span: &Span,
|
||||
ast: &Option<IrAstRef>,
|
||||
need_backtrace: bool,
|
||||
) -> Result<InstructionResult, ShellError> {
|
||||
use self::InstructionResult::*;
|
||||
|
||||
@ -548,7 +552,14 @@ fn eval_instruction<D: DebugContext>(
|
||||
}
|
||||
Instruction::Call { decl_id, src_dst } => {
|
||||
let input = ctx.take_reg(*src_dst);
|
||||
let result = eval_call::<D>(ctx, *decl_id, *span, input)?;
|
||||
let mut result = eval_call::<D>(ctx, *decl_id, *span, input)?;
|
||||
if need_backtrace {
|
||||
match &mut result {
|
||||
PipelineData::ByteStream(s, ..) => s.push_caller_span(*span),
|
||||
PipelineData::ListStream(s, ..) => s.push_caller_span(*span),
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
ctx.put_reg(*src_dst, result);
|
||||
Ok(Continue)
|
||||
}
|
||||
@ -1457,14 +1468,40 @@ fn drain(ctx: &mut EvalContext<'_>, data: PipelineData) -> Result<InstructionRes
|
||||
match data {
|
||||
PipelineData::ByteStream(stream, ..) => {
|
||||
let span = stream.span();
|
||||
if let Err(err) = stream.drain() {
|
||||
let callback_spans = stream.get_caller_spans().clone();
|
||||
if let Err(mut err) = stream.drain() {
|
||||
ctx.stack.set_last_error(&err);
|
||||
return Err(err);
|
||||
if callback_spans.is_empty() {
|
||||
return Err(err);
|
||||
} else {
|
||||
for s in callback_spans {
|
||||
err = ShellError::EvalBlockWithInput {
|
||||
span: s,
|
||||
sources: vec![err],
|
||||
}
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
} else {
|
||||
ctx.stack.set_last_exit_code(0, span);
|
||||
}
|
||||
}
|
||||
PipelineData::ListStream(stream, ..) => stream.drain()?,
|
||||
PipelineData::ListStream(stream, ..) => {
|
||||
let callback_spans = stream.get_caller_spans().clone();
|
||||
if let Err(mut err) = stream.drain() {
|
||||
if callback_spans.is_empty() {
|
||||
return Err(err);
|
||||
} else {
|
||||
for s in callback_spans {
|
||||
err = ShellError::EvalBlockWithInput {
|
||||
span: s,
|
||||
sources: vec![err],
|
||||
}
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
PipelineData::Value(..) | PipelineData::Empty => {}
|
||||
}
|
||||
Ok(Continue)
|
||||
|
120
crates/nu-protocol/src/errors/chained_error.rs
Normal file
120
crates/nu-protocol/src/errors/chained_error.rs
Normal file
@ -0,0 +1,120 @@
|
||||
use super::shell_error::ShellError;
|
||||
use crate::Span;
|
||||
use miette::{LabeledSpan, Severity, SourceCode};
|
||||
use thiserror::Error;
|
||||
|
||||
/// An error struct that contains source errors.
|
||||
///
|
||||
/// However, it's a bit special; if the error is constructed for the first time using
|
||||
/// [`ChainedError::new`], it will behave the same as the single source error.
|
||||
///
|
||||
/// If it's constructed nestedly using [`ChainedError::new_chained`], it will treat all underlying errors as related.
|
||||
///
|
||||
/// For a usage example, please check [`ShellError::into_chainned`].
|
||||
#[derive(Debug, Clone, PartialEq, Error)]
|
||||
pub struct ChainedError {
|
||||
first: bool,
|
||||
pub(crate) sources: Vec<ShellError>,
|
||||
span: Span,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ChainedError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.first {
|
||||
write!(f, "{}", self.sources[0])
|
||||
} else {
|
||||
write!(f, "oops")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainedError {
|
||||
pub fn new(source: ShellError, span: Span) -> Self {
|
||||
Self {
|
||||
first: true,
|
||||
sources: vec![source],
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_chained(sources: Self, span: Span) -> Self {
|
||||
Self {
|
||||
first: false,
|
||||
sources: vec![ShellError::ChainedError(sources)],
|
||||
span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl miette::Diagnostic for ChainedError {
|
||||
fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
|
||||
if self.first {
|
||||
self.sources[0].related()
|
||||
} else {
|
||||
Some(Box::new(self.sources.iter().map(|s| s as _)))
|
||||
}
|
||||
}
|
||||
|
||||
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
|
||||
if self.first {
|
||||
self.sources[0].code()
|
||||
} else {
|
||||
Some(Box::new("chained_error"))
|
||||
}
|
||||
}
|
||||
|
||||
fn severity(&self) -> Option<Severity> {
|
||||
if self.first {
|
||||
self.sources[0].severity()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
|
||||
if self.first {
|
||||
self.sources[0].help()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
|
||||
if self.first {
|
||||
self.sources[0].url()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn labels<'a>(&'a self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + 'a>> {
|
||||
if self.first {
|
||||
self.sources[0].labels()
|
||||
} else {
|
||||
Some(Box::new(
|
||||
vec![LabeledSpan::new_with_span(
|
||||
Some("error happened when running this".to_string()),
|
||||
self.span,
|
||||
)]
|
||||
.into_iter(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, we redirect the source_code method to our own source.
|
||||
fn source_code(&self) -> Option<&dyn SourceCode> {
|
||||
if self.first {
|
||||
self.sources[0].source_code()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn diagnostic_source(&self) -> Option<&dyn miette::Diagnostic> {
|
||||
if self.first {
|
||||
self.sources[0].diagnostic_source()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
@ -50,6 +50,10 @@ pub fn report_compile_error(working_set: &StateWorkingSet, error: &CompileError)
|
||||
|
||||
fn report_error(working_set: &StateWorkingSet, error: &dyn miette::Diagnostic) {
|
||||
eprintln!("Error: {:?}", CliError(error, working_set));
|
||||
let have_no_backtrace = working_set.get_env_var("NU_BACKTRACE").is_none();
|
||||
if have_no_backtrace {
|
||||
eprintln!("set the `NU_BACKTRACE=1` environment variable to display a backtrace.")
|
||||
}
|
||||
// reset vt processing, aka ansi because illbehaved externals can break it
|
||||
#[cfg(windows)]
|
||||
{
|
||||
|
@ -1,3 +1,4 @@
|
||||
mod chained_error;
|
||||
pub mod cli_error;
|
||||
mod compile_error;
|
||||
mod config_error;
|
||||
|
@ -1,3 +1,4 @@
|
||||
use super::chained_error::ChainedError;
|
||||
use crate::{
|
||||
ast::Operator, engine::StateWorkingSet, format_shell_error, record, ConfigError, LabeledError,
|
||||
ParseError, Span, Spanned, Type, Value,
|
||||
@ -1325,6 +1326,10 @@ On Windows, this would be %USERPROFILE%\AppData\Roaming"#
|
||||
#[label = "while running this code"]
|
||||
span: Option<Span>,
|
||||
},
|
||||
|
||||
#[error(transparent)]
|
||||
#[diagnostic(transparent)]
|
||||
ChainedError(ChainedError),
|
||||
}
|
||||
|
||||
impl ShellError {
|
||||
@ -1373,6 +1378,16 @@ impl ShellError {
|
||||
span,
|
||||
)
|
||||
}
|
||||
|
||||
/// Convert self error to a [`ShellError::ChainedError`] variant.
|
||||
pub fn into_chainned(self, span: Span) -> Self {
|
||||
match self {
|
||||
ShellError::ChainedError(inner) => {
|
||||
ShellError::ChainedError(ChainedError::new_chained(inner, span))
|
||||
}
|
||||
other => ShellError::ChainedError(ChainedError::new(other, span)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<dyn std::error::Error>> for ShellError {
|
||||
|
@ -193,6 +193,7 @@ pub struct ByteStream {
|
||||
signals: Signals,
|
||||
type_: ByteStreamType,
|
||||
known_size: Option<u64>,
|
||||
caller_spans: Vec<Span>,
|
||||
}
|
||||
|
||||
impl ByteStream {
|
||||
@ -209,9 +210,22 @@ impl ByteStream {
|
||||
signals,
|
||||
type_,
|
||||
known_size: None,
|
||||
caller_spans: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a caller [`Span`] to the bytestream, it's useful to construct a backtrace.
|
||||
pub fn push_caller_span(&mut self, span: Span) {
|
||||
if span != self.span {
|
||||
self.caller_spans.push(span)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all caller [`Span`], it's useful to construct a backtrace.
|
||||
pub fn get_caller_spans(&self) -> &Vec<Span> {
|
||||
&self.caller_spans
|
||||
}
|
||||
|
||||
/// Create a [`ByteStream`] from an arbitrary reader. The type must be provided.
|
||||
pub fn read(
|
||||
reader: impl Read + Send + 'static,
|
||||
|
@ -15,6 +15,7 @@ pub type ValueIterator = Box<dyn Iterator<Item = Value> + Send + 'static>;
|
||||
pub struct ListStream {
|
||||
stream: ValueIterator,
|
||||
span: Span,
|
||||
caller_spans: Vec<Span>,
|
||||
}
|
||||
|
||||
impl ListStream {
|
||||
@ -27,6 +28,7 @@ impl ListStream {
|
||||
Self {
|
||||
stream: Box::new(InterruptIter::new(iter, signals)),
|
||||
span,
|
||||
caller_spans: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,6 +37,18 @@ impl ListStream {
|
||||
self.span
|
||||
}
|
||||
|
||||
/// Push a caller [`Span`] to the bytestream, it's useful to construct a backtrace.
|
||||
pub fn push_caller_span(&mut self, span: Span) {
|
||||
if span != self.span {
|
||||
self.caller_spans.push(span)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all caller [`Span`], it's useful to construct a backtrace.
|
||||
pub fn get_caller_spans(&self) -> &Vec<Span> {
|
||||
&self.caller_spans
|
||||
}
|
||||
|
||||
/// Changes the [`Span`] associated with this [`ListStream`].
|
||||
pub fn with_span(mut self, span: Span) -> Self {
|
||||
self.span = span;
|
||||
@ -94,6 +108,7 @@ impl ListStream {
|
||||
Self {
|
||||
stream: Box::new(f(self.stream)),
|
||||
span: self.span,
|
||||
caller_spans: self.caller_spans,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ fn fancy_default_errors() {
|
||||
|
||||
assert_eq!(
|
||||
actual.err,
|
||||
"Error: \u{1b}[31m×\u{1b}[0m oh no!\n ╭─[\u{1b}[36;1;4mline2:1:13\u{1b}[0m]\n \u{1b}[2m1\u{1b}[0m │ force_error \"My error\"\n · \u{1b}[35;1m ─────┬────\u{1b}[0m\n · \u{1b}[35;1m╰── \u{1b}[35;1mhere's the error\u{1b}[0m\u{1b}[0m\n ╰────\n\n"
|
||||
"Error: \u{1b}[31m×\u{1b}[0m oh no!\n ╭─[\u{1b}[36;1;4mline2:1:13\u{1b}[0m]\n \u{1b}[2m1\u{1b}[0m │ force_error \"My error\"\n · \u{1b}[35;1m ─────┬────\u{1b}[0m\n · \u{1b}[35;1m╰── \u{1b}[35;1mhere's the error\u{1b}[0m\u{1b}[0m\n ╰────\n\nset the `NU_BACKTRACE=1` environment variable to display a backtrace.\n"
|
||||
);
|
||||
}
|
||||
|
||||
@ -92,6 +92,7 @@ snippet line 1: force_error "my error"
|
||||
label at line 1, columns 13 to 22: here's the error
|
||||
|
||||
|
||||
set the `NU_BACKTRACE=1` environment variable to display a backtrace.
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
@ -686,3 +686,34 @@ fn arg_dont_run_subcommand_if_surrounded_with_quote() {
|
||||
let actual = nu!("nu --testbin cococo '(echo aa)'");
|
||||
assert_eq!(actual.out, "(echo aa)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_error_with_backtrace() {
|
||||
Playground::setup("external error with backtrace", |dirs, sandbox| {
|
||||
sandbox.with_files(&[FileWithContent("tmp_env.nu", "$env.NU_BACKTRACE = 1")]);
|
||||
|
||||
let actual = nu!(
|
||||
env_config: "tmp_env.nu",
|
||||
cwd: dirs.test(),
|
||||
r#"def a [x] { if $x == 3 { nu --testbin --fail }};def b [] {a 1; a 3; a 2}; b"#);
|
||||
let chained_error_cnt: Vec<&str> = actual
|
||||
.err
|
||||
.matches("diagnostic code: chained_error")
|
||||
.collect();
|
||||
assert_eq!(chained_error_cnt.len(), 1);
|
||||
assert!(actual.err.contains("non_zero_exit_code"));
|
||||
let eval_with_input_cnt: Vec<&str> = actual.err.matches("eval_block_with_input").collect();
|
||||
assert_eq!(eval_with_input_cnt.len(), 1);
|
||||
|
||||
let actual = nu!(
|
||||
env_config: "tmp_env.nu",
|
||||
cwd: dirs.test(),
|
||||
r#"nu --testbin --fail"#);
|
||||
let chained_error_cnt: Vec<&str> = actual
|
||||
.err
|
||||
.matches("diagnostic code: chained_error")
|
||||
.collect();
|
||||
// run error make directly, show no backtrace is available
|
||||
assert_eq!(chained_error_cnt.len(), 0);
|
||||
});
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use nu_test_support::fs::Stub::FileWithContentToBeTrimmed;
|
||||
use nu_test_support::fs::Stub::{FileWithContent, FileWithContentToBeTrimmed};
|
||||
use nu_test_support::playground::Playground;
|
||||
use nu_test_support::{nu, pipeline};
|
||||
use pretty_assertions::assert_eq;
|
||||
@ -1135,3 +1135,83 @@ fn error_on_out_greater_pipe() {
|
||||
.err
|
||||
.contains("Redirecting stdout to a pipe is the same as normal piping"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_backtrace() {
|
||||
Playground::setup("error with backtrace", |dirs, sandbox| {
|
||||
sandbox.with_files(&[FileWithContent("tmp_env.nu", "$env.NU_BACKTRACE = 1")]);
|
||||
|
||||
let actual = nu!(
|
||||
env_config: "tmp_env.nu",
|
||||
cwd: dirs.test(),
|
||||
r#"def a [x] { if $x == 3 { error make {msg: 'a custom error'}}};a 3"#);
|
||||
let chained_error_cnt: Vec<&str> = actual
|
||||
.err
|
||||
.matches("diagnostic code: chained_error")
|
||||
.collect();
|
||||
// run `a 3`, and it raises error, so there should be 1.
|
||||
assert_eq!(chained_error_cnt.len(), 1);
|
||||
assert!(actual.err.contains("a custom error"));
|
||||
|
||||
let actual = nu!(
|
||||
env_config: "tmp_env.nu",
|
||||
cwd: dirs.test(),
|
||||
r#"def a [x] { if $x == 3 { error make {msg: 'a custom error'}}};def b [] { a 1; a 3; a 2 };b"#);
|
||||
|
||||
let chained_error_cnt: Vec<&str> = actual
|
||||
.err
|
||||
.matches("diagnostic code: chained_error")
|
||||
.collect();
|
||||
// run `b`, it runs `a 3`, and it raises error, so there should be 2.
|
||||
assert_eq!(chained_error_cnt.len(), 2);
|
||||
assert!(actual.err.contains("a custom error"));
|
||||
|
||||
let actual = nu!(
|
||||
env_config: "tmp_env.nu",
|
||||
cwd: dirs.test(),
|
||||
r#"error make {msg: 'a custom err'}"#);
|
||||
let chained_error_cnt: Vec<&str> = actual
|
||||
.err
|
||||
.matches("diagnostic code: chained_error")
|
||||
.collect();
|
||||
// run error make directly, show no backtrace is available
|
||||
assert_eq!(chained_error_cnt.len(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn liststream_error_with_backtrace() {
|
||||
Playground::setup("liststream error with backtrace", |dirs, sandbox| {
|
||||
sandbox.with_files(&[FileWithContent("tmp_env.nu", "$env.NU_BACKTRACE = 1")]);
|
||||
|
||||
let actual = nu!(
|
||||
env_config: "tmp_env.nu",
|
||||
cwd: dirs.test(),
|
||||
r#"def a [x] { if $x == 3 { [1] | each {error make {'msg': 'a custom error'}}}};a 3"#);
|
||||
assert!(actual.err.contains("a custom error"));
|
||||
|
||||
let actual = nu!(
|
||||
env_config: "tmp_env.nu",
|
||||
cwd: dirs.test(),
|
||||
r#"def a [x] { if $x == 3 { [1] | each {error make {'msg': 'a custom error'}}}};def b [] { a 1; a 3; a 2 };b"#);
|
||||
let chained_error_cnt: Vec<&str> = actual
|
||||
.err
|
||||
.matches("diagnostic code: chained_error")
|
||||
.collect();
|
||||
assert_eq!(chained_error_cnt.len(), 1);
|
||||
assert!(actual.err.contains("a custom error"));
|
||||
let eval_with_input_cnt: Vec<&str> = actual.err.matches("eval_block_with_input").collect();
|
||||
assert_eq!(eval_with_input_cnt.len(), 2);
|
||||
|
||||
let actual = nu!(
|
||||
env_config: "tmp_env.nu",
|
||||
cwd: dirs.test(),
|
||||
r#"[1] | each { error make {msg: 'a custom err'} }"#);
|
||||
let chained_error_cnt: Vec<&str> = actual
|
||||
.err
|
||||
.matches("diagnostic code: chained_error")
|
||||
.collect();
|
||||
// run error make directly, show no backtrace is available
|
||||
assert_eq!(chained_error_cnt.len(), 0);
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user