use crate::color::{
ansi256_to_named_color, ansi256_to_rgb, rgb_to_ansi256, rgb_to_named_color, ColorSpec,
NamedColor,
};
use crate::config::{
get_stderr_terminal_override_for_tests, get_terminal_override_for_tests,
set_stderr_terminal_override_for_tests, set_terminal_override_for_tests,
};
use crate::terminal::{detect_color_level, tests::TestEnv};
use crate::*;
use rstest::*;
use std::env;
use std::ffi::OsString;
use std::io::IsTerminal;
use std::sync::{LazyLock, Mutex, MutexGuard};
static TEST_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
const COLOR_ENV_KEYS: [&str; 10] = [
"NO_COLOR",
"FORCE_COLOR",
"CLICOLOR",
"CLICOLOR_FORCE",
"TERM",
"COLORTERM",
"CI",
"WT_SESSION",
"ConEmuANSI",
"ANSICON",
];
struct TestStateGuard {
_lock: MutexGuard<'static, ()>,
previous_mode: ColorMode,
previous_depth_mode: ColorDepthMode,
previous_env: Vec<(&'static str, Option<OsString>)>,
previous_terminal_override: Option<bool>,
previous_stderr_terminal_override: Option<bool>,
}
impl TestStateGuard {
fn colors_enabled(mode: ColorMode) -> Self {
Self::with_depth(mode, ColorDepthMode::TrueColor, None, Some(false))
}
fn no_color(mode: ColorMode) -> Self {
Self::with_depth(mode, ColorDepthMode::Auto, Some("1"), Some(false))
}
fn auto_terminal(is_terminal: bool) -> Self {
Self::with_depth(
ColorMode::Auto,
ColorDepthMode::Auto,
None,
Some(is_terminal),
)
}
fn with_state(
mode: ColorMode,
no_color: Option<&str>,
terminal_override: Option<bool>,
) -> Self {
Self::with_depth(mode, ColorDepthMode::Auto, no_color, terminal_override)
}
fn with_depth(
mode: ColorMode,
depth_mode: ColorDepthMode,
no_color: Option<&str>,
terminal_override: Option<bool>,
) -> Self {
let guard = TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let previous_mode = ColorizeConfig::color_mode();
let previous_depth_mode = ColorizeConfig::color_depth_mode();
let previous_env = COLOR_ENV_KEYS
.into_iter()
.map(|key| (key, env::var_os(key)))
.collect();
let previous_terminal_override = get_terminal_override_for_tests();
let previous_stderr_terminal_override = get_stderr_terminal_override_for_tests();
for key in COLOR_ENV_KEYS {
env::remove_var(key);
}
match no_color {
Some(value) => env::set_var("NO_COLOR", value),
None => env::remove_var("NO_COLOR"),
}
env::set_var("TERM", "xterm-256color");
ColorizeConfig::set_color_mode(mode);
ColorizeConfig::set_color_depth_mode(depth_mode);
set_terminal_override_for_tests(terminal_override);
set_stderr_terminal_override_for_tests(None);
Self {
_lock: guard,
previous_mode,
previous_depth_mode,
previous_env,
previous_terminal_override,
previous_stderr_terminal_override,
}
}
}
impl Drop for TestStateGuard {
fn drop(&mut self) {
ColorizeConfig::set_color_mode(self.previous_mode);
ColorizeConfig::set_color_depth_mode(self.previous_depth_mode);
set_terminal_override_for_tests(self.previous_terminal_override);
set_stderr_terminal_override_for_tests(self.previous_stderr_terminal_override);
for (key, value) in &self.previous_env {
match value {
Some(value) => env::set_var(key, value),
None => env::remove_var(key),
}
}
}
}
#[rstest]
#[case("red", "\x1b[31mtest\x1b[0m")]
#[case("green", "\x1b[32mtest\x1b[0m")]
#[case("yellow", "\x1b[33mtest\x1b[0m")]
#[case("blue", "\x1b[34mtest\x1b[0m")]
#[case("magenta", "\x1b[35mtest\x1b[0m")]
#[case("cyan", "\x1b[36mtest\x1b[0m")]
#[case("white", "\x1b[37mtest\x1b[0m")]
#[case("black", "\x1b[30mtest\x1b[0m")]
fn test_basic_colors(#[case] color: &str, #[case] expected: &str) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let text = "test";
let actual = match color {
"red" => text.red().to_string(),
"green" => text.green().to_string(),
"yellow" => text.yellow().to_string(),
"blue" => text.blue().to_string(),
"magenta" => text.magenta().to_string(),
"cyan" => text.cyan().to_string(),
"white" => text.white().to_string(),
"black" => text.black().to_string(),
_ => unreachable!(),
};
assert_eq!(actual, expected);
}
#[rstest]
#[case("bright_black", "\x1b[90mtest\x1b[0m")]
#[case("bright_red", "\x1b[91mtest\x1b[0m")]
#[case("bright_green", "\x1b[92mtest\x1b[0m")]
#[case("bright_yellow", "\x1b[93mtest\x1b[0m")]
#[case("bright_blue", "\x1b[94mtest\x1b[0m")]
#[case("bright_magenta", "\x1b[95mtest\x1b[0m")]
#[case("bright_cyan", "\x1b[96mtest\x1b[0m")]
#[case("bright_white", "\x1b[97mtest\x1b[0m")]
fn test_bright_colors(#[case] color: &str, #[case] expected: &str) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let text = "test";
let actual = match color {
"bright_black" => text.bright_black().to_string(),
"bright_red" => text.bright_red().to_string(),
"bright_green" => text.bright_green().to_string(),
"bright_yellow" => text.bright_yellow().to_string(),
"bright_blue" => text.bright_blue().to_string(),
"bright_magenta" => text.bright_magenta().to_string(),
"bright_cyan" => text.bright_cyan().to_string(),
"bright_white" => text.bright_white().to_string(),
_ => unreachable!(),
};
assert_eq!(actual, expected);
}
#[rstest]
#[case("on_red", "\x1b[41mtest\x1b[0m")]
#[case("on_green", "\x1b[42mtest\x1b[0m")]
#[case("on_yellow", "\x1b[43mtest\x1b[0m")]
#[case("on_blue", "\x1b[44mtest\x1b[0m")]
#[case("on_magenta", "\x1b[45mtest\x1b[0m")]
#[case("on_cyan", "\x1b[46mtest\x1b[0m")]
#[case("on_white", "\x1b[47mtest\x1b[0m")]
#[case("on_black", "\x1b[40mtest\x1b[0m")]
fn test_background_colors(#[case] color: &str, #[case] expected: &str) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let text = "test";
let actual = match color {
"on_red" => text.on_red().to_string(),
"on_green" => text.on_green().to_string(),
"on_yellow" => text.on_yellow().to_string(),
"on_blue" => text.on_blue().to_string(),
"on_magenta" => text.on_magenta().to_string(),
"on_cyan" => text.on_cyan().to_string(),
"on_white" => text.on_white().to_string(),
"on_black" => text.on_black().to_string(),
_ => unreachable!(),
};
assert_eq!(actual, expected);
}
#[rstest]
#[case("on_bright_black", "\x1b[100mtest\x1b[0m")]
#[case("on_bright_red", "\x1b[101mtest\x1b[0m")]
#[case("on_bright_green", "\x1b[102mtest\x1b[0m")]
#[case("on_bright_yellow", "\x1b[103mtest\x1b[0m")]
#[case("on_bright_blue", "\x1b[104mtest\x1b[0m")]
#[case("on_bright_magenta", "\x1b[105mtest\x1b[0m")]
#[case("on_bright_cyan", "\x1b[106mtest\x1b[0m")]
#[case("on_bright_white", "\x1b[107mtest\x1b[0m")]
fn test_bright_background_colors(#[case] color: &str, #[case] expected: &str) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let text = "test";
let actual = match color {
"on_bright_black" => text.on_bright_black().to_string(),
"on_bright_red" => text.on_bright_red().to_string(),
"on_bright_green" => text.on_bright_green().to_string(),
"on_bright_yellow" => text.on_bright_yellow().to_string(),
"on_bright_blue" => text.on_bright_blue().to_string(),
"on_bright_magenta" => text.on_bright_magenta().to_string(),
"on_bright_cyan" => text.on_bright_cyan().to_string(),
"on_bright_white" => text.on_bright_white().to_string(),
_ => unreachable!(),
};
assert_eq!(actual, expected);
}
#[rstest]
#[case("bold", "\x1b[1mtest\x1b[0m")]
#[case("dim", "\x1b[2mtest\x1b[0m")]
#[case("italic", "\x1b[3mtest\x1b[0m")]
#[case("underline", "\x1b[4mtest\x1b[0m")]
#[case("inverse", "\x1b[7mtest\x1b[0m")]
#[case("strikethrough", "\x1b[9mtest\x1b[0m")]
fn test_styles(#[case] style: &str, #[case] expected: &str) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let text = "test";
let actual = match style {
"bold" => text.bold().to_string(),
"dim" => text.dim().to_string(),
"italic" => text.italic().to_string(),
"underline" => text.underline().to_string(),
"inverse" => text.inverse().to_string(),
"strikethrough" => text.strikethrough().to_string(),
_ => unreachable!(),
};
assert_eq!(actual, expected);
}
#[rstest]
#[case(255, 128, 0)]
#[case(0, 255, 0)]
#[case(128, 128, 128)]
#[case(0, 0, 0)]
#[case(255, 255, 255)]
fn test_rgb_colors(#[case] r: u8, #[case] g: u8, #[case] b: u8) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let text = "test";
assert_eq!(
text.rgb(r, g, b).to_string(),
format!("\x1b[38;2;{};{};{}m{}\x1b[0m", r, g, b, text)
);
assert_eq!(
text.on_rgb(r, g, b).to_string(),
format!("\x1b[48;2;{};{};{}m{}\x1b[0m", r, g, b, text)
);
}
#[rstest]
#[case(0, "\x1b[38;5;0mtest\x1b[0m")]
#[case(255, "\x1b[38;5;255mtest\x1b[0m")]
fn test_ansi256_foreground_colors(#[case] index: u8, #[case] expected: &str) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!("test".ansi256(index).to_string(), expected);
}
#[rstest]
#[case(0, "\x1b[48;5;0mtest\x1b[0m")]
#[case(255, "\x1b[48;5;255mtest\x1b[0m")]
fn test_ansi256_background_colors(#[case] index: u8, #[case] expected: &str) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!("test".on_ansi256(index).to_string(), expected);
}
#[test]
fn test_color256_aliases_match_ansi256_methods() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!(
"test".color256(208).to_string(),
"test".ansi256(208).to_string()
);
assert_eq!(
"test".on_color256(236).to_string(),
"test".on_ansi256(236).to_string()
);
}
#[rstest]
#[case("#ff8000", 255, 128, 0)]
#[case("#f80", 255, 136, 0)]
#[case("#00ff00", 0, 255, 0)]
#[case("0f8", 0, 255, 136)]
#[case("#808080", 128, 128, 128)]
#[case("#000000", 0, 0, 0)]
#[case("#ffffff", 255, 255, 255)]
fn test_hex_colors(#[case] hex: &str, #[case] r: u8, #[case] g: u8, #[case] b: u8) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let text = "test";
assert_eq!(
text.hex(hex).to_string(),
format!("\x1b[38;2;{};{};{}m{}\x1b[0m", r, g, b, text)
);
assert_eq!(
text.on_hex(hex).to_string(),
format!("\x1b[48;2;{};{};{}m{}\x1b[0m", r, g, b, text)
);
let hex_without_prefix = hex.trim_start_matches('#');
assert_eq!(
text.hex(hex_without_prefix).to_string(),
format!("\x1b[38;2;{};{};{}m{}\x1b[0m", r, g, b, text)
);
assert_eq!(
text.on_hex(hex_without_prefix).to_string(),
format!("\x1b[48;2;{};{};{}m{}\x1b[0m", r, g, b, text)
);
}
#[rstest]
#[case("invalid")]
#[case("#12")]
#[case("#1234")]
#[case("#12345678")]
#[case("not-a-color")]
#[case("#12345")]
#[case("#1234567")]
#[case("#xyz")]
fn test_invalid_hex_returns_plain_text(#[case] hex: &str) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let text = "test";
assert_eq!(text.hex(hex).to_string(), "test");
assert_eq!(text.on_hex(hex).to_string(), "test");
assert_eq!(text.red().hex(hex).to_string(), "test");
assert_eq!(text.on_blue().on_hex(hex).to_string(), "test");
}
#[test]
fn test_clear_returns_plain_text() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!("test".clear().to_string(), "test");
assert_eq!("test".red().clear().to_string(), "test");
assert_eq!("test".ansi256(208).clear().to_string(), "test");
assert_eq!(
"test".blue().italic().on_yellow().clear().to_string(),
"test"
);
}
#[test]
fn test_chaining_composes_once() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!("test".red().bold().to_string(), "\x1b[1;31mtest\x1b[0m");
assert_eq!(
"test".blue().italic().on_yellow().to_string(),
"\x1b[3;34;43mtest\x1b[0m"
);
assert_eq!(
"test".rgb(255, 128, 0).on_blue().to_string(),
"\x1b[38;2;255;128;0;44mtest\x1b[0m"
);
assert_eq!(
"test".ansi256(208).bold().on_ansi256(236).to_string(),
"\x1b[1;38;5;208;48;5;236mtest\x1b[0m"
);
}
#[test]
fn test_conflicting_chains_use_last_color() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!("test".red().green().to_string(), "\x1b[32mtest\x1b[0m");
assert_eq!("test".on_red().on_blue().to_string(), "\x1b[44mtest\x1b[0m");
assert_eq!(
"test".red().ansi256(208).to_string(),
"\x1b[38;5;208mtest\x1b[0m"
);
assert_eq!("test".ansi256(208).red().to_string(), "\x1b[31mtest\x1b[0m");
assert_eq!(
"test".on_blue().on_ansi256(236).to_string(),
"\x1b[48;5;236mtest\x1b[0m"
);
}
#[test]
fn test_style_flags_accumulate() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!("test".bold().dim().to_string(), "\x1b[1;2mtest\x1b[0m");
assert_eq!(
"test".underline().italic().strikethrough().to_string(),
"\x1b[3;4;9mtest\x1b[0m"
);
}
#[test]
fn test_string_and_plain_text_access() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let string = String::from("test");
let styled = string.red().bold();
assert_eq!(styled.to_string(), "\x1b[1;31mtest\x1b[0m");
assert_eq!(styled.plain_text(), "test");
}
#[test]
fn test_format_macro_uses_display() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!(format!("{}", "test".red()), "\x1b[31mtest\x1b[0m");
}
#[test]
fn test_empty_text_keeps_styling_when_color_is_enabled() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!("".red().to_string(), "\x1b[31m\x1b[0m");
assert_eq!("".bold().to_string(), "\x1b[1m\x1b[0m");
assert_eq!(
"".rgb(255, 128, 0).to_string(),
"\x1b[38;2;255;128;0m\x1b[0m"
);
}
fn assert_rgb_approx_eq(actual: &str, expected: &str) {
let extract_rgb = |s: &str| {
let start = s.find("38;2;").or_else(|| s.find("48;2;"));
if let Some(start) = start {
let sequence = &s[start..];
let parts: Vec<&str> = sequence.split(';').collect();
let r = parts.get(2).and_then(|part| part.parse::<i32>().ok());
let g = parts.get(3).and_then(|part| part.parse::<i32>().ok());
let b = parts
.get(4)
.and_then(|part| part.split('m').next())
.and_then(|part| part.parse::<i32>().ok());
if let (Some(r), Some(g), Some(b)) = (r, g, b) {
return (r, g, b);
}
}
panic!("Invalid ANSI color sequence");
};
let (r1, g1, b1) = extract_rgb(actual);
let (r2, g2, b2) = extract_rgb(expected);
assert!(
(r1 - r2).abs() <= 1 && (g1 - g2).abs() <= 1 && (b1 - b2).abs() <= 1,
"RGB values differ by more than 1: ({}, {}, {}) vs ({}, {}, {})",
r1,
g1,
b1,
r2,
g2,
b2
);
}
#[rstest]
#[case(0.0, 100.0, 50.0, 255, 0, 0)]
#[case(60.0, 100.0, 50.0, 255, 255, 0)]
#[case(90.0, 100.0, 50.0, 128, 255, 0)]
#[case(120.0, 100.0, 50.0, 0, 255, 0)]
#[case(150.0, 100.0, 50.0, 0, 255, 128)]
#[case(180.0, 100.0, 50.0, 0, 255, 255)]
#[case(210.0, 100.0, 50.0, 0, 128, 255)]
#[case(240.0, 100.0, 50.0, 0, 0, 255)]
#[case(300.0, 100.0, 50.0, 255, 0, 255)]
#[case(330.0, 100.0, 50.0, 255, 0, 128)]
#[case(360.0, 100.0, 50.0, 255, 0, 0)]
fn test_hsl_colors_comprehensive(
#[case] h: f32,
#[case] s: f32,
#[case] l: f32,
#[case] r: u8,
#[case] g: u8,
#[case] b: u8,
) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let actual = "test".hsl(h, s, l).to_string();
let expected = "test".rgb(r, g, b).to_string();
assert_rgb_approx_eq(&actual, &expected);
}
#[test]
fn test_hsl_edge_cases() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let assert_hsl_rgb = |h, s, l, r, g, b| {
let actual = "test".hsl(h, s, l).to_string();
let expected = "test".rgb(r, g, b).to_string();
assert_rgb_approx_eq(&actual, &expected);
};
assert_hsl_rgb(0.0, 0.0, 0.0, 0, 0, 0);
assert_hsl_rgb(0.0, 0.0, 25.0, 64, 64, 64);
assert_hsl_rgb(0.0, 0.0, 50.0, 128, 128, 128);
assert_hsl_rgb(0.0, 0.0, 75.0, 191, 191, 191);
assert_hsl_rgb(0.0, 0.0, 100.0, 255, 255, 255);
assert_hsl_rgb(0.0, 25.0, 50.0, 159, 96, 96);
assert_hsl_rgb(0.0, 50.0, 50.0, 191, 64, 64);
assert_hsl_rgb(0.0, 75.0, 50.0, 223, 32, 32);
assert_hsl_rgb(120.0, 100.0, 25.0, 0, 128, 0);
assert_hsl_rgb(120.0, 100.0, 75.0, 128, 255, 128);
}
#[test]
fn test_hsl_background_colors() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let actual = "test".on_hsl(0.0, 100.0, 50.0).to_string();
let expected = "test".on_rgb(255, 0, 0).to_string();
assert_rgb_approx_eq(&actual, &expected);
let actual = "test".on_hsl(120.0, 100.0, 50.0).to_string();
let expected = "test".on_rgb(0, 255, 0).to_string();
assert_rgb_approx_eq(&actual, &expected);
let actual = "test".on_hsl(240.0, 100.0, 50.0).to_string();
let expected = "test".on_rgb(0, 0, 255).to_string();
assert_rgb_approx_eq(&actual, &expected);
}
#[test]
fn test_color_mode_always_forces_color() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!("test".red().to_string(), "\x1b[31mtest\x1b[0m");
}
#[test]
fn test_color_mode_auto_respects_tty_detection() {
let _guard = TestStateGuard::auto_terminal(false);
assert_eq!("test".red().to_string(), "test");
}
#[test]
fn test_color_mode_auto_uses_real_stdout_terminal_state_without_override() {
let _guard = TestStateGuard::with_state(ColorMode::Auto, None, None);
let expected = if std::io::stdout().is_terminal() {
ColorLevel::Ansi256
} else {
ColorLevel::NoColor
};
assert_eq!(ColorizeConfig::color_level(RenderTarget::Stdout), expected);
}
#[test]
fn test_render_auto_uses_real_stderr_terminal_state_without_override() {
let _guard = TestStateGuard::with_state(ColorMode::Auto, None, None);
set_stderr_terminal_override_for_tests(None);
let expected = if std::io::stderr().is_terminal() {
"\x1b[31mtest\x1b[0m"
} else {
"test"
};
assert_eq!("test".red().render(RenderTarget::Stderr), expected);
}
#[test]
fn test_color_mode_auto_enables_color_for_terminal_output() {
let _guard = TestStateGuard::auto_terminal(true);
assert_eq!("test".red().to_string(), "\x1b[31mtest\x1b[0m");
}
#[test]
fn test_render_auto_uses_stderr_terminal_state() {
let _guard = TestStateGuard::with_state(ColorMode::Auto, None, Some(false));
set_stderr_terminal_override_for_tests(Some(true));
assert_eq!(
"test".red().render(RenderTarget::Stderr),
"\x1b[31mtest\x1b[0m"
);
}
#[test]
fn test_render_auto_uses_custom_terminal_state() {
let _guard = TestStateGuard::with_state(ColorMode::Auto, None, Some(true));
assert_eq!("test".red().render(RenderTarget::Terminal(false)), "test");
assert_eq!(
"test".red().render(RenderTarget::Terminal(true)),
"\x1b[31mtest\x1b[0m"
);
}
#[test]
fn test_render_always_ignores_target_terminal_state() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!(
"test".red().render(RenderTarget::Terminal(false)),
"\x1b[31mtest\x1b[0m"
);
}
#[test]
fn test_render_never_disables_color_for_all_targets() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Never);
assert_eq!("test".red().render(RenderTarget::Stdout), "test");
assert_eq!("test".red().render(RenderTarget::Stderr), "test");
assert_eq!("test".red().render(RenderTarget::Terminal(true)), "test");
}
#[test]
fn test_render_respects_no_color_in_always_mode() {
let _guard = TestStateGuard::no_color(ColorMode::Always);
assert_eq!("test".red().render(RenderTarget::Terminal(true)), "test");
}
#[test]
fn test_display_remains_stdout_based_in_auto_mode() {
let _guard = TestStateGuard::with_state(ColorMode::Auto, None, Some(false));
set_stderr_terminal_override_for_tests(Some(true));
assert_eq!("test".red().to_string(), "test");
}
#[test]
fn test_color_mode_never_disables_color() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Never);
assert_eq!("test".red().to_string(), "test");
assert_eq!("test".blue().italic().on_yellow().to_string(), "test");
assert_eq!("test".ansi256(208).to_string(), "test");
}
#[test]
fn test_no_color_disables_output_in_auto_and_always() {
let _guard = TestStateGuard::no_color(ColorMode::Always);
assert_eq!("test".red().to_string(), "test");
assert_eq!("test".blue().italic().on_yellow().to_string(), "test");
assert_eq!("test".ansi256(208).to_string(), "test");
}
#[rstest]
#[case(
ColorMode::Never,
ColorDepthMode::Auto,
true,
TestEnv::default(),
ColorLevel::NoColor
)]
#[case(
ColorMode::Auto,
ColorDepthMode::NoColor,
true,
TestEnv::default(),
ColorLevel::NoColor
)]
#[case(ColorMode::Auto, ColorDepthMode::Auto, true, TestEnv::default().with("FORCE_COLOR", "0"), ColorLevel::NoColor)]
#[case(ColorMode::Auto, ColorDepthMode::Auto, true, TestEnv::default().with("FORCE_COLOR", "false"), ColorLevel::NoColor)]
#[case(ColorMode::Auto, ColorDepthMode::Ansi16, true, TestEnv::default().with("NO_COLOR", "1"), ColorLevel::NoColor)]
#[case(ColorMode::Auto, ColorDepthMode::Ansi16, true, TestEnv::default().with("NO_COLOR", ""), ColorLevel::NoColor)]
#[case(ColorMode::Auto, ColorDepthMode::Ansi256, true, TestEnv::default().with("NO_COLOR", "1"), ColorLevel::NoColor)]
#[case(ColorMode::Auto, ColorDepthMode::TrueColor, true, TestEnv::default().with("NO_COLOR", "1"), ColorLevel::NoColor)]
#[case(ColorMode::Never, ColorDepthMode::TrueColor, true, TestEnv::default().with("FORCE_COLOR", "3"), ColorLevel::NoColor)]
#[case(ColorMode::Always, ColorDepthMode::TrueColor, true, TestEnv::default().with("FORCE_COLOR", "0"), ColorLevel::NoColor)]
fn test_color_level_precedence(
#[case] color_mode: ColorMode,
#[case] depth_mode: ColorDepthMode,
#[case] is_terminal: bool,
#[case] env: TestEnv,
#[case] expected: ColorLevel,
) {
assert_eq!(
detect_color_level(is_terminal, color_mode, depth_mode, &env),
expected
);
}
#[rstest]
#[case("1", ColorLevel::Ansi16)]
#[case("2", ColorLevel::Ansi256)]
#[case("3", ColorLevel::TrueColor)]
#[case("truecolor", ColorLevel::TrueColor)]
#[case("ansi256", ColorLevel::Ansi256)]
#[case("ansi16", ColorLevel::Ansi16)]
#[case("on", ColorLevel::Ansi16)]
fn test_force_color_values(#[case] value: &str, #[case] expected: ColorLevel) {
let env = TestEnv::default().with("FORCE_COLOR", value);
assert_eq!(
detect_color_level(false, ColorMode::Auto, ColorDepthMode::Auto, &env),
expected
);
}
#[rstest]
#[case(ColorDepthMode::Ansi16, "3", ColorLevel::TrueColor)]
#[case(ColorDepthMode::Ansi256, "1", ColorLevel::Ansi16)]
#[case(ColorDepthMode::TrueColor, "2", ColorLevel::Ansi256)]
#[case(ColorDepthMode::TrueColor, "0", ColorLevel::NoColor)]
#[case(ColorDepthMode::TrueColor, "false", ColorLevel::NoColor)]
#[case(ColorDepthMode::TrueColor, "off", ColorLevel::NoColor)]
#[case(ColorDepthMode::TrueColor, "none", ColorLevel::NoColor)]
#[case(ColorDepthMode::TrueColor, "never", ColorLevel::NoColor)]
fn test_force_color_overrides_explicit_color_depth(
#[case] depth_mode: ColorDepthMode,
#[case] force_color: &str,
#[case] expected: ColorLevel,
) {
let env = TestEnv::default().with("FORCE_COLOR", force_color);
assert_eq!(
detect_color_level(true, ColorMode::Auto, depth_mode, &env),
expected
);
}
#[rstest]
#[case(ColorMode::Never, ColorDepthMode::TrueColor, "3")]
#[case(ColorMode::Auto, ColorDepthMode::NoColor, "3")]
fn test_hard_disable_modes_override_force_color(
#[case] color_mode: ColorMode,
#[case] depth_mode: ColorDepthMode,
#[case] force_color: &str,
) {
let env = TestEnv::default().with("FORCE_COLOR", force_color);
assert_eq!(
detect_color_level(true, color_mode, depth_mode, &env),
ColorLevel::NoColor
);
}
#[rstest]
#[case("", true, TestEnv::default().with("TERM", "xterm-256color"), ColorLevel::Ansi256)]
#[case("", false, TestEnv::default(), ColorLevel::NoColor)]
#[case("sometimes", false, TestEnv::default(), ColorLevel::Ansi16)]
fn test_force_color_fallback_values(
#[case] value: &str,
#[case] is_terminal: bool,
#[case] env: TestEnv,
#[case] expected: ColorLevel,
) {
let env = env.with("FORCE_COLOR", value);
assert_eq!(
detect_color_level(is_terminal, ColorMode::Auto, ColorDepthMode::Auto, &env),
expected
);
}
#[rstest]
#[case(TestEnv::default().with("NO_COLOR", "1"), true, ColorLevel::NoColor)]
#[case(TestEnv::default().with("NO_COLOR", ""), true, ColorLevel::NoColor)]
#[case(TestEnv::default().with("CLICOLOR", "0"), true, ColorLevel::NoColor)]
#[case(TestEnv::default().with("CLICOLOR_FORCE", "1"), false, ColorLevel::Ansi16)]
#[case(TestEnv::default().with("CLICOLOR", "0").with("CLICOLOR_FORCE", "1"), false, ColorLevel::Ansi16)]
#[case(TestEnv::default(), false, ColorLevel::NoColor)]
#[case(TestEnv::default(), true, ColorLevel::TrueColor)]
#[case(TestEnv::default().with("TERM", "dumb"), true, ColorLevel::NoColor)]
#[case(TestEnv::default().with("TERM", "DUMB"), true, ColorLevel::NoColor)]
#[case(TestEnv::default().with("COLORTERM", "truecolor"), true, ColorLevel::TrueColor)]
#[case(TestEnv::default().with("COLORTERM", "24bit"), true, ColorLevel::TrueColor)]
#[case(TestEnv::default().with("TERM", "xterm-256color"), true, ColorLevel::Ansi256)]
#[case(TestEnv::default().with("TERM", "xterm"), true, ColorLevel::Ansi16)]
#[case(TestEnv::default().with("WT_SESSION", "abc"), true, ColorLevel::TrueColor)]
#[case(TestEnv::default().with("ConEmuANSI", "ON"), true, ColorLevel::Ansi16)]
#[case(TestEnv::default().with("ANSICON", "1"), true, ColorLevel::Ansi16)]
#[case(TestEnv::default().with("CI", "1"), true, ColorLevel::Ansi16)]
fn test_auto_color_level_detection(
#[case] env: TestEnv,
#[case] is_terminal: bool,
#[case] expected: ColorLevel,
) {
assert_eq!(
detect_color_level(is_terminal, ColorMode::Auto, ColorDepthMode::Auto, &env),
expected
);
}
#[test]
fn test_clicolor_force_can_still_detect_higher_depth() {
let env = TestEnv::default()
.with("CLICOLOR_FORCE", "1")
.with("COLORTERM", "truecolor");
assert_eq!(
detect_color_level(false, ColorMode::Auto, ColorDepthMode::Auto, &env),
ColorLevel::TrueColor
);
}
#[test]
fn test_always_mode_uses_truecolor_without_terminal_env() {
assert_eq!(
detect_color_level(
false,
ColorMode::Always,
ColorDepthMode::Auto,
&TestEnv::default()
),
ColorLevel::TrueColor
);
}
#[rstest]
#[case(
ColorMode::Auto,
ColorDepthMode::Ansi256,
true,
TestEnv::default().with("CLICOLOR", "0"),
ColorLevel::NoColor
)]
#[case(
ColorMode::Always,
ColorDepthMode::Auto,
false,
TestEnv::default().with("CLICOLOR", "0"),
ColorLevel::NoColor
)]
#[case(
ColorMode::Auto,
ColorDepthMode::Ansi256,
true,
TestEnv::default().with("CLICOLOR", "0").with("FORCE_COLOR", "2"),
ColorLevel::Ansi256
)]
#[case(
ColorMode::Auto,
ColorDepthMode::Auto,
false,
TestEnv::default().with("CLICOLOR", "0").with("CLICOLOR_FORCE", "1"),
ColorLevel::Ansi16
)]
#[case(
ColorMode::Auto,
ColorDepthMode::Ansi256,
false,
TestEnv::default().with("CLICOLOR", "0").with("CLICOLOR_FORCE", "1"),
ColorLevel::Ansi256
)]
#[case(
ColorMode::Auto,
ColorDepthMode::Ansi256,
false,
TestEnv::default(),
ColorLevel::NoColor
)]
#[case(
ColorMode::Always,
ColorDepthMode::Ansi256,
false,
TestEnv::default(),
ColorLevel::Ansi256
)]
#[case(
ColorMode::Always,
ColorDepthMode::Ansi16,
false,
TestEnv::default(),
ColorLevel::Ansi16
)]
#[case(
ColorMode::Auto,
ColorDepthMode::Ansi256,
false,
TestEnv::default().with("FORCE_COLOR", "2"),
ColorLevel::Ansi256
)]
#[case(
ColorMode::Auto,
ColorDepthMode::Ansi256,
false,
TestEnv::default().with("CLICOLOR_FORCE", "1"),
ColorLevel::Ansi256
)]
fn test_color_depth_mode_selects_depth_after_output_is_enabled(
#[case] color_mode: ColorMode,
#[case] depth_mode: ColorDepthMode,
#[case] is_terminal: bool,
#[case] env: TestEnv,
#[case] expected: ColorLevel,
) {
assert_eq!(
detect_color_level(is_terminal, color_mode, depth_mode, &env),
expected
);
}
#[rstest]
#[case(ColorMode::Auto, ColorDepthMode::Auto, true, ColorLevel::NoColor)]
#[case(ColorMode::Auto, ColorDepthMode::Ansi256, true, ColorLevel::Ansi256)]
#[case(ColorMode::Auto, ColorDepthMode::Ansi256, false, ColorLevel::NoColor)]
#[case(
ColorMode::Always,
ColorDepthMode::TrueColor,
false,
ColorLevel::TrueColor
)]
fn test_term_dumb_is_capability_hint_not_hard_disable(
#[case] color_mode: ColorMode,
#[case] depth_mode: ColorDepthMode,
#[case] is_terminal: bool,
#[case] expected: ColorLevel,
) {
let env = TestEnv::default().with("TERM", "dumb");
assert_eq!(
detect_color_level(is_terminal, color_mode, depth_mode, &env),
expected
);
}
#[test]
fn test_color_depth_mode_accessors() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
ColorizeConfig::set_color_depth_mode(ColorDepthMode::Ansi256);
assert_eq!(ColorizeConfig::color_depth_mode(), ColorDepthMode::Ansi256);
}
#[test]
fn test_terminal_capabilities_for_known_target() {
let _guard =
TestStateGuard::with_depth(ColorMode::Auto, ColorDepthMode::Auto, None, Some(false));
let target = RenderTarget::Capabilities(TerminalCapabilities {
is_terminal: false,
color_level: ColorLevel::Ansi256,
});
assert_eq!(ColorizeConfig::color_level(target), ColorLevel::Ansi256);
assert_eq!(
"test".rgb(255, 128, 0).render(target),
"\x1b[38;5;208mtest\x1b[0m"
);
}
#[test]
fn test_terminal_capabilities_accessor_returns_known_target_capabilities() {
let _guard =
TestStateGuard::with_depth(ColorMode::Auto, ColorDepthMode::Auto, None, Some(false));
let expected = TerminalCapabilities {
is_terminal: true,
color_level: ColorLevel::Ansi256,
};
assert_eq!(
ColorizeConfig::terminal_capabilities(RenderTarget::Capabilities(expected)),
expected
);
}
#[test]
fn test_terminal_capabilities_respect_color_mode_never() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Never);
let target = RenderTarget::Capabilities(TerminalCapabilities {
is_terminal: true,
color_level: ColorLevel::TrueColor,
});
assert_eq!(ColorizeConfig::color_level(target), ColorLevel::NoColor);
assert_eq!("test".red().render(target), "test");
}
#[test]
fn test_terminal_capabilities_respect_no_color_env() {
let _guard = TestStateGuard::no_color(ColorMode::Always);
let target = RenderTarget::Capabilities(TerminalCapabilities {
is_terminal: true,
color_level: ColorLevel::TrueColor,
});
assert_eq!(ColorizeConfig::color_level(target), ColorLevel::NoColor);
assert_eq!("test".red().render(target), "test");
}
#[test]
fn test_terminal_capabilities_respect_color_depth_no_color() {
let _guard =
TestStateGuard::with_depth(ColorMode::Always, ColorDepthMode::NoColor, None, Some(true));
let target = RenderTarget::Capabilities(TerminalCapabilities {
is_terminal: true,
color_level: ColorLevel::TrueColor,
});
assert_eq!(ColorizeConfig::color_level(target), ColorLevel::NoColor);
assert_eq!("test".red().render(target), "test");
}
#[rstest]
#[case("FORCE_COLOR", "0", ColorLevel::TrueColor, ColorLevel::TrueColor)]
#[case("CLICOLOR", "0", ColorLevel::TrueColor, ColorLevel::TrueColor)]
#[case("FORCE_COLOR", "3", ColorLevel::Ansi16, ColorLevel::Ansi16)]
fn test_terminal_capabilities_ignore_normal_target_env_overrides(
#[case] key: &str,
#[case] value: &str,
#[case] supplied: ColorLevel,
#[case] expected: ColorLevel,
) {
let _guard =
TestStateGuard::with_depth(ColorMode::Auto, ColorDepthMode::Auto, None, Some(false));
env::set_var(key, value);
let target = RenderTarget::Capabilities(TerminalCapabilities {
is_terminal: true,
color_level: supplied,
});
assert_eq!(ColorizeConfig::color_level(target), expected);
}
#[test]
fn test_terminal_true_uses_env_depth_detection() {
let _guard =
TestStateGuard::with_depth(ColorMode::Auto, ColorDepthMode::Auto, None, Some(false));
env::set_var("COLORTERM", "truecolor");
assert_eq!(
ColorizeConfig::color_level(RenderTarget::Terminal(true)),
ColorLevel::TrueColor
);
}
#[rstest]
#[case(ColorLevel::TrueColor, "\x1b[38;2;255;128;0mtest\x1b[0m")]
#[case(ColorLevel::Ansi256, "\x1b[38;5;208mtest\x1b[0m")]
#[case(ColorLevel::Ansi16, "\x1b[33mtest\x1b[0m")]
#[case(ColorLevel::NoColor, "test")]
fn test_rgb_degrades_by_color_level(#[case] level: ColorLevel, #[case] expected: &str) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let target = RenderTarget::Capabilities(TerminalCapabilities {
is_terminal: true,
color_level: level,
});
assert_eq!("test".rgb(255, 128, 0).render(target), expected);
}
#[rstest]
#[case(ColorLevel::TrueColor, "\x1b[38;5;208mtest\x1b[0m")]
#[case(ColorLevel::Ansi256, "\x1b[38;5;208mtest\x1b[0m")]
#[case(ColorLevel::Ansi16, "\x1b[93mtest\x1b[0m")]
#[case(ColorLevel::NoColor, "test")]
fn test_ansi256_degrades_by_color_level(#[case] level: ColorLevel, #[case] expected: &str) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let target = RenderTarget::Capabilities(TerminalCapabilities {
is_terminal: true,
color_level: level,
});
assert_eq!("test".ansi256(208).render(target), expected);
}
#[rstest]
#[case(ColorLevel::TrueColor, "\x1b[48;2;255;128;0mtest\x1b[0m")]
#[case(ColorLevel::Ansi256, "\x1b[48;5;208mtest\x1b[0m")]
#[case(ColorLevel::Ansi16, "\x1b[43mtest\x1b[0m")]
#[case(ColorLevel::NoColor, "test")]
fn test_rgb_background_degrades_by_color_level(#[case] level: ColorLevel, #[case] expected: &str) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let target = RenderTarget::Capabilities(TerminalCapabilities {
is_terminal: true,
color_level: level,
});
assert_eq!("test".on_rgb(255, 128, 0).render(target), expected);
}
#[rstest]
#[case(ColorLevel::TrueColor, "\x1b[48;5;208mtest\x1b[0m")]
#[case(ColorLevel::Ansi256, "\x1b[48;5;208mtest\x1b[0m")]
#[case(ColorLevel::Ansi16, "\x1b[103mtest\x1b[0m")]
#[case(ColorLevel::NoColor, "test")]
fn test_ansi256_background_degrades_by_color_level(
#[case] level: ColorLevel,
#[case] expected: &str,
) {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let target = RenderTarget::Capabilities(TerminalCapabilities {
is_terminal: true,
color_level: level,
});
assert_eq!("test".on_ansi256(208).render(target), expected);
}
#[test]
fn test_color_specs_return_none_without_color_support() {
assert_eq!(
ColorSpec::Named(NamedColor::Red).foreground_code(ColorLevel::NoColor),
None
);
assert_eq!(
ColorSpec::Named(NamedColor::Red).background_code(ColorLevel::NoColor),
None
);
}
#[test]
fn test_styled_text_inherent_ansi256_aliases() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!(
StyledText::plain("test").color256(208).to_string(),
"\x1b[38;5;208mtest\x1b[0m"
);
assert_eq!(
StyledText::plain("test").on_color256(236).to_string(),
"\x1b[48;5;236mtest\x1b[0m"
);
}
#[test]
fn test_styled_text_inherent_bright_black_methods() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!(
StyledText::plain("test").bright_black().to_string(),
"\x1b[90mtest\x1b[0m"
);
assert_eq!(
StyledText::plain("test").on_bright_black().to_string(),
"\x1b[100mtest\x1b[0m"
);
}
#[test]
fn test_named_colors_remain_named_for_color_levels() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
for level in [
ColorLevel::Ansi16,
ColorLevel::Ansi256,
ColorLevel::TrueColor,
] {
let target = RenderTarget::Capabilities(TerminalCapabilities {
is_terminal: true,
color_level: level,
});
assert_eq!("test".red().render(target), "\x1b[31mtest\x1b[0m");
}
}
#[test]
fn test_no_color_suppresses_styles_and_raw_codes() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let target = RenderTarget::Capabilities(TerminalCapabilities {
is_terminal: false,
color_level: ColorLevel::NoColor,
});
assert_eq!("test".bold().red().colorize("4").render(target), "test");
}
#[rstest]
#[case(0, (0, 0, 0))]
#[case(15, (255, 255, 255))]
#[case(16, (0, 0, 0))]
#[case(21, (0, 0, 255))]
#[case(208, (255, 135, 0))]
#[case(232, (8, 8, 8))]
#[case(255, (238, 238, 238))]
fn test_ansi256_palette_rgb_values(#[case] index: u8, #[case] expected: (u8, u8, u8)) {
assert_eq!(ansi256_to_rgb(index), expected);
}
#[rstest]
#[case((0, 0, 0), 0)]
#[case((255, 128, 0), 208)]
#[case((0, 0, 255), 12)]
#[case((128, 128, 128), 8)]
#[case((238, 238, 238), 255)]
fn test_rgb_to_ansi256_known_values(#[case] rgb: (u8, u8, u8), #[case] expected: u8) {
assert_eq!(rgb_to_ansi256(rgb.0, rgb.1, rgb.2), expected);
}
#[rstest]
#[case((255, 0, 0), NamedColor::BrightRed)]
#[case((0, 0, 128), NamedColor::Blue)]
#[case((128, 128, 128), NamedColor::BrightBlack)]
#[case((255, 255, 255), NamedColor::BrightWhite)]
#[case((255, 128, 0), NamedColor::Yellow)]
fn test_rgb_to_named_color_known_values(#[case] rgb: (u8, u8, u8), #[case] expected: NamedColor) {
assert_eq!(rgb_to_named_color(rgb.0, rgb.1, rgb.2), expected);
}
#[test]
fn test_rgb_to_named_color_ties_use_palette_order() {
assert_eq!(rgb_to_named_color(64, 64, 64), NamedColor::Black);
}
#[rstest]
#[case(8, NamedColor::BrightBlack)]
#[case(12, NamedColor::BrightBlue)]
#[case(208, NamedColor::BrightYellow)]
#[case(236, NamedColor::Black)]
fn test_ansi256_to_named_color_known_values(#[case] index: u8, #[case] expected: NamedColor) {
assert_eq!(ansi256_to_named_color(index), expected);
}
#[test]
#[allow(deprecated)]
fn test_set_terminal_check_compatibility_mapping() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Never);
ColorizeConfig::set_terminal_check(false);
assert_eq!(ColorizeConfig::color_mode(), ColorMode::Always);
ColorizeConfig::set_terminal_check(true);
assert_eq!(ColorizeConfig::color_mode(), ColorMode::Auto);
}
#[test]
fn test_raw_colorize_codes_still_render() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
assert_eq!("test".colorize("31;1").to_string(), "\x1b[31;1mtest\x1b[0m");
assert_eq!(
"test".colorize("31").green().to_string(),
"\x1b[31;32mtest\x1b[0m"
);
}
#[rstest]
#[case(NamedColor::BrightBlack, "100")]
#[case(NamedColor::BrightRed, "101")]
#[case(NamedColor::BrightGreen, "102")]
#[case(NamedColor::BrightYellow, "103")]
#[case(NamedColor::BrightBlue, "104")]
#[case(NamedColor::BrightMagenta, "105")]
#[case(NamedColor::BrightCyan, "106")]
#[case(NamedColor::BrightWhite, "107")]
fn test_bright_background_color_codes(#[case] color: NamedColor, #[case] expected: &str) {
assert_eq!(
ColorSpec::Named(color).background_code(ColorLevel::Ansi16),
Some(expected.to_string())
);
}
#[test]
fn test_ansi256_color_codes() {
assert_eq!(
ColorSpec::Ansi256(208).foreground_code(ColorLevel::Ansi256),
Some("38;5;208".to_string())
);
assert_eq!(
ColorSpec::Ansi256(236).background_code(ColorLevel::Ansi256),
Some("48;5;236".to_string())
);
}
#[test]
fn test_from_styled_text_to_string() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let rendered: String = "test".red().bold().into();
assert_eq!(rendered, "\x1b[1;31mtest\x1b[0m");
}
#[test]
#[should_panic(expected = "Invalid ANSI color sequence")]
fn test_assert_rgb_approx_eq_invalid_sequence() {
assert_rgb_approx_eq("invalid", "also invalid");
}
#[test]
#[should_panic(expected = "RGB values differ by more than 1: (255, 0, 0) vs (252, 0, 0)")]
fn test_assert_rgb_approx_eq_large_diff() {
let _guard = TestStateGuard::colors_enabled(ColorMode::Always);
let color1 = "test".rgb(255, 0, 0).to_string();
let color2 = "test".rgb(252, 0, 0).to_string();
assert_rgb_approx_eq(&color1, &color2);
}