refactor: move embedded tests into dedicated test directory

This commit is contained in:
Valdemar Lorenzen
2026-10-16 16:30:07 +02:00
parent de6a57e67d
commit 2772e6878a
55 changed files with 45752 additions and 45858 deletions
+4 -473
View File
@@ -1050,478 +1050,9 @@ pub fn project(text: &str, source: &Source) -> Result<String, String> {
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
struct Fixture(PathBuf);
impl Fixture {
fn new() -> Self {
let path = std::env::temp_dir().join(format!(
"vamp-resolver-test-{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir(&path).unwrap();
Self(path)
}
fn jj(&self, args: &[&str]) -> String {
let output = Command::new("jj")
.current_dir(&self.0)
.env("JJ_CONFIG", "/dev/null")
.args([
"--config=user.name=Resolver",
"--config=user.email=resolver@example.test",
"--config=debug.commit-timestamp=\"2001-01-01T00:00:00Z\"",
"--config=debug.operation-timestamp=\"2001-01-01T00:00:00Z\"",
])
.args(args)
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap()
}
fn conflict(&self) {
self.jj(&["git", "init", "--no-colocate", "."]);
self.jj(&["config", "set", "--repo", "user.name", "Resolver"]);
self.jj(&[
"config",
"set",
"--repo",
"user.email",
"resolver@example.test",
]);
self.jj(&[
"config",
"set",
"--repo",
"debug.commit-timestamp",
"\"2001-01-01T00:00:00Z\"",
]);
self.jj(&[
"config",
"set",
"--repo",
"debug.operation-timestamp",
"\"2001-01-01T00:00:00Z\"",
]);
fs::write(self.0.join("file"), "base\n").unwrap();
self.jj(&["bookmark", "create", "base"]);
self.jj(&["new", "base"]);
fs::write(self.0.join("file"), "left\n").unwrap();
self.jj(&["bookmark", "create", "left"]);
self.jj(&["new", "base"]);
fs::write(self.0.join("file"), "right\n").unwrap();
self.jj(&["bookmark", "create", "right"]);
self.jj(&["new", "left", "right"]);
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
#[test]
fn discovery_is_readonly_and_save_uses_jj_authority_and_stale_guards() {
let fixture = Fixture::new();
fixture.conflict();
let operation = fixture.jj(&[
"--ignore-working-copy",
"op",
"log",
"--no-graph",
"-n",
"1",
"-T",
"id",
]);
let workspace = load(&fixture.0, &|| false).unwrap();
assert_eq!(workspace.files.len(), 1);
let file = &workspace.files[0];
assert!(file.sources.iter().any(|s| s.base && s.bytes == b"base\n"));
assert!(file.sources.iter().any(|s| !s.base && s.bytes == b"left\n"));
assert!(
file.sources
.iter()
.any(|s| !s.base && s.bytes == b"right\n")
);
for bookmark in ["base", "left", "right"] {
assert!(
file.sources
.iter()
.any(|s| s.label.contains(&format!("[{bookmark}]")))
);
}
assert_eq!(
operation,
fixture.jj(&[
"--ignore-working-copy",
"op",
"log",
"--no-graph",
"-n",
"1",
"-T",
"id"
])
);
let blocks = regions(file.result.as_ref().unwrap());
assert_eq!(blocks.len(), 1);
for source in &file.sources {
assert_eq!(blocks[0].choose(source).unwrap().as_bytes(), source.bytes);
}
let mut changed = blocks[0].clone();
changed.sides.pop();
assert!(changed.choose(&file.sources[0]).is_err());
fs::write(fixture.0.join("file"), "external\n").unwrap();
assert!(
save(&workspace, 0, Resolution::Text("lost\n".into()), &|| false)
.unwrap_err()
.contains("externally")
);
fs::write(fixture.0.join("file"), file.result.as_ref().unwrap()).unwrap();
fixture.jj(&["new", "base"]);
let before = fs::read(fixture.0.join("file")).unwrap();
assert!(
save(
&workspace,
0,
Resolution::Text("wrong checkout\n".into()),
&|| false
)
.unwrap_err()
.contains("checkout changed")
);
assert_eq!(fs::read(fixture.0.join("file")).unwrap(), before);
}
#[test]
fn text_choice_is_confirmed_by_jj() {
let fixture = Fixture::new();
fixture.conflict();
let mut workspace = load(&fixture.0, &|| false).unwrap();
let cancelled_after_write =
|| fs::read(fixture.0.join("file")).is_ok_and(|bytes| bytes == b"draft\n");
let saved = save(
&workspace,
0,
Resolution::Text("draft\n".into()),
&cancelled_after_write,
)
.unwrap();
assert_eq!(saved.file.status, Status::Unconfirmed);
assert!(saved.confirmation.is_some());
assert_eq!(fs::read(fixture.0.join("file")).unwrap(), b"draft\n");
assert_eq!(
entries(&fixture.0, &workspace.checkout.commit, &|| false).unwrap(),
[PathBuf::from("file")]
);
workspace.checkout = saved.checkout;
workspace.files[0] = saved.file;
let outcome = save(
&workspace,
0,
Resolution::Text("resolved\n".into()),
&|| false,
)
.unwrap();
assert_eq!(
outcome.file.status,
Status::Resolved,
"{:?}",
outcome.confirmation
);
assert_eq!(fs::read(fixture.0.join("file")).unwrap(), b"resolved\n");
assert!(
entries(&fixture.0, &outcome.checkout.commit, &|| false)
.unwrap()
.is_empty()
);
}
#[test]
fn all_three_sides_and_bases_are_loaded_and_directory_children_are_protected() {
let fixture = Fixture::new();
fixture.conflict();
fixture.jj(&["new", "base"]);
fs::write(fixture.0.join("file"), "third\n").unwrap();
fixture.jj(&["bookmark", "create", "third"]);
fixture.jj(&["new", "left", "right", "third"]);
let workspace = load(&fixture.0, &|| false).unwrap();
let file = &workspace.files[0];
assert_eq!(file.sources.iter().filter(|s| !s.base).count(), 3);
assert_eq!(file.sources.iter().filter(|s| s.base).count(), 2);
assert!(file.sources.iter().any(|s| s.bytes == b"third\n"));
let blocks = regions(file.result.as_ref().unwrap());
for source in &file.sources {
assert_eq!(blocks[0].choose(source).unwrap().as_bytes(), source.bytes);
}
fs::remove_file(fixture.0.join("file")).unwrap();
fs::create_dir(fixture.0.join("file")).unwrap();
fs::write(fixture.0.join("file/untracked"), b"keep my draft").unwrap();
let workspace = load(&fixture.0, &|| false).unwrap();
assert!(
save(&workspace, 0, Resolution::Source(0), &|| false)
.unwrap_err()
.contains("untracked or modified children")
);
assert_eq!(
fs::read(fixture.0.join("file/untracked")).unwrap(),
b"keep my draft"
);
}
#[test]
fn binary_symlink_and_deleted_file_choices_preserve_types_and_bytes() {
let fixture = Fixture::new();
fixture.conflict();
fixture.jj(&["edit", "left"]);
fs::write(fixture.0.join("binary"), b"left\0\xff").unwrap();
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(fixture.0.join("binary"), fs::Permissions::from_mode(0o755))
.unwrap();
}
std::os::unix::fs::symlink("left-target", fixture.0.join("link")).unwrap();
fixture.jj(&["new", "right"]);
fs::write(fixture.0.join("binary"), b"right\0\xfe").unwrap();
std::os::unix::fs::symlink("right-target", fixture.0.join("link")).unwrap();
fixture.jj(&["bookmark", "create", "right-extra"]);
fixture.jj(&["new", "left", "right-extra"]);
let mut workspace = load(&fixture.0, &|| false).unwrap();
for (name, kind, bytes) in [
("binary", FileKind::Binary, b"left\0\xff".as_slice()),
("link", FileKind::Symlink, b"left-target".as_slice()),
] {
let file = workspace
.files
.iter()
.position(|f| f.path == Path::new(name))
.unwrap();
assert!(workspace.files[file].result.is_none());
let source = workspace.files[file]
.sources
.iter()
.position(|s| !s.base && s.kind == kind && s.bytes == bytes)
.unwrap();
assert!(
save(
&workspace,
file,
Resolution::Text("not allowed".into()),
&|| false
)
.is_err()
);
let outcome = save(&workspace, file, Resolution::Source(source), &|| false).unwrap();
assert_eq!(
outcome.file.status,
Status::Resolved,
"{:?}",
outcome.confirmation
);
workspace.checkout = outcome.checkout;
workspace.files[file] = outcome.file;
if kind == FileKind::Symlink {
assert_eq!(
fs::read_link(fixture.0.join(name)).unwrap(),
Path::new("left-target")
);
} else {
assert_eq!(fs::read(fixture.0.join(name)).unwrap(), bytes);
}
if kind == FileKind::Binary {
use std::os::unix::fs::PermissionsExt;
assert_eq!(
fs::metadata(fixture.0.join(name))
.unwrap()
.permissions()
.mode()
& 0o111,
0o111
);
}
}
// add/add conflicts retain an absent base as an explicit deletion choice.
let file = workspace
.files
.iter()
.position(|f| f.path == Path::new("binary"))
.unwrap();
let source = workspace.files[file]
.sources
.iter()
.position(|s| s.kind == FileKind::Deleted)
.unwrap();
let outcome = save(&workspace, file, Resolution::Source(source), &|| false).unwrap();
assert_eq!(
outcome.file.status,
Status::Resolved,
"{:?}",
outcome.confirmation
);
assert!(!fixture.0.join("binary").exists());
}
#[test]
fn marker_styles_widths_multisides_and_missing_eol() {
let snapshot = "before\n<<<<<<<<<<< conflict 1 of 1\n+++++++++++ a\nleft\n----------- base\nold\n+++++++++++ b\nright\n----------- base2\nold2\n+++++++++++ c\nthird\n>>>>>>>>>>> conflict 1 of 1 ends\nafter\n";
let parsed = regions(snapshot);
assert_eq!(parsed[0].lines, 1..13);
assert_eq!(parsed[0].sides, ["left\n", "right\n", "third\n"]);
assert_eq!(parsed[0].bases, ["old\n", "old2\n"]);
let prefixed = format!("<<<<<<< literal marker\n{snapshot}");
assert_eq!(regions(&prefixed).len(), 1);
assert_eq!(regions(&prefixed)[0].lines, 2..14);
let diff = "<<<<<<< conflict 1 of 1\n%%%%%%% diff from: base\n^^^^^^^ to: left\n-old\n+left\n+++++++ right\nright\n>>>>>>> conflict 1 of 1 ends\n";
assert_eq!(regions(diff)[0].sides, ["left\n", "right\n"]);
let git = "<<<<<<< left\nleft\n||||||| base\nold\n=======\nright\n>>>>>>> right";
assert_eq!(regions(git)[0].sides, ["left", "right"]);
assert!(regions("<<<<<<< unfinished\nleft\n").is_empty());
}
}
#[path = "../tests/unit/resolve.rs"]
mod tests;
#[cfg(test)]
mod projection_tests {
use super::*;
fn test_sources(sides: usize) -> Vec<Source> {
(0..sides * 2 - 1)
.map(|index| Source {
label: format!("source {index}"),
base: index % 2 == 1,
kind: FileKind::Text,
bytes: b"historical full file; must never replace working text\n".to_vec(),
tree_id: String::new(),
term: index / 2,
sides,
bases: sides - 1,
node: Node::Absent,
})
.collect()
}
fn snapshot(left: &str, base: &str, right: &str) -> String {
format!(
"<<<<<<< conflict\n+++++++ side 1\n{left}------- base\n{base}+++++++ side 2\n{right}>>>>>>> conflict ends\n"
)
}
#[test]
fn current_working_projections_keep_partial_resolutions_and_multiple_regions() {
let sources = test_sources(2);
let raw = format!(
"unsaved heading\nalready manually resolved\n{}auto-merged middle\n<<<<<<< conflict\n%%%%%%% Changes from base to side 1\n\\\\\\\\\\\\\\ continuation\n-old two\n+left two\n+++++++ side 2\nright two\n>>>>>>> conflict ends\nunsaved tail",
snapshot("left one\n", "old one\n", "right one\n")
);
for (index, first, second) in [
(0, "left one", "left two"),
(1, "old one", "old two"),
(2, "right one", "right two"),
] {
assert_eq!(
project(&raw, &sources[index]).unwrap(),
format!(
"unsaved heading\nalready manually resolved\n{first}\nauto-merged middle\n{second}\nunsaved tail"
)
);
}
}
#[test]
fn alternatives_are_frozen_from_the_same_working_text_as_the_seed() {
let sources = test_sources(2);
let raw = format!(
"unsaved heading\n{}auto-merged middle\n{}unsaved tail",
snapshot("left-one\n", "old-one\n", "right-one\n"),
snapshot("left-two\n", "old-two\n", "right-two\n")
);
let projection = Projection::new(&raw, &sources, 0).unwrap();
let mut draft = projection.seed().to_owned();
draft = draft.replace("unsaved heading", "later draft edit");
draft.push_str("\nnew draft ending\n");
assert_eq!(
projection.alternative(2).unwrap(),
"unsaved heading\nright-one\nauto-merged middle\nright-two\nunsaved tail"
);
assert_eq!(
projection.alternative(1).unwrap(),
"unsaved heading\nold-one\nauto-merged middle\nold-two\nunsaved tail"
);
assert_eq!(
projection.seed(),
"unsaved heading\nleft-one\nauto-merged middle\nleft-two\nunsaved tail"
);
assert_eq!(
draft,
"later draft edit\nleft-one\nauto-merged middle\nleft-two\nunsaved tail\nnew draft ending\n"
);
}
#[test]
fn all_multiway_sides_and_bases_project_the_same_current_context() {
let sources = test_sources(3);
let raw = "unsaved α\n<<<<<<<<<<< conflict\n+++++++++++ side 1\none\n----------- base 1\nold one\n+++++++++++ side 2\ntwo\n----------- base 2\nold two\n+++++++++++ side 3\nthree\n>>>>>>>>>>> conflict ends\nunsaved ω";
let projection = Projection::new(raw, &sources, 0).unwrap();
for (index, choice) in ["one", "old one", "two", "old two", "three"]
.into_iter()
.enumerate()
{
let expected = format!("unsaved α\n{choice}\nunsaved ω");
assert_eq!(project(raw, &sources[index]).unwrap(), expected);
assert_eq!(projection.alternative(index).unwrap(), expected);
}
assert_eq!(
Projection::new(raw, &sources, 4).unwrap().seed(),
"unsaved α\nthree\nunsaved ω"
);
}
#[test]
fn projections_preserve_mixed_eof_and_crlf_terms() {
let sources = test_sources(2);
for eol in ["\n", "\r\n"] {
let choices = [
format!("left{eol}"),
"old".to_owned(),
format!("right{eol}{eol}"),
];
// jj appends one separator EOL to *each* term when any lacks an EOL,
// and omits the EOL on the final marker. Existing term EOLs remain.
let raw = format!(
"heading{eol}<<<<<<< conflict{eol}+++++++ side 1{eol}{}{eol}------- base{eol}{}{eol}+++++++ side 2{eol}{}{eol}>>>>>>> conflict ends",
choices[0], choices[1], choices[2]
);
let projection = Projection::new(&raw, &sources, 0).unwrap();
for (index, choice) in choices.iter().enumerate() {
assert_eq!(
project(&raw, &sources[index]).unwrap(),
format!("heading{eol}{choice}")
);
assert_eq!(
projection.alternative(index).unwrap(),
format!("heading{eol}{choice}")
);
}
}
}
#[test]
fn empty_choices_markerfree_text_and_invalid_choices_are_handled() {
let mut sources = test_sources(2);
sources[1].kind = FileKind::Deleted;
let raw = format!("before\n{}after\n", snapshot("", "", "new\n"));
let projection = Projection::new(&raw, &sources, 0).unwrap();
assert_eq!(projection.seed(), "before\nafter\n");
assert_eq!(projection.alternative(1).unwrap(), "before\nafter\n");
assert_eq!(projection.alternative(2).unwrap(), "before\nnew\nafter\n");
let resolved = Projection::new("manually resolved, no final EOL", &sources, 0).unwrap();
assert_eq!(
resolved.alternative(2).unwrap(),
"manually resolved, no final EOL"
);
assert!(resolved.alternative(sources.len()).is_err());
assert!(Projection::new(&raw, &sources, sources.len()).is_err());
assert!(Projection::new(&raw, &test_sources(3), 0).is_err());
sources[0].kind = FileKind::Binary;
assert!(project(&raw, &sources[0]).is_err());
}
}
#[path = "../tests/unit/resolve_projection.rs"]
mod projection_tests;