pub mod bench;
pub mod explain;
pub mod fuzz;
pub mod lint;
pub mod matrix;
pub mod profile;
pub mod screenshot;
pub mod stress;
use std::panic::{catch_unwind, AssertUnwindSafe};
use rich::cells::cell_len;
use rich::protocol::{Support, TargetCapabilities};
use rich::{Console, ConsoleOptions, Renderable, Segment, Theme};
use crate::capabilities::ColorDepth;
use crate::target::{RenderTarget, TargetKind};
pub use screenshot::{assert_screenshots, Approvals, Matrix, Outcome, Screenshot, Shot};
pub(crate) const UNBOUNDED_HEIGHT: usize = 10_000;
#[derive(Clone, Debug)]
pub(crate) struct Probe {
pub width: usize,
pub height: Option<usize>,
pub color: ColorDepth,
pub unicode: bool,
pub hyperlinks: bool,
pub theme: Theme,
}
impl Probe {
pub fn new(width: usize) -> Self {
Probe {
width,
height: None,
color: ColorDepth::TrueColor,
unicode: true,
hyperlinks: true,
theme: crate::theme::extended_theme(),
}
}
pub fn from_capabilities(caps: &TargetCapabilities, theme: Theme) -> Self {
Probe {
width: caps.width,
height: None,
color: ColorDepth::from_color_system(caps.color_system),
unicode: caps.unicode,
hyperlinks: caps.hyperlinks,
theme,
}
}
pub fn capabilities(&self) -> TargetCapabilities {
TargetCapabilities {
width: self.width,
height: self.height.unwrap_or(UNBOUNDED_HEIGHT),
color_system: self.color.color_system(),
interactive: false,
unicode: self.unicode,
hyperlinks: self.hyperlinks,
sixel: Support::Unsupported,
}
}
pub fn target(&self) -> RenderTarget {
RenderTarget::new(TargetKind::Capture, self.capabilities(), self.theme.clone())
}
pub fn options(&self, console: &Console) -> ConsoleOptions {
let mut options = console.options().update_width(self.width);
options.height = self.height;
options
}
pub fn segments(&self, renderable: &dyn Renderable) -> Vec<Segment> {
if self.width == 0 {
return Vec::new();
}
let console = self.target().console();
let mut segments = renderable.rich_render(&console, &self.options(&console));
segments.retain(|s| !s.control);
if !self.hyperlinks {
for segment in &mut segments {
segment.style = segment.style.as_ref().map(|s| s.update_link(None));
}
}
segments
}
pub fn try_segments(&self, renderable: &dyn Renderable) -> Result<Vec<Segment>, String> {
catch_unwind(AssertUnwindSafe(|| self.segments(renderable))).map_err(panic_message)
}
}
pub(crate) fn measure_shortfall(
renderable: &dyn Renderable,
probe: &Probe,
at: usize,
full: &[String],
overflow_counts: bool,
) -> Option<String> {
let mut narrow = probe.clone();
narrow.width = at;
let lines = match narrow.try_segments(renderable) {
Ok(segments) => plain_lines(&segments),
Err(message) => {
return Some(format!(
"a render at the measured maximum {at} panicked: {message}"
))
}
};
if overflow_counts {
if let Some(wide) = lines.iter().map(|l| cell_len(l)).max().filter(|&w| w > at) {
return Some(format!(
"measure maximum is {at} but a render at that width is {wide} cells wide"
));
}
}
if lines.len() <= full.len() {
return None;
}
let widest = |lines: &[String]| lines.iter().map(|l| visible_width(l)).max().unwrap_or(0);
let mut wider = probe.clone();
wider.width = probe.width + 4;
let expands = wider
.try_segments(renderable)
.is_ok_and(|segments| widest(&plain_lines(&segments)) > widest(full));
(!expands).then(|| {
format!(
"measure maximum is {at} but a render at that width takes {} lines, not {}",
lines.len(),
full.len()
)
})
}
pub(crate) fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&str>() {
(*s).to_owned()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"<non-string panic payload>".to_owned()
}
}
pub(crate) fn plain_lines(segments: &[Segment]) -> Vec<String> {
let text: String = segments
.iter()
.filter(|s| !s.control)
.map(|s| s.text.as_str())
.collect();
if text.is_empty() {
return Vec::new();
}
let mut lines: Vec<String> = text.split('\n').map(str::to_owned).collect();
if text.ends_with('\n') {
lines.pop();
}
lines
}
pub(crate) fn max_width(lines: &[String]) -> usize {
lines.iter().map(|l| cell_len(l)).max().unwrap_or(0)
}
pub(crate) fn visible_width(line: &str) -> usize {
cell_len(line.trim_matches(' '))
}
pub(crate) fn is_decoration(c: char, ascii: bool) -> bool {
matches!(c, '\u{2500}'..='\u{259f}') || (ascii && matches!(c, '+' | '-' | '|' | '='))
}
pub(crate) fn is_ellipsis(c: char) -> bool {
c == '…'
}
pub(crate) fn content_chars(lines: &[String], ascii: bool) -> Vec<char> {
lines
.iter()
.flat_map(|l| l.chars())
.filter(|&c| !c.is_whitespace() && !is_decoration(c, ascii) && !is_ellipsis(c))
.collect()
}
pub(crate) fn depth_key(depth: ColorDepth) -> &'static str {
match depth {
ColorDepth::None => "none",
ColorDepth::Ansi16 => "ansi16",
ColorDepth::Ansi256 => "ansi256",
ColorDepth::TrueColor => "truecolor",
}
}
pub(crate) fn plural(n: usize, word: &str) -> String {
format!("{n} {word}{}", if n == 1 { "" } else { "s" })
}
pub(crate) struct NoHeight<'a>(pub &'a dyn Renderable);
impl Renderable for NoHeight<'_> {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let mut options = options.clone();
options.height = None;
self.0.rich_render(console, &options)
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> rich::measure::Measurement {
self.0.measure(console, options)
}
}
pub(crate) fn table_then_line(
table: Option<rich::Table>,
summary: String,
console: &Console,
options: &ConsoleOptions,
) -> Vec<Segment> {
let mut out = Vec::new();
if let Some(table) = table {
out = table.rich_render(console, options);
if out.last().is_some_and(|s| !s.text.ends_with('\n')) {
out.push(Segment::line());
}
}
out.extend(rich::Text::new(summary).rich_render(console, options));
if out.last().is_some_and(|s| !s.text.ends_with('\n')) {
out.push(Segment::line());
}
out
}