#![allow(dead_code)]
use std::fmt::Debug;
#[track_caller]
pub fn assert_eq_logged<T: PartialEq + Debug>(context: &str, actual: T, expected: T) {
tracing::debug!(
context = context,
expected = ?expected,
actual = ?actual,
"asserting equality"
);
if actual != expected {
tracing::error!(
context = context,
expected = ?expected,
actual = ?actual,
"assertion failed: values not equal"
);
}
assert_eq!(
actual, expected,
"{context}: expected {expected:?}, got {actual:?}"
);
tracing::trace!(context = context, "assertion passed");
}
#[track_caller]
pub fn assert_true_logged(context: &str, value: bool) {
tracing::debug!(context = context, value = value, "asserting true");
if !value {
tracing::error!(
context = context,
value = value,
"assertion failed: expected true"
);
}
assert!(value, "{context}: expected true, got false");
tracing::trace!(context = context, "assertion passed");
}
#[track_caller]
pub fn assert_false_logged(context: &str, value: bool) {
tracing::debug!(context = context, value = value, "asserting false");
if value {
tracing::error!(
context = context,
value = value,
"assertion failed: expected false"
);
}
assert!(!value, "{context}: expected false, got true");
tracing::trace!(context = context, "assertion passed");
}
#[track_caller]
pub fn assert_ok_logged<T: Debug, E: Debug>(context: &str, result: Result<T, E>) -> T {
tracing::debug!(context = context, result = ?result, "asserting Ok");
match result {
Ok(value) => {
tracing::trace!(context = context, value = ?value, "assertion passed: got Ok");
value
}
Err(ref e) => {
tracing::error!(context = context, error = ?e, "assertion failed: expected Ok, got Err");
panic!("{context}: expected Ok, got Err({e:?})");
}
}
}
#[track_caller]
pub fn assert_err_logged<T: Debug, E: Debug>(context: &str, result: Result<T, E>) -> E {
tracing::debug!(context = context, result = ?result, "asserting Err");
match result {
Err(e) => {
tracing::trace!(context = context, error = ?e, "assertion passed: got Err");
e
}
Ok(ref value) => {
tracing::error!(
context = context,
value = ?value,
"assertion failed: expected Err, got Ok"
);
panic!("{context}: expected Err, got Ok({value:?})");
}
}
}
#[track_caller]
pub fn assert_some_logged<T: Debug>(context: &str, option: Option<T>) -> T {
tracing::debug!(context = context, option = ?option, "asserting Some");
match option {
Some(value) => {
tracing::trace!(context = context, value = ?value, "assertion passed: got Some");
value
}
None => {
tracing::error!(
context = context,
"assertion failed: expected Some, got None"
);
panic!("{context}: expected Some, got None");
}
}
}
#[track_caller]
pub fn assert_none_logged<T: Debug>(context: &str, option: Option<T>) {
tracing::debug!(context = context, option = ?option, "asserting None");
if let Some(ref value) = option {
tracing::error!(
context = context,
value = ?value,
"assertion failed: expected None, got Some"
);
panic!("{context}: expected None, got Some({value:?})");
}
tracing::trace!(context = context, "assertion passed");
}
#[track_caller]
pub fn assert_contains_logged(context: &str, haystack: &str, needle: &str) {
tracing::debug!(
context = context,
haystack_len = haystack.len(),
needle = needle,
"asserting contains"
);
if !haystack.contains(needle) {
tracing::error!(
context = context,
haystack = haystack,
needle = needle,
"assertion failed: string does not contain substring"
);
panic!(
"{context}: expected string to contain {needle:?}, but it doesn't.\nString: {haystack:?}"
);
}
tracing::trace!(context = context, "assertion passed");
}
#[track_caller]
pub fn assert_not_contains_logged(context: &str, haystack: &str, needle: &str) {
tracing::debug!(
context = context,
haystack_len = haystack.len(),
needle = needle,
"asserting not contains"
);
if haystack.contains(needle) {
tracing::error!(
context = context,
haystack = haystack,
needle = needle,
"assertion failed: string contains unwanted substring"
);
panic!(
"{context}: expected string to not contain {needle:?}, but it does.\nString: {haystack:?}"
);
}
tracing::trace!(context = context, "assertion passed");
}
#[track_caller]
pub fn assert_approx_eq_logged(context: &str, actual: f64, expected: f64, epsilon: f64) {
tracing::debug!(
context = context,
expected = expected,
actual = actual,
epsilon = epsilon,
"asserting approximate equality"
);
let diff = (actual - expected).abs();
if diff > epsilon {
tracing::error!(
context = context,
expected = expected,
actual = actual,
diff = diff,
epsilon = epsilon,
"assertion failed: values not approximately equal"
);
panic!("{context}: expected {expected} (within {epsilon}), got {actual} (diff: {diff})");
}
tracing::trace!(context = context, "assertion passed");
}
#[track_caller]
pub fn assert_len_logged<T>(context: &str, slice: &[T], expected_len: usize) {
let actual_len = slice.len();
tracing::debug!(
context = context,
expected_len = expected_len,
actual_len = actual_len,
"asserting length"
);
if actual_len != expected_len {
tracing::error!(
context = context,
expected_len = expected_len,
actual_len = actual_len,
"assertion failed: unexpected length"
);
panic!("{context}: expected length {expected_len}, got {actual_len}");
}
tracing::trace!(context = context, "assertion passed");
}
pub mod ansi {
pub const BOLD: &str = "\x1b[1m";
pub const DIM: &str = "\x1b[2m";
pub const ITALIC: &str = "\x1b[3m";
pub const UNDERLINE: &str = "\x1b[4m";
pub const BLINK: &str = "\x1b[5m";
pub const REVERSE: &str = "\x1b[7m";
pub const STRIKETHROUGH: &str = "\x1b[9m";
pub const RESET: &str = "\x1b[0m";
pub const FG_BLACK: &str = "\x1b[30m";
pub const FG_RED: &str = "\x1b[31m";
pub const FG_GREEN: &str = "\x1b[32m";
pub const FG_YELLOW: &str = "\x1b[33m";
pub const FG_BLUE: &str = "\x1b[34m";
pub const FG_MAGENTA: &str = "\x1b[35m";
pub const FG_CYAN: &str = "\x1b[36m";
pub const FG_WHITE: &str = "\x1b[37m";
pub const BG_BLACK: &str = "\x1b[40m";
pub const BG_RED: &str = "\x1b[41m";
pub const BG_GREEN: &str = "\x1b[42m";
pub const BG_YELLOW: &str = "\x1b[43m";
pub const BG_BLUE: &str = "\x1b[44m";
pub const BG_MAGENTA: &str = "\x1b[45m";
pub const BG_CYAN: &str = "\x1b[46m";
pub const BG_WHITE: &str = "\x1b[47m";
}
#[must_use]
pub fn has_ansi_codes(output: &str) -> bool {
output.contains("\x1b[")
}
#[track_caller]
pub fn assert_has_ansi_codes(context: &str, output: &str) {
tracing::debug!(
context = context,
output_len = output.len(),
"asserting has ANSI codes"
);
if !has_ansi_codes(output) {
tracing::error!(
context = context,
output = output,
"assertion failed: no ANSI codes found"
);
panic!("{context}: expected ANSI escape codes but found none.\nOutput: {output:?}");
}
tracing::trace!(context = context, "assertion passed: has ANSI codes");
}
#[track_caller]
pub fn assert_no_ansi_codes(context: &str, output: &str) {
tracing::debug!(
context = context,
output_len = output.len(),
"asserting no ANSI codes"
);
if has_ansi_codes(output) {
tracing::error!(
context = context,
output = output,
"assertion failed: unexpected ANSI codes found"
);
panic!("{context}: expected no ANSI codes but found some.\nOutput: {output:?}");
}
tracing::trace!(context = context, "assertion passed: no ANSI codes");
}
#[must_use]
pub fn has_style(output: &str, ansi_code: &str) -> bool {
output.contains(ansi_code)
}
#[track_caller]
pub fn assert_has_bold(context: &str, output: &str) {
tracing::debug!(context = context, "asserting has bold style");
let has_bold = output.contains("\x1b[1m") || output.contains("\x1b[1;");
if !has_bold {
tracing::error!(
context = context,
output = output,
"assertion failed: no bold ANSI code found"
);
panic!("{context}: expected bold style (SGR 1) but not found.\nOutput: {output:?}");
}
tracing::trace!(context = context, "assertion passed: has bold");
}
#[track_caller]
pub fn assert_has_italic(context: &str, output: &str) {
tracing::debug!(context = context, "asserting has italic style");
let has_italic = output.contains("\x1b[3m") || output.contains(";3m") || output.contains(";3;");
if !has_italic {
tracing::error!(
context = context,
output = output,
"assertion failed: no italic ANSI code found"
);
panic!("{context}: expected italic style (SGR 3) but not found.\nOutput: {output:?}");
}
tracing::trace!(context = context, "assertion passed: has italic");
}
#[track_caller]
pub fn assert_has_underline(context: &str, output: &str) {
tracing::debug!(context = context, "asserting has underline style");
let has_underline =
output.contains("\x1b[4m") || output.contains(";4m") || output.contains(";4;");
if !has_underline {
tracing::error!(
context = context,
output = output,
"assertion failed: no underline ANSI code found"
);
panic!("{context}: expected underline style (SGR 4) but not found.\nOutput: {output:?}");
}
tracing::trace!(context = context, "assertion passed: has underline");
}
#[track_caller]
pub fn assert_has_color(context: &str, output: &str, color_name: &str) {
tracing::debug!(context = context, color = color_name, "asserting has color");
let has_color = match color_name.to_lowercase().as_str() {
"black" => {
output.contains("\x1b[30m")
|| output.contains("\x1b[38;5;0m")
|| output.contains("\x1b[38;2;0;0;0m")
}
"red" => {
output.contains("\x1b[31m")
|| output.contains("\x1b[38;5;")
|| output.contains("\x1b[38;2;")
}
"green" => {
output.contains("\x1b[32m")
|| output.contains("\x1b[38;5;")
|| output.contains("\x1b[38;2;")
}
"yellow" => {
output.contains("\x1b[33m")
|| output.contains("\x1b[38;5;")
|| output.contains("\x1b[38;2;")
}
"blue" => {
output.contains("\x1b[34m")
|| output.contains("\x1b[38;5;")
|| output.contains("\x1b[38;2;")
}
"magenta" => {
output.contains("\x1b[35m")
|| output.contains("\x1b[38;5;")
|| output.contains("\x1b[38;2;")
}
"cyan" => {
output.contains("\x1b[36m")
|| output.contains("\x1b[38;5;")
|| output.contains("\x1b[38;2;")
}
"white" => {
output.contains("\x1b[37m")
|| output.contains("\x1b[38;5;15m")
|| output.contains("\x1b[38;2;")
}
_ => {
output.contains("\x1b[3") || output.contains("\x1b[38;")
}
};
if !has_color {
tracing::error!(
context = context,
color = color_name,
output = output,
"assertion failed: color not found"
);
panic!("{context}: expected {color_name} color but not found.\nOutput: {output:?}");
}
tracing::trace!(
context = context,
color = color_name,
"assertion passed: has color"
);
}
#[track_caller]
pub fn assert_has_reset(context: &str, output: &str) {
tracing::debug!(context = context, "asserting has reset");
if !output.contains(ansi::RESET) {
tracing::error!(
context = context,
output = output,
"assertion failed: no reset code found"
);
panic!("{context}: expected reset code (SGR 0) but not found.\nOutput: {output:?}");
}
tracing::trace!(context = context, "assertion passed: has reset");
}
#[must_use]
pub fn strip_ansi(s: &str) -> String {
let re = regex::Regex::new(r"\x1b\[[0-9;]*m|\x1b\]8;;[^\x1b]*\x1b\\").unwrap();
re.replace_all(s, "").to_string()
}
#[track_caller]
pub fn assert_no_raw_markup(context: &str, output: &str) {
tracing::debug!(context = context, "asserting no raw markup tags");
let markup_patterns = [
"[bold]",
"[/bold]",
"[italic]",
"[/italic]",
"[underline]",
"[/underline]",
"[red]",
"[/red]",
"[green]",
"[/green]",
"[blue]",
"[/blue]",
"[yellow]",
"[/yellow]",
"[/]",
];
for pattern in markup_patterns {
if output.contains(pattern) {
tracing::error!(
context = context,
pattern = pattern,
output = output,
"assertion failed: raw markup tag found"
);
panic!("{context}: found raw markup tag '{pattern}' in output.\nOutput: {output:?}");
}
}
tracing::trace!(context = context, "assertion passed: no raw markup");
}
#[must_use]
pub fn count_ansi_code(output: &str, ansi_code: &str) -> usize {
output.matches(ansi_code).count()
}
#[must_use]
pub fn extract_styled_regions(output: &str) -> Vec<(String, bool)> {
let re = regex::Regex::new(r"(\x1b\[[0-9;]*m)").unwrap();
let mut regions = Vec::new();
let mut last_end = 0;
let mut in_styled_region = false;
for cap in re.captures_iter(output) {
let mat = cap.get(0).unwrap();
if mat.start() > last_end {
let text = &output[last_end..mat.start()];
if !text.is_empty() {
regions.push((text.to_string(), in_styled_region));
}
}
let code = cap.get(0).map_or("", |m| m.as_str());
in_styled_region = code != ansi::RESET;
last_end = mat.end();
}
if last_end < output.len() {
let text = &output[last_end..];
if !text.is_empty() {
regions.push((text.to_string(), in_styled_region));
}
}
regions
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::init_test_logging;
#[test]
fn test_assert_eq_logged_pass() {
init_test_logging();
assert_eq_logged("simple equality", 42, 42);
}
#[test]
#[should_panic(expected = "expected 42")]
fn test_assert_eq_logged_fail() {
init_test_logging();
assert_eq_logged("will fail", 0, 42);
}
#[test]
fn test_assert_ok_logged_pass() {
init_test_logging();
let result: Result<i32, &str> = Ok(42);
let value = assert_ok_logged("ok result", result);
assert_eq!(value, 42);
}
#[test]
fn test_assert_contains_logged_pass() {
init_test_logging();
assert_contains_logged("substring", "hello world", "world");
}
#[test]
fn test_assert_len_logged_pass() {
init_test_logging();
let vec = vec![1, 2, 3, 4, 5];
assert_len_logged("vec length", &vec, 5);
}
}