use crate::{
DiffDocument, DiffSide, FileDiff, FileStatus, Hunk, PatchLine, RepoPath, SourceDocument,
SourceResult, SourceUnavailable, StageState,
};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct DocumentBuilder {
repo_root: String,
files: Vec<FileDiff>,
overrides: Vec<(RepoPath, DiffSide, SourceResult)>,
}
impl Default for DocumentBuilder {
fn default() -> Self {
Self::new()
}
}
impl DocumentBuilder {
#[must_use]
pub fn new() -> Self {
Self {
repo_root: "/repo".to_owned(),
files: Vec::new(),
overrides: Vec::new(),
}
}
#[must_use]
pub fn repo_root(mut self, repo_root: impl Into<String>) -> Self {
self.repo_root = repo_root.into();
self
}
#[must_use]
pub fn changed(self, path: &str, old: &str, new: &str) -> Self {
self.changed_with(path, old, new, std::convert::identity)
}
#[must_use]
pub fn changed_with(
self,
path: &str,
old: &str,
new: &str,
customize: impl FnOnce(FileDiff) -> FileDiff,
) -> Self {
let file = customize(FileDiff::from_texts(path, old, new).expect("valid fixture path"));
self.file(file)
}
#[must_use]
pub fn deleted(self, path: &str, old: &str) -> Self {
self.changed(path, old, "")
}
#[must_use]
pub fn untracked(self, path: &str, new: &str) -> Self {
self.changed_with(path, "", new, |file| FileDiff {
status: FileStatus::Untracked,
..file
})
}
#[must_use]
pub fn changed_staged(self, path: &str, old: &str, new: &str, staged: StageState) -> Self {
self.changed_with(path, old, new, |file| FileDiff { staged, ..file })
}
#[must_use]
pub fn renamed(self, old_path: &str, new_path: &str, old: &str, new: &str) -> Self {
let old_path = fixture_path(old_path);
self.changed_with(new_path, old, new, |file| FileDiff {
old_path: Some(old_path),
status: FileStatus::Renamed,
..file
})
}
#[must_use]
pub fn copied(self, old_path: &str, new_path: &str, old: &str, new: &str) -> Self {
let old_path = fixture_path(old_path);
self.changed_with(new_path, old, new, |file| FileDiff {
old_path: Some(old_path),
status: FileStatus::Copied,
..file
})
}
#[must_use]
pub fn changed_with_hunk_window(
self,
path: &str,
old: &str,
new: &str,
window: std::ops::RangeInclusive<usize>,
) -> Self {
let mut file = FileDiff::from_texts(path, old, new).expect("valid fixture path");
let mut hunk = file.hunks.remove(0);
hunk.lines.retain(|line| {
line.old_line_no
.is_some_and(|number| window.contains(&number))
|| line
.new_line_no
.is_some_and(|number| window.contains(&number))
});
let start = *window.start();
let count = window.end().saturating_sub(start).saturating_add(1);
hunk.old_start = start;
hunk.old_count = count;
hunk.new_start = start;
hunk.new_count = count;
hunk.header = format!("@@ -{start},{count} +{start},{count} @@");
file.hunks = vec![hunk];
self.file(file)
}
#[must_use]
pub fn added(self, path: &str, new: &str) -> Self {
self.changed(path, "", new)
}
#[must_use]
pub fn binary(self, path: &str) -> Self {
let path = fixture_path(path);
self.file(FileDiff {
old_path: Some(path.clone()),
path,
status: FileStatus::Modified,
staged: StageState::Unstaged,
hunks: Vec::new(),
binary: true,
mode: None,
no_newline_at_end: false,
omitted_bytes: None,
old_source: Err(SourceUnavailable::Binary),
new_source: Err(SourceUnavailable::Binary),
})
}
#[must_use]
pub fn generated(self, path: &str, lines: usize) -> Self {
let stem = path.rsplit('/').next().unwrap_or(path).replace('.', "_");
let patch_lines: Vec<PatchLine> = (1..=lines)
.map(|line| PatchLine::added(format!("let {stem}_value_{line} = {line};"), line))
.collect();
let path = fixture_path(path);
self.file(FileDiff {
old_path: None,
path,
status: FileStatus::Added,
staged: StageState::Unstaged,
hunks: vec![Hunk {
header: format!("@@ -0,0 +1,{lines} @@"),
function_context: None,
old_start: 0,
old_count: 0,
new_start: 1,
new_count: lines,
lines: patch_lines,
}],
binary: false,
mode: None,
no_newline_at_end: false,
omitted_bytes: None,
old_source: FileDiff::uncaptured_source(FileStatus::Added, DiffSide::Old),
new_source: FileDiff::uncaptured_source(FileStatus::Added, DiffSide::New),
})
}
#[must_use]
pub fn generated_files(mut self, count: usize, lines: usize) -> Self {
for index in 0..count {
self = self.generated(&format!("src/file_{index:02}.rs"), lines);
}
self
}
#[must_use]
pub fn file(mut self, file: FileDiff) -> Self {
self.files.push(file);
self
}
#[must_use]
pub fn source(
mut self,
path: &str,
side: DiffSide,
result: Result<impl AsRef<str>, SourceUnavailable>,
) -> Self {
let result = result.and_then(|text| SourceDocument::new(text).map(Arc::new));
self.overrides.push((fixture_path(path), side, result));
self
}
#[must_use]
pub fn build(self) -> Arc<DiffDocument> {
let mut files = self.files;
for (path, side, result) in self.overrides {
let file = files
.iter_mut()
.find(|file| file.path == path)
.unwrap_or_else(|| panic!("source override for unknown fixture path {path}"));
match side {
DiffSide::Old => file.old_source = result,
DiffSide::New => file.new_source = result,
}
}
Arc::new(DiffDocument {
repo_root: self.repo_root,
files,
})
}
}
#[must_use]
pub fn file_diff() -> FileDiff {
FileDiff::from_texts("src/lib.rs", "old\n", "new\n").expect("valid default fixture")
}
#[must_use]
pub fn hunk() -> Hunk {
file_diff().hunks.remove(0)
}
#[must_use]
pub fn patch_line() -> PatchLine {
PatchLine::added("new", 1)
}
#[must_use]
pub fn diff_document() -> DiffDocument {
DiffDocument {
repo_root: "/repo".to_owned(),
files: vec![file_diff()],
}
}
fn fixture_path(path: &str) -> RepoPath {
RepoPath::new(path).expect("valid fixture path")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn changed_with_applies_struct_update_overrides_and_preserves_sources() {
let document = DocumentBuilder::new()
.changed_with("src/lib.rs", "old\n", "new\n", |file| FileDiff {
staged: StageState::Staged,
..file
})
.source(
"src/lib.rs",
DiffSide::New,
Err::<&str, _>(SourceUnavailable::Binary),
)
.build();
let file = &document.files[0];
assert_eq!(file.staged, StageState::Staged);
assert_eq!(file.source_document(DiffSide::Old).unwrap().text(), "old\n");
assert_eq!(
file.source_unavailable(DiffSide::New),
Some(&SourceUnavailable::Binary)
);
}
#[test]
fn low_level_factories_support_struct_update_syntax() {
let file = FileDiff {
binary: true,
hunks: Vec::new(),
..file_diff()
};
let document = DiffDocument {
files: vec![file],
..diff_document()
};
assert!(document.files[0].binary);
}
#[test]
fn common_file_kinds_have_consistent_status_and_sources() {
let document = DocumentBuilder::new()
.deleted("deleted.rs", "old\n")
.untracked("new.rs", "new\n")
.renamed("old.rs", "renamed.rs", "old\n", "new\n")
.copied("source.rs", "copy.rs", "old\n", "new\n")
.build();
let text = |index: usize, side| {
document.files[index]
.source_document(side)
.expect("fixture source")
.text()
.to_owned()
};
assert_eq!(document.files[0].status, FileStatus::Deleted);
assert_eq!(document.files[1].status, FileStatus::Untracked);
assert_eq!(document.files[2].status, FileStatus::Renamed);
assert_eq!(document.files[3].status, FileStatus::Copied);
assert_eq!(
document.files[0].source_unavailable(DiffSide::New),
Some(&SourceUnavailable::Absent)
);
assert_eq!(
document.files[1].source_unavailable(DiffSide::Old),
Some(&SourceUnavailable::Absent)
);
assert_eq!(text(1, DiffSide::New), "new\n");
assert_eq!(text(2, DiffSide::Old), "old\n");
assert_eq!(text(2, DiffSide::New), "new\n");
assert_eq!(text(3, DiffSide::Old), "old\n");
assert_eq!(text(3, DiffSide::New), "new\n");
}
}