use crate::common::{TestRepo, repo, wt_command};
use ansi_str::AnsiStr;
use rstest::rstest;
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone)]
struct ColumnPositions {
age: Option<usize>,
cmts: Option<usize>,
cmt_diff: Option<usize>,
wt_diff: Option<usize>,
remote: Option<usize>,
commit: Option<usize>,
message: Option<usize>,
state: Option<usize>,
path: Option<usize>,
}
impl ColumnPositions {
fn from_header(header: &str) -> Self {
let mut positions = ColumnPositions {
age: None,
cmts: None,
cmt_diff: None,
wt_diff: None,
remote: None,
commit: None,
message: None,
state: None,
path: None,
};
let byte_to_display_pos = |byte_pos: usize| -> usize { header[..byte_pos].width() };
if let Some(byte_pos) = header.find("Age") {
positions.age = Some(byte_to_display_pos(byte_pos));
}
if let Some(byte_pos) = header.find("Main-Cmt") {
positions.cmts = Some(byte_to_display_pos(byte_pos));
}
if let Some(byte_pos) = header.find("main↕") {
positions.cmt_diff = Some(byte_to_display_pos(byte_pos));
}
if let Some(byte_pos) = header.find("HEAD±") {
positions.wt_diff = Some(byte_to_display_pos(byte_pos));
}
if let Some(byte_pos) = header.find("Remote") {
positions.remote = Some(byte_to_display_pos(byte_pos));
}
if let Some(byte_pos) = header.find("Commit") {
positions.commit = Some(byte_to_display_pos(byte_pos));
}
if let Some(byte_pos) = header.find("Message") {
positions.message = Some(byte_to_display_pos(byte_pos));
}
if let Some(byte_pos) = header.find("State") {
positions.state = Some(byte_to_display_pos(byte_pos));
}
if let Some(byte_pos) = header.find("Path") {
positions.path = Some(byte_to_display_pos(byte_pos));
}
positions
}
}
#[derive(Debug, Clone)]
struct ColumnBoundary {
start: usize, end: usize, content: String,
}
fn find_column_boundaries(line: &str) -> Vec<ColumnBoundary> {
let mut boundaries = Vec::new();
let chars: Vec<char> = line.chars().collect();
let mut char_idx = 0;
let mut display_pos = 0;
while char_idx < chars.len() {
while char_idx < chars.len() && chars[char_idx] == ' ' {
char_idx += 1;
display_pos += 1;
}
if char_idx >= chars.len() {
break;
}
let start_display_pos = display_pos;
let mut content = String::new();
while char_idx < chars.len() {
if chars[char_idx] == ' ' {
let mut space_count = 0;
let mut j = char_idx;
while j < chars.len() && chars[j] == ' ' {
space_count += 1;
j += 1;
}
if space_count >= 2 {
break;
} else {
content.push(chars[char_idx]);
char_idx += 1;
display_pos += 1;
}
} else {
let ch = chars[char_idx];
content.push(ch);
char_idx += 1;
display_pos += UnicodeWidthStr::width(ch.to_string().as_str());
}
}
boundaries.push(ColumnBoundary {
start: start_display_pos,
end: display_pos,
content: content.trim_end().to_string(),
});
}
boundaries
}
fn verify_table_alignment(output: &str) -> Result<(), String> {
let lines: Vec<&str> = output.lines().collect();
if lines.is_empty() {
return Err("No output to verify".to_string());
}
let stripped_lines: Vec<String> = lines.iter().map(|l| l.ansi_strip().into_owned()).collect();
if stripped_lines.is_empty() {
return Err("No lines after stripping ANSI codes".to_string());
}
let header = &stripped_lines[0];
let header_positions = ColumnPositions::from_header(header);
println!("\n=== Table Alignment Verification ===");
println!("Header: {}", header);
println!("Header length: {}", header.width());
println!("Header positions: {:?}", header_positions);
println!();
let mut all_row_boundaries: Vec<Vec<ColumnBoundary>> = Vec::new();
let mut errors = Vec::new();
for (idx, row) in stripped_lines.iter().skip(1).enumerate() {
if row.trim().is_empty() {
continue;
}
let row_num = idx + 1;
println!("Row {}: {}", row_num, row);
println!(" Length: {}", row.width());
let boundaries = find_column_boundaries(row);
println!(" Boundaries: {:?}", boundaries);
all_row_boundaries.push(boundaries.clone());
let positions = [
("Branch", Some(0usize)), ("Age", header_positions.age),
("Main-Cmt", header_positions.cmts),
("Main-Δ", header_positions.cmt_diff),
("Dirty", header_positions.wt_diff),
("Remote", header_positions.remote),
("Commit", header_positions.commit),
("Message", header_positions.message),
("State", header_positions.state),
("Path", header_positions.path),
];
for (col_name, maybe_pos) in positions.iter() {
if let Some(expected_pos) = maybe_pos {
let actual_content_pos = boundaries
.iter()
.find(|b| b.start <= *expected_pos && b.end > *expected_pos)
.map(|b| b.start);
if *col_name == "Path" {
if let Some(path_boundary) = boundaries.iter().find(|b| {
b.start <= *expected_pos
&& b.end > *expected_pos
&& b.content.starts_with("./")
}) && path_boundary.start != *expected_pos
{
errors.push(format!(
"Row {}: Path column content starts at display position {} but header says it should be at {}. Misalignment: {} characters.\n Row text: '{}'\n Path content: '{}'",
row_num,
path_boundary.start,
expected_pos,
path_boundary.start.abs_diff(*expected_pos),
row,
path_boundary.content
));
}
}
if let Some(actual_start) = actual_content_pos
&& actual_start != *expected_pos
{
let content_at_pos = boundaries
.iter()
.find(|b| b.start == actual_start)
.map(|b| &b.content);
if let Some(content) = content_at_pos
&& !content.is_empty()
&& content.trim() != ""
{
println!(
" ⚠️ Column '{}': content starts at {} instead of {} (content: '{}')",
col_name, actual_start, expected_pos, content
);
}
}
}
}
println!();
}
if all_row_boundaries.len() > 1 {
println!("=== Cross-row alignment check ===");
let first_row_boundary_starts: Vec<usize> =
all_row_boundaries[0].iter().map(|b| b.start).collect();
for boundaries in all_row_boundaries.iter().skip(1) {
let this_row_starts: Vec<usize> = boundaries.iter().map(|b| b.start).collect();
for &expected_start in first_row_boundary_starts.iter() {
let matching_boundary = this_row_starts.iter().find(|&&s| s == expected_start);
if matching_boundary.is_none() {
continue;
}
}
}
}
if !errors.is_empty() {
Err(format!(
"\n=== ALIGNMENT ERRORS ===\n{}\n",
errors.join("\n\n")
))
} else {
println!("✓ All rows properly aligned");
Ok(())
}
}
#[rstest]
fn test_alignment_verification_with_varying_content(mut repo: TestRepo) {
repo.add_worktree("main-feature");
repo.add_worktree("short");
repo.add_worktree("very-long");
let feature_path = repo.worktrees.get("main-feature").unwrap();
for i in 0..10 {
std::fs::write(feature_path.join(format!("file{}.txt", i)), "content").unwrap();
}
let short_path = repo.worktrees.get("short").unwrap();
std::fs::write(short_path.join("single.txt"), "x").unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
let output = cmd.output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
println!("=== RAW OUTPUT ===");
println!("{}", stdout);
println!("==================");
match verify_table_alignment(&stdout) {
Ok(()) => println!("\n✓ Alignment verification passed"),
Err(e) => panic!("\n{}", e),
}
}
#[rstest]
fn test_alignment_with_unicode_content(mut repo: TestRepo) {
repo.commit("Initial commit with émoji 🎉");
repo.add_worktree("cafe");
repo.add_worktree("naive");
repo.add_worktree("resume");
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
let output = cmd.output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
println!("=== RAW OUTPUT WITH UNICODE ===");
println!("{}", stdout);
println!("================================");
match verify_table_alignment(&stdout) {
Ok(()) => println!("\n✓ Unicode alignment verification passed"),
Err(e) => panic!("\n{}", e),
}
}
#[rstest]
fn test_alignment_with_sparse_columns(mut repo: TestRepo) {
repo.add_worktree("no-changes-1");
repo.add_worktree("with-changes");
let changes_path = repo.worktrees.get("with-changes").unwrap();
for i in 0..100 {
std::fs::write(changes_path.join(format!("file{}.txt", i)), "content").unwrap();
}
repo.add_worktree("no-changes-2");
repo.add_worktree("small-change");
let small_path = repo.worktrees.get("small-change").unwrap();
std::fs::write(small_path.join("one.txt"), "x").unwrap();
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
let output = cmd.output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
println!("=== RAW OUTPUT WITH SPARSE COLUMNS ===");
println!("{}", stdout);
println!("=======================================");
match verify_table_alignment(&stdout) {
Ok(()) => println!("\n✓ Sparse column alignment verification passed"),
Err(e) => panic!("\n{}", e),
}
}
#[rstest]
fn test_alignment_real_world_scenario(mut repo: TestRepo) {
repo.add_worktree("feature-tiny");
let tiny_path = repo.worktrees.get("feature-tiny").unwrap();
std::fs::write(tiny_path.join("file.txt"), "x").unwrap();
repo.add_worktree("feature-small");
let small_path = repo.worktrees.get("feature-small").unwrap();
for i in 0..10 {
std::fs::write(small_path.join(format!("file{}.txt", i)), "content").unwrap();
}
repo.add_worktree("feature-medium");
let medium_path = repo.worktrees.get("feature-medium").unwrap();
for i in 0..100 {
std::fs::write(medium_path.join(format!("file{}.txt", i)), "content").unwrap();
}
repo.add_worktree("feature-large");
let large_path = repo.worktrees.get("feature-large").unwrap();
for i in 0..1000 {
std::fs::write(large_path.join(format!("file{}.txt", i)), "content").unwrap();
}
repo.add_worktree("no-changes");
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list").current_dir(repo.root_path());
let output = cmd.output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
println!("=== RAW OUTPUT: Real World Scenario ===");
println!("{}", stdout);
println!("========================================");
match verify_table_alignment(&stdout) {
Ok(()) => println!("\n✓ Real world scenario alignment verification passed"),
Err(e) => panic!("\n{}", e),
}
}
#[rstest]
fn test_alignment_at_different_terminal_widths(mut repo: TestRepo) {
repo.add_worktree("feature-a");
repo.add_worktree("feature-b");
let path_a = repo.worktrees.get("feature-a").unwrap();
std::fs::write(path_a.join("file.txt"), "content").unwrap();
for width in [80, 120, 150, 200] {
println!("\n### Testing at width {} ###", width);
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("list")
.current_dir(repo.root_path())
.env("COLUMNS", width.to_string());
let output = cmd.output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
println!("{}", stdout);
match verify_table_alignment(&stdout) {
Ok(()) => println!("✓ Width {} aligned correctly", width),
Err(e) => panic!("\nWidth {} failed:\n{}", width, e),
}
}
}