use super::*;
use sley_grep::{Regex, RegexMode};
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' {
while chars.next().is_some_and(|ch| ch != 'm') {}
continue;
}
out.push(c);
}
out
}
#[test]
fn parse_color_red() {
assert_eq!(parse_color_value("red"), Some("\x1b[31m".to_string()));
}
#[test]
fn parse_color_bold_green() {
assert_eq!(parse_color_value("bold green"), Some("\x1b[1;32m".to_string()));
}
#[test]
fn parse_color_reset() {
assert_eq!(parse_color_value("reset"), Some("\x1b[m".to_string()));
}
#[test]
fn parse_color_unknown_word_returns_none() {
assert_eq!(parse_color_value("notacolor"), None);
}
#[test]
fn parse_color_empty_returns_empty_string() {
assert_eq!(parse_color_value(""), Some(String::new()));
}
#[test]
fn parse_color_reverse_attr() {
assert_eq!(parse_color_value("reverse"), Some("\x1b[7m".to_string()));
}
#[test]
fn parse_color_dim() {
assert_eq!(parse_color_value("dim"), Some("\x1b[2m".to_string()));
}
#[test]
fn parse_color_blue_background() {
assert_eq!(parse_color_value("red blue"), Some("\x1b[31;44m".to_string()));
}
#[test]
fn push_colored_line_wraps_body() {
let mut out = Vec::new();
push_colored_line(&mut out, "\x1b[31m", "\x1b[m", b"hello\n");
assert_eq!(strip_ansi(&String::from_utf8_lossy(&out)), "hello\n");
}
#[test]
fn push_colored_line_empty_body_emits_newline_only() {
let mut out = Vec::new();
push_colored_line(&mut out, "\x1b[31m", "\x1b[m", b"\n");
assert_eq!(out, b"\n");
}
#[test]
fn push_colored_line_no_color_passes_through() {
let mut out = Vec::new();
push_colored_line(&mut out, "", "", b"x\n");
assert_eq!(out, b"x\n");
}
#[test]
fn push_colored_line_without_trailing_newline() {
let mut out = Vec::new();
push_colored_line(&mut out, "\x1b[32m", "\x1b[m", b"no newline");
assert_eq!(strip_ansi(&String::from_utf8_lossy(&out)), "no newline");
}
#[test]
fn diff_colors_enabled_defaults() {
let colors = DiffColors::enabled(None);
assert!(colors.meta.contains("\x1b["));
assert!(colors.old.contains("\x1b[31m"));
assert!(colors.new.contains("\x1b[32m"));
assert_eq!(colors.reset, "\x1b[m");
}
#[test]
fn render_colors_maps_palette() {
let colors = DiffColors::enabled(None);
let mapped = render_colors(&colors);
assert_eq!(mapped.old, colors.old.as_str());
assert_eq!(mapped.new, colors.new.as_str());
}
#[test]
fn default_funcname_heading_accepts_underscore() {
assert_eq!(
default_funcname_heading(b"fn main() {\n"),
Some(b"fn main() {".to_vec())
);
}
#[test]
fn default_funcname_heading_rejects_digit_lead() {
assert_eq!(default_funcname_heading(b"1bad\n"), None);
}
#[test]
fn default_funcname_heading_trims_trailing_space() {
assert_eq!(
default_funcname_heading(b"hello \n"),
Some(b"hello".to_vec())
);
}
#[test]
fn default_funcname_heading_dollar_prefix() {
assert_eq!(
default_funcname_heading(b"$injector\n"),
Some(b"$injector".to_vec())
);
}
#[test]
fn compiled_funcname_matches_simple_pattern() {
let compiled = CompiledFuncname::compile(b"^fn.*", true, false).expect("compile");
assert_eq!(
compiled.match_line(b"fn foo() {\n"),
Some(b"fn foo() {".to_vec())
);
}
#[test]
fn compiled_funcname_negated_pattern_rejects() {
let compiled = CompiledFuncname::compile(b"!^//\n^fn.*", true, false).expect("compile");
assert_eq!(compiled.match_line(b"// comment\n"), None);
assert_eq!(
compiled.match_line(b"fn foo()\n"),
Some(b"fn foo()".to_vec())
);
}
#[test]
fn heading_classifier_uses_default_without_driver() {
let mut classify = heading_classifier(None);
assert_eq!(classify(b"struct S {\n"), Some(b"struct S {".to_vec()));
}
#[test]
fn word_diff_buffers_plain_mode_brackets() {
let colors = DiffColors::enabled(None);
let regex = Regex::compile_bytes(b"[a-zA-Z]+", RegexMode::Ere, false, false).ok();
let config = WordDiffConfig {
mode: WordDiffMode::Plain,
regex: regex.as_ref(),
colors: &colors,
};
let mut buffers = WordDiffBuffers::new();
buffers.push_minus(b"hello ");
buffers.push_plus(b"hella ");
let mut out = Vec::new();
buffers.flush(&mut out, &config);
let rendered = String::from_utf8_lossy(&out);
assert!(rendered.contains("[-"));
assert!(rendered.contains("-]"));
assert!(rendered.contains("{+"));
assert!(rendered.contains("+}"));
}
#[test]
fn word_diff_buffers_color_mode_no_prefix() {
let colors = DiffColors::enabled(None);
let config = WordDiffConfig {
mode: WordDiffMode::Color,
regex: None,
colors: &colors,
};
let mut buffers = WordDiffBuffers::new();
buffers.push_minus(b"foo");
buffers.push_plus(b"bar");
let mut out = Vec::new();
buffers.flush(&mut out, &config);
assert!(!String::from_utf8_lossy(&out).contains("{+"));
}
#[test]
fn word_diff_emit_context_line_porcelain() {
let colors = DiffColors::enabled(None);
let config = WordDiffConfig {
mode: WordDiffMode::Porcelain,
regex: None,
colors: &colors,
};
let mut out = Vec::new();
WordDiffBuffers::emit_context_line(&mut out, &config, b"ctx\n");
assert!(String::from_utf8_lossy(&out).ends_with("~\n"));
}
#[test]
fn word_diff_adapter_implements_flush() {
use crate::render::HunkWordDiff;
let colors = DiffColors::enabled(None);
let config = WordDiffConfig {
mode: WordDiffMode::Plain,
regex: None,
colors: &colors,
};
let mut adapter = WordDiffAdapter::new(&config);
HunkWordDiff::push_minus(&mut adapter, b"a");
HunkWordDiff::push_plus(&mut adapter, b"b");
let mut out = Vec::new();
HunkWordDiff::flush(&mut adapter, &mut out);
assert!(!out.is_empty());
}
#[test]
fn word_diff_only_deletion() {
let colors = DiffColors::enabled(None);
let config = WordDiffConfig {
mode: WordDiffMode::Plain,
regex: None,
colors: &colors,
};
let mut buffers = WordDiffBuffers::new();
buffers.push_minus(b"gone");
let mut out = Vec::new();
buffers.flush(&mut out, &config);
assert!(String::from_utf8_lossy(&out).contains("[-gone-]"));
}
#[test]
fn word_diff_empty_flush_is_noop() {
let colors = DiffColors::enabled(None);
let config = WordDiffConfig {
mode: WordDiffMode::Color,
regex: None,
colors: &colors,
};
let mut buffers = WordDiffBuffers::new();
let mut out = vec![b'x'];
buffers.flush(&mut out, &config);
assert_eq!(out, vec![b'x']);
}
#[test]
fn word_diff_whitespace_tokenization() {
let colors = DiffColors::enabled(None);
let config = WordDiffConfig {
mode: WordDiffMode::Plain,
regex: None,
colors: &colors,
};
let mut buffers = WordDiffBuffers::new();
buffers.push_minus(b"aa bb");
buffers.push_plus(b"aa cc");
let mut out = Vec::new();
buffers.flush(&mut out, &config);
let s = String::from_utf8_lossy(&out);
assert!(s.contains("bb"));
assert!(s.contains("cc"));
}
#[test]
fn parse_color_bold_red_green_bg() {
let parsed = parse_color_value("bold red green").expect("parse");
assert!(parsed.contains("1"));
assert!(parsed.contains("31"));
assert!(parsed.contains("42"));
}
#[test]
fn parse_color_normal_then_cyan() {
let parsed = parse_color_value("cyan").expect("parse");
assert!(parsed.contains("36"));
}
#[test]
fn compiled_funcname_capture_group() {
let compiled = CompiledFuncname::compile(b"^[[:space:]]*(func.*)", true, false).expect("compile");
assert_eq!(
compiled.match_line(b" func foo()\n"),
Some(b"func foo()".to_vec())
);
}
#[test]
fn default_funcname_empty_line_none() {
assert_eq!(default_funcname_heading(b"\n"), None);
}
#[test]
fn push_colored_line_multibyte_safe() {
let mut out = Vec::new();
push_colored_line(&mut out, "\x1b[36m", "\x1b[m", "café\n".as_bytes());
assert!(out.ends_with(b"\n"));
}