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()); }