mz_testdrive/
error.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Error handling.
11//!
12//! Errors inside of the testdrive library are represented as an
13//! `anyhow::Error`. As the error bubbles up the stack, it may be upgraded to a
14//! `PosError`, which attaches source code position information. The position
15//! information tracked by a `PosError` uses a parser-specific representation
16//! that is not human-readable, so `PosError`s are upgraded to `Error`s before
17//! they are returned externally.
18
19use std::fmt::{self, Write as _};
20use std::io::{self, IsTerminal, Write};
21use std::path::{Path, PathBuf};
22
23use mz_ore::error::ErrorExt;
24use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
25
26/// An error produced when parsing or executing a testdrive script.
27///
28/// Errors are optionally associated with a location in a testdrive script. When
29/// printed with the [`Error::print_error`] method, the location of the error
30/// along with a snippet of the source code at that location will be printed
31/// alongside the error message.
32pub struct Error {
33    source: anyhow::Error,
34    location: Option<ErrorLocation>,
35}
36
37impl Error {
38    pub(crate) fn new(source: anyhow::Error, location: Option<ErrorLocation>) -> Self {
39        Error { source, location }
40    }
41
42    /// Prints the error to `stdout`, with coloring if the terminal supports it.
43    pub fn print_error(&self) -> io::Result<()> {
44        let color_choice = if std::io::stdout().is_terminal() {
45            ColorChoice::Auto
46        } else {
47            ColorChoice::Never
48        };
49        let mut stdout = StandardStream::stdout(color_choice);
50        eprintln!("^^^ +++");
51        match &self.location {
52            Some(location) => {
53                let mut color_spec = ColorSpec::new();
54                color_spec.set_bold(true);
55                stdout.set_color(&color_spec)?;
56                if let Some(filename) = &location.filename {
57                    write!(
58                        &mut stdout,
59                        "{}:{}:{}: ",
60                        filename.display(),
61                        location.line,
62                        location.col
63                    )?;
64                } else {
65                    write!(&mut stdout, "{}:{}: ", location.line, location.col)?;
66                }
67                write_error_heading(&mut stdout, &color_spec)?;
68                writeln!(&mut stdout, "{}", self.source.display_with_causes())?;
69                color_spec.set_bold(false);
70                stdout.set_color(&color_spec)?;
71                write!(&mut stdout, "{}", location.snippet)?;
72                writeln!(&mut stdout, "{}^", " ".repeat(location.col - 1))?;
73            }
74            None => {
75                let color_spec = ColorSpec::new();
76                write_error_heading(&mut stdout, &color_spec)?;
77                writeln!(&mut stdout, "{}", self.source.display_with_causes())?;
78            }
79        }
80        std::io::stdout().flush()
81    }
82}
83
84impl fmt::Display for Error {
85    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
86        match &self.location {
87            Some(location) => {
88                if let Some(filename) = &location.filename {
89                    write!(
90                        f,
91                        "{}:{}:{}: ",
92                        filename.display(),
93                        location.line,
94                        location.col
95                    )?;
96                } else {
97                    write!(f, "{}:{}: ", location.line, location.col)?;
98                }
99                writeln!(f, "{}", self.source.display_with_causes())?;
100                write!(f, "{}", location.snippet)?;
101                writeln!(f, "{}^", " ".repeat(location.col - 1))
102            }
103            None => {
104                write!(f, "{}", self.source.display_with_causes())
105            }
106        }
107    }
108}
109
110fn write_error_heading(stream: &mut StandardStream, color_spec: &ColorSpec) -> io::Result<()> {
111    stream.set_color(color_spec.clone().set_fg(Some(Color::Red)))?;
112    write!(stream, "error: ")?;
113    stream.set_color(color_spec)
114}
115
116impl From<anyhow::Error> for Error {
117    fn from(source: anyhow::Error) -> Error {
118        Error {
119            source,
120            location: None,
121        }
122    }
123}
124
125pub(crate) struct ErrorLocation {
126    filename: Option<PathBuf>,
127    snippet: String,
128    line: usize,
129    col: usize,
130}
131
132impl ErrorLocation {
133    pub(crate) fn new(
134        filename: Option<&Path>,
135        contents: &str,
136        line: usize,
137        col: usize,
138    ) -> ErrorLocation {
139        let mut snippet = String::new();
140        writeln!(&mut snippet, "     |").unwrap();
141        for (i, l) in contents.lines().enumerate() {
142            let l_lc = l.to_lowercase();
143            if i >= line {
144                break;
145            } else if l_lc.contains("postgres-") || l_lc.contains("secret") || l_lc.contains("url")
146            {
147                writeln!(
148                    &mut snippet,
149                    "{:4} | {} ... [rest of line truncated for security]",
150                    i + 1,
151                    l.get(0..20).unwrap_or(l)
152                )
153                .unwrap();
154            } else if i + 2 >= line {
155                writeln!(&mut snippet, "{:4} | {}", i + 1, l).unwrap();
156            }
157        }
158        write!(&mut snippet, "     | ").unwrap();
159
160        ErrorLocation {
161            filename: filename.map(|f| f.to_path_buf()),
162            snippet,
163            line,
164            col,
165        }
166    }
167}
168
169pub(crate) struct PosError {
170    pub(crate) source: anyhow::Error,
171    pub(crate) pos: Option<usize>,
172}
173
174impl PosError {
175    pub(crate) fn new(source: anyhow::Error, pos: usize) -> PosError {
176        PosError {
177            source,
178            pos: Some(pos),
179        }
180    }
181}
182
183impl From<anyhow::Error> for PosError {
184    fn from(source: anyhow::Error) -> PosError {
185        PosError { source, pos: None }
186    }
187}