use ::test::TestDesc;
use anyhow::Result;
use backtrace::BacktraceFrame;
#[cfg(target_arch = "wasm32")]
use beet_utils::prelude::*;
use colorize::*;
use std::panic::PanicHookInfo;
use std::path::Path;
use std::path::PathBuf;
pub struct BacktraceLocation {
pub cwd_path: PathBuf,
pub line_no: usize,
pub col_no: usize,
}
impl BacktraceLocation {
pub fn with_cwd(
cwd_path: impl AsRef<Path>,
line_no: usize,
col_no: usize,
) -> Self {
Self {
cwd_path: cwd_path.as_ref().to_path_buf(),
line_no,
col_no,
}
}
pub fn from_test_desc(desc: &TestDesc) -> Self {
Self::with_cwd(&desc.source_file, desc.start_line, desc.start_col)
}
pub fn from_panic_info(info: &PanicHookInfo, desc: &TestDesc) -> Self {
if let Some(location) = info.location() {
Self::with_cwd(
location.file(),
location.line() as usize,
location.column() as usize,
)
} else {
Self::from_test_desc(desc)
}
}
pub fn from_unresolved_frame(
frame: &BacktraceFrame,
) -> anyhow::Result<Self> {
let mut frame = frame.to_owned().clone();
frame.resolve();
let symbol = frame
.symbols()
.get(0)
.ok_or_else(|| anyhow::anyhow!("No symbols"))?;
let file = symbol
.filename()
.ok_or_else(|| anyhow::anyhow!("Bactrace has no file"))?;
let line_no = symbol.lineno().unwrap_or_default() as usize;
let col_no = symbol.colno().unwrap_or_default() as usize;
Ok(Self::with_cwd(file, line_no, col_no))
}
pub fn stack_line_string(&self, cwd_root: &Path) -> String {
let prefix = String::from("at").faint();
let ws_path = self
.cwd_path
.strip_prefix(&cwd_root)
.unwrap_or(&self.cwd_path)
.to_string_lossy()
.to_string()
.cyan();
let line_loc =
String::from(format!(":{}:{}", self.line_no, self.col_no)).faint();
format!("{} {}{}", prefix, ws_path, line_loc)
}
pub const LINE_CONTEXT_SIZE: usize = 2;
pub fn file_context(&self) -> Result<String> {
let cwd_root = Self::cwd_root();
let abs_path = cwd_root.join(&self.cwd_path);
let file = read_file(&abs_path)?;
let lines: Vec<&str> = file.split("\n").collect();
let start = usize::max(
0,
self.line_no.saturating_sub(Self::LINE_CONTEXT_SIZE + 1),
);
let end =
usize::min(lines.len() - 1, self.line_no + Self::LINE_CONTEXT_SIZE);
let mut output = String::new();
for i in start..end {
let curr_line_no = i + 1;
let is_err_line = curr_line_no == self.line_no;
let prefix =
String::from(if is_err_line { ">" } else { " " }).red();
let buffer = line_number_buffer(curr_line_no);
let line_prefix =
String::from(format!("{}{}|", curr_line_no, buffer)).faint();
let full_prefix = format!("{} {}", prefix, line_prefix);
output.push_str(&full_prefix);
output.push_str(lines[i]);
output.push('\n');
if is_err_line {
output.push_str(
&format!("{}|", " ".repeat(2 + LINE_BUFFER_LEN)).faint(),
);
output.push_str(&" ".repeat(self.col_no));
output.push_str(&String::from("^").red().as_str());
output.push('\n');
}
}
let mut stack_locations = vec![self.stack_line_string(&cwd_root)];
if std::env::var("RUST_BACKTRACE").unwrap_or_default() == "full" {
stack_locations.extend(
backtrace::Backtrace::new().frames().into_iter().filter_map(
|frame| {
Self::from_unresolved_frame(&frame)
.map(|loc| loc.stack_line_string(&cwd_root))
.ok()
},
),
);
} else if std::env::var("RUST_BACKTRACE").unwrap_or_default() == "1" {
let bt = backtrace::Backtrace::new();
let locations = bt
.frames()
.iter()
.filter(|frame| {
let sym = match frame.symbols().first() {
Some(s) => s,
None => return false,
};
if sym.filename().map_or(true, |f| {
let s = f.to_string_lossy();
s.contains(".cargo/registry/src/")
|| s.contains(".rustup/toolchains/")
|| s.contains("library/std/src/")
|| s.contains("library/alloc/src/")
|| s.contains("sweet/src/")
}) {
return false;
}
let name = match sym.name() {
Some(n) => n.to_string(),
None => return false,
};
!name.starts_with("std::")
&& !name.starts_with("core::")
&& !name.starts_with("backtrace::")
&& !name.starts_with("rayon::")
})
.filter_map(|frame| {
Self::from_unresolved_frame(&frame)
.map(|loc| loc.stack_line_string(&cwd_root))
.ok()
});
stack_locations.extend(locations);
}
output.push('\n');
output.push_str(&stack_locations.join("\n"));
Ok(output)
}
pub fn cwd_root() -> PathBuf {
#[cfg(not(target_arch = "wasm32"))]
return beet_utils::prelude::workspace_root();
#[cfg(target_arch = "wasm32")]
return js_runtime::sweet_root()
.map(PathBuf::from)
.unwrap_or_default();
}
}
fn read_file(path: &Path) -> Result<String> {
let bail = |cwd: &str| {
let sweet_root = std::env::var("SWEET_ROOT");
anyhow::anyhow!(
"Failed to read file:\ncwd:\t{}\npath:\t{}\nSWEET_ROOT: {:?}\n{CONTEXT}",
cwd,
&path.display(),
sweet_root
)
};
const CONTEXT: &str = r#"
This can happen when working with workspaces and the sweet root has not been set.
(This setting is required because rust does not have a CARGO_WORKSPACE_DIR)
Please configure the following:
``` .cargo/config.toml
[env]
SWEET_ROOT = { value = "", relative = true }
```
"#;
#[cfg(target_arch = "wasm32")]
let file = js_runtime::read_file(&path.to_string_lossy().to_string())
.ok_or_else(|| bail(&js_runtime::cwd()))?;
#[cfg(not(target_arch = "wasm32"))]
let file = beet_utils::prelude::fs_ext::read_to_string(path).map_err(|_| {
bail(
&std::env::current_dir()
.unwrap_or_default()
.display()
.to_string(),
)
})?;
Ok(file)
}
const LINE_BUFFER_LEN: usize = 3;
fn line_number_buffer(line_no: usize) -> String {
let line_no = line_no.to_string();
let digits = line_no.len();
let len = LINE_BUFFER_LEN.saturating_sub(digits);
" ".repeat(len)
}