use iso_code::git::parse_worktree_list_porcelain;
use iso_code::types::WorktreeState;
use proptest::prelude::*;
use proptest::strategy::Strategy;
#[derive(Debug, Clone)]
struct BlockSpec {
path: String,
head: String,
branch: Option<String>,
bare: bool,
detached: bool,
locked: Option<Option<String>>, prunable: Option<Option<String>>,
}
fn hex40() -> impl Strategy<Value = String> {
"[0-9a-f]{40}".prop_map(|s| s.to_string())
}
fn safe_path(nul_delimited: bool) -> impl Strategy<Value = String> {
if nul_delimited {
"/[\\sA-Za-z0-9_\\-/\\.áéíñü ]{1,24}".prop_map(|s| s.to_string()).boxed()
} else {
"/[A-Za-z0-9_\\-/\\. áéíñü]{1,24}".prop_map(|s| s.to_string()).boxed()
}
}
fn branch_name() -> impl Strategy<Value = String> {
"[a-zA-Z][a-zA-Z0-9_\\-./]{0,30}".prop_map(|s| s.to_string())
}
fn block_spec(nul: bool) -> impl Strategy<Value = BlockSpec> {
(
safe_path(nul),
hex40(),
prop::option::of(branch_name()),
any::<bool>(), any::<bool>(), prop::option::of(prop::option::of("[a-zA-Z0-9 ]{0,20}".prop_map(|s| s.to_string()))),
prop::option::of(prop::option::of("[a-zA-Z0-9 ]{0,20}".prop_map(|s| s.to_string()))),
)
.prop_map(|(path, head, branch, bare, detached, locked, prunable)| BlockSpec {
path,
head,
branch,
bare,
detached,
locked,
prunable,
})
}
fn encode_block(spec: &BlockSpec, nul_delimited: bool, ref_prefix: bool) -> Vec<u8> {
let sep: u8 = if nul_delimited { 0 } else { b'\n' };
let mut out = Vec::new();
out.extend_from_slice(b"worktree ");
out.extend_from_slice(spec.path.as_bytes());
out.push(sep);
out.extend_from_slice(b"HEAD ");
out.extend_from_slice(spec.head.as_bytes());
out.push(sep);
if spec.bare {
out.extend_from_slice(b"bare");
out.push(sep);
} else if spec.detached {
out.extend_from_slice(b"detached");
out.push(sep);
} else if let Some(ref b) = spec.branch {
out.extend_from_slice(b"branch ");
if ref_prefix {
out.extend_from_slice(b"refs/heads/");
}
out.extend_from_slice(b.as_bytes());
out.push(sep);
}
if let Some(ref reason) = spec.locked {
out.extend_from_slice(b"locked");
if let Some(r) = reason {
out.push(b' ');
out.extend_from_slice(r.as_bytes());
}
out.push(sep);
}
if let Some(ref reason) = spec.prunable {
out.extend_from_slice(b"prunable");
if let Some(r) = reason {
out.push(b' ');
out.extend_from_slice(r.as_bytes());
}
out.push(sep);
}
out
}
fn encode_output(blocks: &[BlockSpec], nul_delimited: bool, ref_prefix: bool) -> Vec<u8> {
let mut out = Vec::new();
for (i, b) in blocks.iter().enumerate() {
out.extend_from_slice(&encode_block(b, nul_delimited, ref_prefix));
if nul_delimited {
out.push(0);
} else {
out.push(b'\n');
}
let _ = i;
}
out
}
proptest! {
#[test]
fn newline_mode_parses_without_panic(
blocks in prop::collection::vec(block_spec(false), 0..6),
ref_prefix in any::<bool>(),
) {
let bytes = encode_output(&blocks, false, ref_prefix);
let result = parse_worktree_list_porcelain(&bytes, false).unwrap();
for h in &result {
prop_assert!(!h.path.as_os_str().is_empty(), "empty path in result");
prop_assert!(matches!(
h.state,
WorktreeState::Active | WorktreeState::Locked | WorktreeState::Orphaned
));
}
}
#[test]
fn nul_mode_parses_without_panic(
blocks in prop::collection::vec(block_spec(true), 0..6),
ref_prefix in any::<bool>(),
) {
let bytes = encode_output(&blocks, true, ref_prefix);
let result = parse_worktree_list_porcelain(&bytes, true).unwrap();
for h in &result {
prop_assert!(!h.path.as_os_str().is_empty());
}
}
#[test]
fn random_garbage_does_not_panic(bytes in prop::collection::vec(any::<u8>(), 0..512)) {
let _ = parse_worktree_list_porcelain(&bytes, false);
let _ = parse_worktree_list_porcelain(&bytes, true);
}
#[test]
fn ref_prefix_is_stripped(name in branch_name()) {
let block = BlockSpec {
path: "/tmp/wt".into(),
head: "a".repeat(40),
branch: Some(name.clone()),
bare: false,
detached: false,
locked: None,
prunable: None,
};
let with_prefix = encode_output(std::slice::from_ref(&block), false, true);
let without_prefix = encode_output(&[block], false, false);
let a = parse_worktree_list_porcelain(&with_prefix, false).unwrap();
let b = parse_worktree_list_porcelain(&without_prefix, false).unwrap();
prop_assert_eq!(a.len(), 1);
prop_assert_eq!(b.len(), 1);
prop_assert_eq!(&a[0].branch, &name);
prop_assert_eq!(&b[0].branch, &name);
}
#[test]
fn locked_has_precedence_over_prunable(
name in branch_name(),
nul in any::<bool>(),
) {
let block = BlockSpec {
path: "/tmp/wt".into(),
head: "a".repeat(40),
branch: Some(name),
bare: false,
detached: false,
locked: Some(None),
prunable: Some(None),
};
let bytes = encode_output(&[block], nul, true);
let result = parse_worktree_list_porcelain(&bytes, nul).unwrap();
prop_assert_eq!(result.len(), 1);
prop_assert!(matches!(result[0].state, WorktreeState::Locked));
}
}