use hunkpick::model::{FileContent, Patch};
use hunkpick::{emit, parser, renumber, select, validate};
use proptest::prelude::*;
#[derive(Clone, Debug)]
enum Edit {
Replace,
Insert,
Delete,
TwoBlocks,
}
#[derive(Clone, Debug)]
struct DiffShape {
lines: usize,
edits: Vec<(usize, Edit)>,
crlf: bool,
no_eof_newline: bool,
preamble: bool,
no_trailing_newline: bool,
high_line_numbers: bool,
}
fn arb_shape() -> impl Strategy<Value = DiffShape> {
let edit = prop_oneof![
Just(Edit::Replace),
Just(Edit::Insert),
Just(Edit::Delete),
Just(Edit::TwoBlocks),
];
(
2usize..24,
prop::collection::vec((0usize..24, edit), 1..6),
any::<bool>(),
any::<bool>(),
any::<bool>(),
any::<bool>(),
any::<bool>(),
)
.prop_map(
|(
lines,
edits,
crlf,
no_eof_newline,
preamble,
no_trailing_newline,
high_line_numbers,
)| DiffShape {
lines,
edits,
crlf,
no_eof_newline,
preamble,
no_trailing_newline,
high_line_numbers,
},
)
}
fn render(shape: &DiffShape) -> Vec<u8> {
let nl: &[u8] = if shape.crlf { b"\r\n" } else { b"\n" };
let mut out: Vec<u8> = Vec::new();
if shape.preamble {
out.extend_from_slice(b"From: Someone <someone@example.invalid>");
out.extend_from_slice(nl);
out.extend_from_slice(b"Subject: [PATCH] generated");
out.extend_from_slice(nl);
out.extend_from_slice(nl);
}
out.extend_from_slice(b"diff --git a/f b/f");
out.extend_from_slice(nl);
out.extend_from_slice(b"--- a/f");
out.extend_from_slice(nl);
out.extend_from_slice(b"+++ b/f");
out.extend_from_slice(nl);
let base = if shape.high_line_numbers {
u32::MAX as usize - 10_000
} else {
0
};
let mut at = 1usize;
let mut delta: i64 = 0;
let mut hunks = 0usize;
for (raw, edit) in &shape.edits {
if at + 2 > shape.lines {
break;
}
let start = at + raw % (shape.lines - at + 1).max(1);
let start = start.min(shape.lines.saturating_sub(1)).max(at);
let (old_lines, new_lines, body): (usize, usize, Vec<(u8, String)>) = match edit {
Edit::Replace => (
2,
2,
vec![
(b' ', format!("line {start}")),
(b'-', format!("line {}", start + 1)),
(b'+', format!("changed {}", start + 1)),
],
),
Edit::Insert => (
1,
2,
vec![
(b' ', format!("line {start}")),
(b'+', format!("inserted at {start}")),
],
),
Edit::Delete => (
2,
1,
vec![
(b' ', format!("line {start}")),
(b'-', format!("line {}", start + 1)),
],
),
Edit::TwoBlocks => (
5,
5,
vec![
(b' ', format!("line {start}")),
(b'-', format!("line {}", start + 1)),
(b'+', format!("changed {}", start + 1)),
(b' ', format!("line {}", start + 2)),
(b'-', format!("line {}", start + 3)),
(b'+', format!("changed {}", start + 3)),
(b' ', format!("line {}", start + 4)),
],
),
};
let new_start = (start as i64 + delta).max(1) as usize;
delta += new_lines as i64 - old_lines as i64;
out.extend_from_slice(
format!(
"@@ -{} +{} @@",
range(start + base, old_lines),
range(new_start + base, new_lines)
)
.as_bytes(),
);
out.extend_from_slice(nl);
for (marker, text) in &body {
out.push(*marker);
out.extend_from_slice(text.as_bytes());
out.extend_from_slice(nl);
}
at = start + old_lines + 1;
hunks += 1;
}
if shape.no_eof_newline && hunks > 0 {
out.extend_from_slice(b"\\ No newline at end of file");
out.extend_from_slice(nl);
}
if shape.no_trailing_newline {
while out.last() == Some(&b'\n') || out.last() == Some(&b'\r') {
out.pop();
}
}
out
}
fn range(start: usize, count: usize) -> String {
if count == 1 {
start.to_string()
} else {
format!("{start},{count}")
}
}
fn sub_hunk_count(patch: &Patch) -> usize {
select::build_view(patch)
.first()
.map(|subs| subs.len())
.unwrap_or(0)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(512))]
#[test]
fn parse_then_emit_is_the_input(shape in arb_shape()) {
let src = render(&shape);
let patch = parser::parse(&src).expect("generated diffs are well formed");
prop_assert_eq!(emit::emit(&patch), src);
}
#[test]
fn a_selection_is_never_internally_inconsistent(shape in arb_shape(), mask in any::<u32>()) {
let src = render(&shape);
let patch = parser::parse(&src).expect("generated diffs are well formed");
let count = sub_hunk_count(&patch);
prop_assume!(count > 0);
let picks: Vec<String> = (1..=count)
.filter(|i| mask & (1 << (i % 32)) != 0)
.map(|i| i.to_string())
.collect();
prop_assume!(!picks.is_empty());
let selectors = select::parse_selectors(&picks).expect("indices always parse");
let result = select::select(&patch, &selectors).expect("in-range indices resolve");
prop_assert_eq!(validate::validate_internal(&result), Ok(()));
prop_assert_eq!(validate::validate_input(&result), Ok(()));
}
#[test]
fn renumbering_is_idempotent(shape in arb_shape()) {
let src = render(&shape);
let mut once = parser::parse(&src).expect("generated diffs are well formed");
renumber::renumber_new_side(&mut once);
let mut twice = once.clone();
renumber::renumber_new_side(&mut twice);
prop_assert_eq!(twice, once);
}
#[test]
fn selecting_everything_preserves_the_change_counts(shape in arb_shape()) {
let src = render(&shape);
let patch = parser::parse(&src).expect("generated diffs are well formed");
prop_assume!(sub_hunk_count(&patch) > 0);
let selectors = select::parse_selectors(&["*".to_string()]).unwrap();
let result = select::select(&patch, &selectors).expect("'*' always resolves");
prop_assert_eq!(change_counts(&result), change_counts(&patch));
}
}
fn change_counts(patch: &Patch) -> (u32, u32) {
let mut added = 0;
let mut deleted = 0;
for f in &patch.files {
if let FileContent::Text(hunks) = &f.content {
for h in hunks {
let (a, d) = h.change_counts();
added += a;
deleted += d;
}
}
}
(added, deleted)
}