458 lines · 15.7 KB · Rust
RawDownload
use super::*;use crate::util::git::{    BranchEntry, parse_range, parse_unified_diff_header, sort_branches_main_first,}; #[test]fn test_parse_range_with_comma() {    let (start, count) =        parse_range("10,5").expect("parse_range should succeed for range with count");    assert_eq!(start, 10);    assert_eq!(count, 5);} #[test]fn test_parse_range_without_comma() {    let (start, count) =        parse_range("10").expect("parse_range should succeed for range without count");    assert_eq!(start, 10);    assert_eq!(count, 1);} #[test]fn test_parse_unified_diff_header_basic() {    let header = "@@ -10,5 +12,7 @@";    let parsed = parse_unified_diff_header(header)        .expect("parse_unified_diff_header should succeed for basic header");    assert_eq!(parsed.old_start_line, 10);    assert_eq!(parsed.old_line_count, 5);    assert_eq!(parsed.new_start_line, 12);    assert_eq!(parsed.new_line_count, 7);} #[test]fn test_parse_unified_diff_header_with_context() {    let header = "@@ -4978,33 +4978,43 @@ impl TerminalView {";    let parsed = parse_unified_diff_header(header)        .expect("parse_unified_diff_header should succeed for header with context");    assert_eq!(parsed.old_start_line, 4978);    assert_eq!(parsed.old_line_count, 33);    assert_eq!(parsed.new_start_line, 4978);    assert_eq!(parsed.new_line_count, 43);} #[test]fn test_parse_unified_diff_header_single_line() {    let header = "@@ -10 +12,3 @@";    let parsed = parse_unified_diff_header(header)        .expect("parse_unified_diff_header should succeed for single line header");    assert_eq!(parsed.old_start_line, 10);    assert_eq!(parsed.old_line_count, 1);    assert_eq!(parsed.new_start_line, 12);    assert_eq!(parsed.new_line_count, 3);} #[test]fn test_sort_branches_main_first_empty() {    let branches: Vec<BranchEntry> = vec![];    let result: Vec<_> = sort_branches_main_first(&branches).collect();    assert!(result.is_empty());} #[test]fn test_sort_branches_main_first_no_main() {    let branches = vec![        BranchEntry {            name: "feature-a".to_string(),            is_main: false,        },        BranchEntry {            name: "feature-b".to_string(),            is_main: false,        },        BranchEntry {            name: "feature-c".to_string(),            is_main: false,        },    ];    let result: Vec<_> = sort_branches_main_first(&branches).collect();    // No main branches — order should be unchanged.    assert_eq!(result, branches.iter().collect::<Vec<_>>());} #[test]fn test_sort_branches_main_first_promotes_main() {    let branches = vec![        BranchEntry {            name: "feature-a".to_string(),            is_main: false,        },        BranchEntry {            name: "main".to_string(),            is_main: true,        },        BranchEntry {            name: "feature-b".to_string(),            is_main: false,        },    ];    let result: Vec<_> = sort_branches_main_first(&branches)        .map(|entry| entry.name.as_str())        .collect();    assert_eq!(result, vec!["main", "feature-a", "feature-b"]);} #[test]fn test_sort_branches_main_first_main_already_first() {    let branches = vec![        BranchEntry {            name: "main".to_string(),            is_main: true,        },        BranchEntry {            name: "feature-a".to_string(),            is_main: false,        },        BranchEntry {            name: "feature-b".to_string(),            is_main: false,        },    ];    let result: Vec<_> = sort_branches_main_first(&branches)        .map(|entry| entry.name.as_str())        .collect();    assert_eq!(result, vec!["main", "feature-a", "feature-b"]);} #[test]fn test_sort_branches_main_first_preserves_recency_order_for_non_main() {    // Non-main branches should remain in their original (recency) order.    let branches = vec![        BranchEntry {            name: "recent-feature".to_string(),            is_main: false,        },        BranchEntry {            name: "main".to_string(),            is_main: true,        },        BranchEntry {            name: "older-feature".to_string(),            is_main: false,        },        BranchEntry {            name: "oldest-feature".to_string(),            is_main: false,        },    ];    let result: Vec<_> = sort_branches_main_first(&branches)        .map(|entry| entry.name.as_str())        .collect();    assert_eq!(        result,        vec!["main", "recent-feature", "older-feature", "oldest-feature"]    );} #[test]fn test_sort_branches_main_first_multiple_main_flags() {    // Defensive: both flagged as main (shouldn't happen in practice, but    // sort_branches_main_first should handle it gracefully).    let branches = vec![        BranchEntry {            name: "feature".to_string(),            is_main: false,        },        BranchEntry {            name: "main".to_string(),            is_main: true,        },        BranchEntry {            name: "master".to_string(),            is_main: true,        },    ];    let result: Vec<_> = sort_branches_main_first(&branches)        .map(|entry| entry.name.as_str())        .collect();    // Both main-flagged entries appear first, non-main last.    assert_eq!(result, vec!["main", "master", "feature"]);} #[test]fn test_parse_unified_diff_header_malformed() {    let header = "not a diff header";    let result = parse_unified_diff_header(header);    assert!(result.is_err());     let header2 = "@@ incomplete";    let result2 = parse_unified_diff_header(header2);    assert!(result2.is_err());} #[test]fn test_parse_git_status_modified_file_with_spaces() {    // Porcelain v2 output for a modified file with spaces in the name.    // Format: 1 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>    let status_output = "1 .M N... 100644 100644 100644 abc1234 def5678 test file.txt";    let result = LocalDiffStateModel::parse_git_status(status_output).unwrap();    assert_eq!(result.len(), 1);    assert_eq!(result[0].0, "test file.txt");    assert_eq!(result[0].1, GitFileStatus::Modified);} #[test]fn test_parse_git_status_modified_file_with_multiple_spaces() {    // Filename with multiple spaces.    let status_output = "1 .M N... 100644 100644 100644 abc1234 def5678 path to/my test file.txt";    let result = LocalDiffStateModel::parse_git_status(status_output).unwrap();    assert_eq!(result.len(), 1);    assert_eq!(result[0].0, "path to/my test file.txt");    assert_eq!(result[0].1, GitFileStatus::Modified);} #[test]fn test_parse_git_status_new_file_with_spaces() {    let status_output = "1 A. N... 000000 100644 100644 0000000 abc1234 new file name.rs";    let result = LocalDiffStateModel::parse_git_status(status_output).unwrap();    assert_eq!(result.len(), 1);    assert_eq!(result[0].0, "new file name.rs");    assert_eq!(result[0].1, GitFileStatus::New);} #[test]fn test_parse_git_status_renamed_file_with_spaces() {    // Porcelain v2 renamed entry (type 2) with spaces in the new path.    // Format: 2 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <X><score> <path>\0<origPath>    let status_output =        "2 R. N... 100644 100644 100644 abc1234 def5678 R100 new name.txt\0old name.txt";    let result = LocalDiffStateModel::parse_git_status(status_output).unwrap();    assert_eq!(result.len(), 1);    assert_eq!(result[0].0, "new name.txt");    assert!(matches!(        &result[0].1,        GitFileStatus::Renamed { old_path } if old_path == "old name.txt"    ));} #[test]fn test_parse_git_status_untracked_file_with_spaces() {    let status_output = "? my untracked file.txt";    let result = LocalDiffStateModel::parse_git_status(status_output).unwrap();    assert_eq!(result.len(), 1);    assert_eq!(result[0].0, "my untracked file.txt");    assert_eq!(result[0].1, GitFileStatus::Untracked);} #[test]fn test_parse_git_status_unmerged_file_with_spaces() {    // Porcelain v2 unmerged entry (type u) with spaces in the path.    // Format: u <xy> <sub> <m1> <m2> <m3> <mW> <h1> <h2> <h3> <path>    let status_output =        "u UU N... 100644 100644 100644 100644 abc1234 def5678 ghi9012 conflict file.txt";    let result = LocalDiffStateModel::parse_git_status(status_output).unwrap();    assert_eq!(result.len(), 1);    assert_eq!(result[0].0, "conflict file.txt");    assert_eq!(result[0].1, GitFileStatus::Conflicted);} #[test]fn test_parse_git_status_mixed_entries_with_spaces() {    // Multiple entries separated by NUL, mixing files with and without spaces.    let status_output = "1 .M N... 100644 100644 100644 abc1234 def5678 test file.txt\0\         1 .M N... 100644 100644 100644 abc1234 def5678 normal.txt\0\         ? another file with spaces.rs";    let result = LocalDiffStateModel::parse_git_status(status_output).unwrap();    assert_eq!(result.len(), 3);    assert_eq!(result[0].0, "test file.txt");    assert_eq!(result[1].0, "normal.txt");    assert_eq!(result[2].0, "another file with spaces.rs");} #[test]fn test_parse_git_status_file_without_spaces_still_works() {    // Ensure the splitn change doesn't break files without spaces.    let status_output = "1 .M N... 100644 100644 100644 abc1234 def5678 simple.txt";    let result = LocalDiffStateModel::parse_git_status(status_output).unwrap();    assert_eq!(result.len(), 1);    assert_eq!(result[0].0, "simple.txt");    assert_eq!(result[0].1, GitFileStatus::Modified);} #[tokio::test]async fn untracked_directory_diff_is_empty_and_non_binary() {    let repo_dir = tempfile::tempdir().expect("create temp repo dir");    std::fs::create_dir(repo_dir.path().join("nested-repo")).expect("create nested dir");     // `git status` reports a nested repo/worktree as a single untracked    // directory entry (with a trailing slash). It must short-circuit to an    // empty non-binary diff — the error fallback would otherwise mislabel it    // as binary and the view would render "Binary file - no diff available"    // instead of "New empty file".    let diff = LocalDiffStateModel::get_file_diff(        repo_dir.path(),        "nested-repo/",        &GitFileStatus::Untracked,        false,        None,    )    .await    .expect("get_file_diff should succeed for an untracked directory");     assert!(!diff.is_binary);    assert_eq!(diff.hunks.len(), 0);    assert_eq!(diff.status, GitFileStatus::Untracked);} #[tokio::test]async fn untracked_directory_has_no_baseline_content() {    let repo_dir = tempfile::tempdir().expect("create temp repo dir");    std::fs::create_dir(repo_dir.path().join("nested-repo")).expect("create nested dir");    std::fs::write(repo_dir.path().join("new-file.txt"), "hello\n").expect("write file");     // No baseline for a directory entry, so no editor is constructed for it.    let dir_content = LocalDiffStateModel::get_file_content_at_head(        repo_dir.path(),        "nested-repo/",        &GitFileStatus::Untracked,    )    .await;    assert_eq!(dir_content, None);     // Regular untracked files keep their empty baseline.    let file_content = LocalDiffStateModel::get_file_content_at_head(        repo_dir.path(),        "new-file.txt",        &GitFileStatus::Untracked,    )    .await;    assert_eq!(file_content, Some(String::new()));} #[tokio::test]async fn renamed_file_content_at_head_reads_old_path() {    let repo_dir = tempfile::tempdir().expect("create temp repo dir");    let repo_path = repo_dir.path();     // Set up a real git repo with one committed file, then rename it in the working tree    // (without committing the rename) so HEAD only knows about the old path.    run_git_command(repo_path, &["init", "-b", "main"])        .await        .expect("git init");    run_git_command(repo_path, &["config", "user.email", "test@test.com"])        .await        .expect("git config email");    run_git_command(repo_path, &["config", "user.name", "Test"])        .await        .expect("git config name");    std::fs::write(repo_path.join("old.txt"), "hello world\n").expect("write old.txt");    run_git_command(repo_path, &["add", "old.txt"])        .await        .expect("git add");    run_git_command(repo_path, &["commit", "-m", "initial"])        .await        .expect("git commit");     // Rename in the working tree only — `old.txt` no longer exists at this path, so `git    // show HEAD:new.txt` would fail (the bug in APP-5111).    std::fs::rename(repo_path.join("old.txt"), repo_path.join("new.txt"))        .expect("rename old.txt to new.txt");     let content = LocalDiffStateModel::get_file_content_at_head(        repo_path,        "new.txt",        &GitFileStatus::Renamed {            old_path: "old.txt".to_string(),        },    )    .await;     // The baseline content at HEAD must come from the old path, not the new one, so the code    // review pane can render a diff instead of "Unable to load file content".    assert_eq!(content, Some("hello world\n".to_string()));} #[tokio::test]async fn staged_rename_and_modify_produces_non_empty_diff() {    let repo_dir = tempfile::tempdir().expect("create temp repo dir");    let repo_path = repo_dir.path();     run_git_command(repo_path, &["init", "-b", "main"])        .await        .expect("git init");    run_git_command(repo_path, &["config", "user.email", "test@test.com"])        .await        .expect("git config email");    run_git_command(repo_path, &["config", "user.name", "Test"])        .await        .expect("git config name");    std::fs::write(        repo_path.join("old.txt"),        "line one\nline two\nline three\n",    )    .expect("write old.txt");    run_git_command(repo_path, &["add", "old.txt"])        .await        .expect("git add");    run_git_command(repo_path, &["commit", "-m", "initial"])        .await        .expect("git commit");     // Stage both the rename and a content edit, so nothing is left unstaged (git status    // reports this as a plain "R " entry with no unstaged component).    run_git_command(repo_path, &["mv", "old.txt", "new.txt"])        .await        .expect("git mv");    std::fs::write(        repo_path.join("new.txt"),        "line one\nline two changed\nline three\n",    )    .expect("write new.txt");    run_git_command(repo_path, &["add", "new.txt"])        .await        .expect("git add new.txt");     let diff = LocalDiffStateModel::get_file_diff(        repo_path,        "new.txt",        &GitFileStatus::Renamed {            old_path: "old.txt".to_string(),        },        false,        None,    )    .await    .expect("get_file_diff should succeed for a fully staged rename+modify");     // A fully staged rename with a staged content edit must still render an inline diff    // instead of falling through to "File renamed without changes": comparing only the    // index against the working tree (as before the fix) produced an empty diff here,    // since both changes were already staged.    assert!(        !diff.is_empty(),        "expected a non-empty diff for a fully staged rename+modify"    );} #[tokio::test]async fn num_lines_in_file_if_non_binary_counts_lines_in_text_file() {    let dir = tempfile::tempdir().expect("create temp dir");    let file_path = dir.path().join("file.txt");    std::fs::write(&file_path, "one\ntwo\nthree\n").expect("write file");     let num_lines = LocalDiffStateModel::num_lines_in_file_if_non_binary(&file_path)        .await        .expect("counting a regular file should succeed");    assert_eq!(num_lines, Some(3));} #[tokio::test]async fn num_lines_in_file_if_non_binary_errors_for_directory() {    let dir = tempfile::tempdir().expect("create temp dir");     // Directories aren't countable. The metadata callers degrade this error    // to a 0-line contribution per entry instead of failing the whole    // metadata computation.    let result = LocalDiffStateModel::num_lines_in_file_if_non_binary(dir.path()).await;    assert!(result.is_err());}