use super::{DefaultBranchName, LineDiff, WorktreeInfo};
use insta::assert_debug_snapshot;
use rstest::rstest;
fn parse_single(input: &str) -> WorktreeInfo {
let list = WorktreeInfo::parse_porcelain_list(input).expect("parse ok");
assert_eq!(list.len(), 1);
list.into_iter().next().unwrap()
}
#[test]
fn test_parse_worktree_list_no_trailing_blank_line() {
let output = "worktree /path/to/repo1\nHEAD abc123\nbranch refs/heads/main\n\nworktree /path/to/repo2\nHEAD def456\nbranch refs/heads/dev";
let result = WorktreeInfo::parse_porcelain_list(output);
assert!(result.is_ok());
let worktrees = result.unwrap();
assert_eq!(
worktrees.len(),
2,
"Should parse both worktrees even without trailing blank line"
);
}
#[test]
fn test_parse_worktree_list_multiple_worktrees() {
let output = "worktree /path/to/main\nHEAD abc123\nbranch refs/heads/main\n\nworktree /path/to/feature\nHEAD def456\nbranch refs/heads/feature\ndetached\n\n";
let worktrees = WorktreeInfo::parse_porcelain_list(output).unwrap();
let [main_wt, feature_wt]: [WorktreeInfo; 2] = worktrees.try_into().unwrap();
assert_eq!(main_wt.branch, Some("main".to_string()));
assert!(!main_wt.detached);
assert_eq!(feature_wt.branch, Some("feature".to_string()));
assert!(feature_wt.detached);
}
#[rstest]
#[case::missing_path("worktree\nHEAD abc123\n\n", "missing path")]
#[case::head_missing_sha(
"worktree /path/to/repo\nHEAD\nbranch refs/heads/main\n\n",
"missing SHA"
)]
#[case::branch_missing_ref("worktree /path/to/repo\nHEAD abc123\nbranch\n\n", "missing ref")]
fn test_parse_worktree_list_error_cases(#[case] input: &str, #[case] expected_message: &str) {
let result = WorktreeInfo::parse_porcelain_list(input);
assert!(result.is_err(), "Parsing should fail");
let err = result.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains(expected_message),
"Error should mention {expected_message}, got: {msg}"
);
}
#[rstest]
#[case::normal("ref: refs/heads/main\tHEAD\n", Ok("main"))]
#[case::feature_branch(
"ref: refs/heads/feature/nested/branch\tHEAD\n",
Ok("feature/nested/branch")
)]
#[case::empty_output("", Err(Some("symbolic ref")))]
#[case::missing_prefix("refs/heads/main\tHEAD\n", Err(None))]
#[case::missing_tab("ref: refs/heads/main", Err(None))]
#[case::multiple_matches(
"ref: refs/heads/main\tHEAD\nref: refs/heads/develop\tHEAD\n",
Ok("main")
)]
#[case::missing_refs_heads_prefix("ref: main\tHEAD\n", Err(None))]
fn test_parse_remote_default_branch(
#[case] input: &str,
#[case] expected: Result<&str, Option<&str>>,
) {
let result = DefaultBranchName::from_remote(input).map(DefaultBranchName::into_string);
match expected {
Ok(expected_branch) => {
assert!(result.is_ok());
assert_eq!(result.unwrap(), expected_branch);
}
Err(expected_substr) => {
assert!(result.is_err());
if let Some(substr) = expected_substr {
let msg = result.unwrap_err().to_string();
assert!(
msg.contains(substr),
"Error should mention {substr}, got: {msg}"
);
}
}
}
}
#[test]
fn test_parse_local_default_branch_normal() {
let result =
DefaultBranchName::from_local("origin", "origin/main").map(DefaultBranchName::into_string);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "main");
}
#[test]
fn test_parse_local_default_branch_without_remote_prefix() {
let result =
DefaultBranchName::from_local("origin", "main").map(DefaultBranchName::into_string);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "main");
}
#[test]
fn test_parse_local_default_branch_with_nested_slashes() {
let result = DefaultBranchName::from_local("origin", "origin/feature/sub/branch")
.map(DefaultBranchName::into_string);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "feature/sub/branch");
}
#[test]
fn test_parse_local_default_branch_empty_output() {
let result = DefaultBranchName::from_local("origin", "").map(DefaultBranchName::into_string);
assert!(result.is_err(), "Empty output should error");
let err = result.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Empty branch"),
"Error should mention empty branch, got: {msg}"
);
}
#[test]
fn test_parse_local_default_branch_whitespace_only() {
let result =
DefaultBranchName::from_local("origin", " \n ").map(DefaultBranchName::into_string);
assert!(result.is_err(), "Whitespace-only should error");
}
#[test]
fn test_parse_local_default_branch_empty_remote() {
let result =
DefaultBranchName::from_local("", "/weird/branch").map(DefaultBranchName::into_string);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "weird/branch");
}
#[rstest]
#[case::all_parts(" 23 files changed, 624 insertions(+), 160 deletions(-)", 624, 160)]
#[case::insertions_only(" 1 file changed, 6 insertions(+)", 6, 0)]
#[case::deletions_only(" 2 files changed, 10 deletions(-)", 0, 10)]
#[case::empty("", 0, 0)]
#[case::whitespace(" \n ", 0, 0)]
#[case::singular(" 1 file changed, 1 insertion(+), 1 deletion(-)", 1, 1)]
fn test_line_diff_from_shortstat(
#[case] input: &str,
#[case] expected_added: usize,
#[case] expected_deleted: usize,
) {
let diff = LineDiff::from_shortstat(input);
assert_eq!(diff.added, expected_added);
assert_eq!(diff.deleted, expected_deleted);
}
#[test]
fn snapshot_parse_worktree_list_empty_output() {
let result = WorktreeInfo::parse_porcelain_list("").expect("parse ok");
assert_debug_snapshot!(result, @"[]");
}
#[test]
fn snapshot_parse_worktree_list_missing_head() {
let wt = parse_single("worktree /path/to/repo\nbranch refs/heads/main\n\n");
assert_debug_snapshot!(wt, @r#"
WorktreeInfo {
path: "/path/to/repo",
head: "",
branch: Some(
"main",
),
bare: false,
detached: false,
locked: None,
prunable: None,
}
"#);
}
#[test]
fn snapshot_parse_worktree_list_locked_with_empty_reason() {
let wt =
parse_single("worktree /path/to/repo\nHEAD abc123\nbranch refs/heads/main\nlocked\n\n");
assert_debug_snapshot!(wt, @r#"
WorktreeInfo {
path: "/path/to/repo",
head: "abc123",
branch: Some(
"main",
),
bare: false,
detached: false,
locked: Some(
"",
),
prunable: None,
}
"#);
}
#[test]
fn snapshot_parse_worktree_list_locked_with_reason() {
let wt = parse_single(
"worktree /path/to/repo\nHEAD abc123\nbranch refs/heads/main\nlocked working on it\n\n",
);
assert_debug_snapshot!(wt, @r#"
WorktreeInfo {
path: "/path/to/repo",
head: "abc123",
branch: Some(
"main",
),
bare: false,
detached: false,
locked: Some(
"working on it",
),
prunable: None,
}
"#);
}
#[test]
fn snapshot_parse_worktree_list_prunable_empty() {
let wt =
parse_single("worktree /path/to/repo\nHEAD abc123\nbranch refs/heads/main\nprunable\n\n");
assert_debug_snapshot!(wt, @r#"
WorktreeInfo {
path: "/path/to/repo",
head: "abc123",
branch: Some(
"main",
),
bare: false,
detached: false,
locked: None,
prunable: Some(
"",
),
}
"#);
}
#[test]
fn snapshot_parse_worktree_list_fields_before_worktree() {
let wt = parse_single(
"HEAD abc123\nbranch refs/heads/main\nworktree /path/to/repo\nHEAD def456\n\n",
);
assert_debug_snapshot!(wt, @r#"
WorktreeInfo {
path: "/path/to/repo",
head: "def456",
branch: None,
bare: false,
detached: false,
locked: None,
prunable: None,
}
"#);
}
#[test]
fn snapshot_parse_worktree_list_bare_repository() {
let wt = parse_single("worktree /path/to/repo\nbare\n\n");
assert_debug_snapshot!(wt, @r#"
WorktreeInfo {
path: "/path/to/repo",
head: "",
branch: None,
bare: true,
detached: false,
locked: None,
prunable: None,
}
"#);
}
#[test]
fn snapshot_parse_worktree_list_detached_head() {
let wt = parse_single("worktree /path/to/repo\nHEAD abc123\ndetached\n\n");
assert_debug_snapshot!(wt, @r#"
WorktreeInfo {
path: "/path/to/repo",
head: "abc123",
branch: None,
bare: false,
detached: true,
locked: None,
prunable: None,
}
"#);
}
#[test]
fn snapshot_parse_worktree_list_branch_with_refs_prefix() {
let wt = parse_single(
"worktree /path/to/repo\nHEAD abc123\nbranch refs/heads/feature/nested/branch\n\n",
);
assert_debug_snapshot!(wt, @r#"
WorktreeInfo {
path: "/path/to/repo",
head: "abc123",
branch: Some(
"feature/nested/branch",
),
bare: false,
detached: false,
locked: None,
prunable: None,
}
"#);
}
#[test]
fn snapshot_parse_worktree_list_branch_without_refs_prefix() {
let wt = parse_single("worktree /path/to/repo\nHEAD abc123\nbranch main\n\n");
assert_debug_snapshot!(wt, @r#"
WorktreeInfo {
path: "/path/to/repo",
head: "abc123",
branch: Some(
"main",
),
bare: false,
detached: false,
locked: None,
prunable: None,
}
"#);
}
#[test]
fn snapshot_parse_worktree_list_unknown_attributes() {
let wt = parse_single("worktree /path/to/repo\nHEAD abc123\nfutureattr somevalue\n\n");
assert_debug_snapshot!(wt, @r#"
WorktreeInfo {
path: "/path/to/repo",
head: "abc123",
branch: None,
bare: false,
detached: false,
locked: None,
prunable: None,
}
"#);
}