use super::*;
use crate::working_copy::{WorkingCopy, WorkingCopyRead};
use std::io::Write;
#[test]
fn test() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
repo.add_file("dir/file", b"a\nb\nc\nd\n".to_vec());
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
txn.write().add_file("dir/file", 0)?;
let channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let _h0 = record_all(&repo, &changes, &txn, &channel, "")?;
use std::io::Write;
repo.write_file("dir/file", Inode::ROOT)?
.write_all(b"a\nx\nb\nd\n")?;
let h1 = record_all(&repo, &changes, &txn, &channel, "")?;
let _channel2 = txn
.write()
.fork(&channel, &SmallString::from_str("main2"))?;
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h1,
0,
&mut Default::default(),
)?;
let conflicts = output::output_repository_no_pending(
&repo, &changes, &txn, &channel, "", true, None, 1, 0,
)?;
if !conflicts.is_empty() {
panic!("conflicts = {:#?}", conflicts);
}
let mut buf = Vec::new();
repo.read_file("dir/file", &mut buf)?;
assert_eq!(std::str::from_utf8(&buf), Ok("a\nb\nc\nd\n"));
txn.commit()?;
Ok(())
}
#[test]
fn replace() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
repo.add_file("dir/file", b"a\nb\nc\nd\n".to_vec());
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
txn.write().add_file("dir/file", 0)?;
let channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let _h0 = record_all(&repo, &changes, &txn, &channel, "")?;
repo.write_file("dir/file", Inode::ROOT)?
.write_all(b"a\nx\ny\nd\n")?;
let h1 = record_all(&repo, &changes, &txn, &channel, "")?;
let _channel2 = txn
.write()
.fork(&channel, &SmallString::from_str("main2"))?;
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h1,
0,
&mut Default::default(),
)?;
let conflicts = output::output_repository_no_pending(
&repo, &changes, &txn, &channel, "", true, None, 1, 0,
)?;
if !conflicts.is_empty() {
panic!("conflicts = {:#?}", conflicts);
}
let mut buf = Vec::new();
repo.read_file("dir/file", &mut buf)?;
assert_eq!(std::str::from_utf8(&buf), Ok("a\nb\nc\nd\n"));
txn.commit()?;
Ok(())
}
#[test]
fn file_move() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
repo.add_file("file", b"a\nb\nc\nd\n".to_vec());
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
txn.write().add_file("file", 0)?;
let channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let _h0 = record_all(&repo, &changes, &txn, &channel, "")?;
repo.rename("file", "dir/file")?;
txn.write().move_file("file", "dir/file", 0)?;
debug!("recording the move");
let h1 = record_all(&repo, &changes, &txn, &channel, "")?;
debug!("unrecording the move");
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h1,
0,
&mut Default::default(),
)?;
assert_eq!(
crate::fs::iter_working_copy(&*txn.read(), Inode::ROOT)
.map(|n| n.unwrap().1)
.collect::<Vec<_>>(),
vec!["dir", "dir/file"]
);
assert_eq!(repo.list_files(), vec!["dir", "dir/file"]);
output::output_repository_no_pending(&repo, &changes, &txn, &channel, "", true, None, 1, 0)?;
assert_eq!(
crate::fs::iter_working_copy(&*txn.read(), Inode::ROOT)
.map(|n| n.unwrap().1)
.collect::<Vec<_>>(),
vec!["file"]
);
let mut files = repo.list_files();
files.sort();
assert_eq!(files, vec!["dir", "file"]);
txn.commit()?;
Ok(())
}
#[test]
fn reconnect_lines() -> Result<(), anyhow::Error> {
reconnect_(false)
}
#[test]
fn reconnect_files() -> Result<(), anyhow::Error> {
reconnect_(true)
}
fn reconnect_(delete_file: bool) -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let repo2 = working_copy::memory::Memory::new();
let repo3 = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
repo.add_file("file", b"a\nb\nc\nd\n".to_vec());
let env = pristine::sanakirja::Pristine::new_anon()?;
let env2 = pristine::sanakirja::Pristine::new_anon()?;
let env3 = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
let txn2 = env2.arc_txn_begin().unwrap();
let txn3 = env3.arc_txn_begin().unwrap();
txn.write().add_file("file", 0)?;
let channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let h0 = record_all(&repo, &changes, &txn, &channel, "")?;
let channel2 = txn2
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let channel3 = txn3
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
apply::apply_change_arc(&changes, &txn2, &channel2, &h0)?;
output::output_repository_no_pending(&repo2, &changes, &txn2, &channel2, "", true, None, 1, 0)?;
apply::apply_change_arc(&changes, &txn3, &channel3, &h0)?;
output::output_repository_no_pending(&repo3, &changes, &txn3, &channel3, "", true, None, 1, 0)?;
if delete_file {
repo.remove_path("file", false)?;
} else {
repo.write_file("file", Inode::ROOT)?.write_all(b"a\nd\n")?;
}
record_all_output(&repo, changes.clone(), &txn, &channel, "")?;
repo2
.write_file("file", Inode::ROOT)?
.write_all(b"a\nb\nx\nc\nd\n")?;
let h2 = record_all(&repo2, &changes, &txn2, &channel2, "")?;
repo2
.write_file("file", Inode::ROOT)?
.write_all(b"a\nb\nx\nc\ny\nd\n")?;
let h3 = record_all(&repo2, &changes, &txn2, &channel2, "")?;
apply::apply_change_arc(&changes, &txn, &channel, &h2)?;
apply::apply_change_arc(&changes, &txn, &channel, &h3)?;
output::output_repository_no_pending(&repo, &changes, &txn, &channel, "", true, None, 1, 0)?;
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h2,
0,
&mut Default::default(),
)?;
Ok(())
}
#[test]
fn zombie_file_test() -> Result<(), anyhow::Error> {
zombie_(None, true)
}
#[test]
fn zombie_file_rev() -> Result<(), anyhow::Error> {
zombie_(None, false)
}
#[test]
fn zombie_lines_test() -> Result<(), anyhow::Error> {
zombie_(Some(b"a\nd\n"), true)
}
#[test]
fn zombie_lines_rev() -> Result<(), anyhow::Error> {
zombie_(Some(b"a\nd\n"), false)
}
fn zombie_(file: Option<&[u8]>, order: bool) -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let repo2 = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
repo.add_file("file", b"a\nb\nc\nd\n".to_vec());
let env = pristine::sanakirja::Pristine::new_anon()?;
let env2 = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
let txn2 = env2.arc_txn_begin().unwrap();
txn.write().add_file("file", 0)?;
let channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let h0 = record_all(&repo, &changes, &txn, &channel, "")?;
let channel2 = txn2
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
apply::apply_change_arc(&changes, &txn2, &channel2, &h0)?;
output::output_repository_no_pending(&repo2, &changes, &txn2, &channel2, "", true, None, 1, 0)?;
if let Some(file) = file {
repo.write_file("file", Inode::ROOT)?.write_all(file)?;
} else {
repo.remove_path("file", false)?;
}
let h1 = record_all_output(&repo, changes.clone(), &txn, &channel, "")?;
debug!("h1 = {:?}", h1);
repo2
.write_file("file", Inode::ROOT)?
.write_all(b"a\nb\nx\nc\ny\nd\n")?;
let h2 = record_all_output(&repo2, changes.clone(), &txn2, &channel2, "")?;
debug!("h2 = {:?}", h2);
debug!("apply h2 to txn");
apply::apply_change_arc(&changes, &txn, &channel, &h2)?;
crate::pristine::debug(
&*txn.read(),
&*channel.read(),
std::fs::File::create("alice-conflict.dot").unwrap(),
)?;
if order {
debug!("unrecording h2 = {:?} from txn", h2);
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h2,
0,
&mut Default::default(),
)?;
check_unrec(&*txn.read(), &*channel.read(), h2.into());
} else {
debug!("unrecording h1 = {:?} from txn", h1);
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h1,
0,
&mut Default::default(),
)?;
check_unrec(&*txn.read(), &*channel.read(), h1.into());
}
crate::pristine::debug(
&*txn.read(),
&*channel.read(),
std::fs::File::create("alice-unrec.dot").unwrap(),
)?;
let conflicts = output::output_repository_no_pending(
&repo, &changes, &txn, &channel, "", true, None, 1, 0,
)?;
let mut buf = Vec::new();
if let Some(f) = file {
if !conflicts.is_empty() {
panic!("conflicts = {:#?}", conflicts)
}
repo.read_file("file", &mut buf)?;
if order {
assert_eq!(&buf[..], f);
} else {
assert_eq!(&buf[..], b"a\nb\nx\nc\ny\nd\n");
}
} else {
if !conflicts.is_empty() {
panic!("conflicts = {:#?}", conflicts)
}
}
let (alive_, reachable_) = check_alive(&*txn.read(), &channel.read());
if !alive_.is_empty() {
panic!("alive: {:?}", alive_);
}
if !reachable_.is_empty() {
panic!("reachable: {:?}", reachable_);
}
txn.commit()?;
debug!("apply h1 to txn2");
apply::apply_change_arc(&changes, &txn2, &channel2, &h1)?;
crate::pristine::debug(
&*txn2.read(),
&*channel2.read(),
std::fs::File::create("bob-conflict.dot").unwrap(),
)?;
if order {
debug!("unrecording h1 = {:?} from txn2", h1);
crate::unrecord::unrecord(
&mut *txn2.write(),
&channel2,
&changes,
&h1,
0,
&mut Default::default(),
)?;
} else {
debug!("unrecording h2 = {:?} from txn2", h2);
crate::unrecord::unrecord(
&mut *txn2.write(),
&channel2,
&changes,
&h2,
0,
&mut Default::default(),
)?;
}
crate::pristine::debug(
&*txn2.read(),
&*channel2.read(),
std::fs::File::create("bob-unrec.dot").unwrap(),
)?;
let conflicts = output::output_repository_no_pending(
&repo, &changes, &txn2, &channel, "", true, None, 1, 0,
)?;
if !conflicts.is_empty() {
panic!("conflicts = {:#?}", conflicts)
}
{
let (alive_, reachable_) = check_alive(&*txn2.read(), &channel2.read());
if !alive_.is_empty() {
panic!("alive: {:?}", alive_);
}
if !reachable_.is_empty() {
panic!("reachable: {:?}", reachable_);
}
}
Ok(())
}
fn check_unrec<T: crate::pristine::GraphIter + crate::pristine::ChannelTxnT>(
txn: &T,
channel: &T::Channel,
h: Hash,
) {
let h = txn.get_internal(&h.into()).unwrap();
for c in txn.iter_graph(txn.graph(channel), None).unwrap() {
let (v, e) = c.unwrap();
debug!("{:?} {:?}", v, e);
if let Some(h) = h {
assert!(v.change != *h);
assert!(e.dest().change != *h);
assert!(e.introduced_by() != *h);
} else {
if !v.change.is_root() {
txn.get_external(&v.change).unwrap();
txn.get_changeset(txn.changes(channel), &v.change)
.unwrap()
.unwrap();
}
if !e.dest().change.is_root() {
txn.get_external(&e.dest().change).unwrap();
txn.get_changeset(txn.changes(channel), &e.dest().change)
.unwrap()
.unwrap();
}
if !e.introduced_by().is_root() {
txn.get_external(&e.introduced_by()).unwrap();
txn.get_changeset(txn.changes(channel), &e.introduced_by())
.unwrap()
.unwrap();
}
}
}
}
fn canon_content(o: &[u8]) -> Vec<Vec<u8>> {
let mut v: Vec<Vec<u8>> = o
.split(|&b| b == b'\n')
.filter(|l| {
!l.starts_with(b">>>>>>>") && !l.starts_with(b"=======") && !l.starts_with(b"<<<<<<<")
})
.map(|l| l.to_vec())
.collect();
v.sort();
v
}
#[test]
fn zombie_unrec() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let repo2 = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
repo.add_file("file", b"a\nb\nc\nd\n".to_vec());
let env = pristine::sanakirja::Pristine::new_anon()?;
let env2 = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
let txn2 = env2.arc_txn_begin().unwrap();
txn.write().add_file("file", 0)?;
let channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let h0 = record_all(&repo, &changes, &txn, &channel, "")?;
let channel2 = txn2
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
apply::apply_change_arc(&changes, &txn2, &channel2, &h0)?;
output::output_repository_no_pending(&repo2, &changes, &txn2, &channel2, "", true, None, 1, 0)?;
repo.write_file("file", Inode::ROOT)?
.write_all(b"a\ny\nd\n")?;
let h1 = record_all_output(&repo, changes.clone(), &txn, &channel, "")?;
debug!("h1 = {:?}", h1);
repo2
.write_file("file", Inode::ROOT)?
.write_all(b"a\nb\nx\nc\nd\n")?;
let h2 = record_all_output(&repo2, changes.clone(), &txn2, &channel2, "")?;
debug!("h2 = {:?}", h2);
debug!("apply h2 to txn");
apply::apply_change_arc(&changes, &txn, &channel, &h2)?;
crate::pristine::debug(
&*txn.read(),
&*channel.read(),
std::fs::File::create("alice-conflict.dot").unwrap(),
)?;
debug!("unrecording h2 = {:?} from txn", h2);
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h2,
0,
&mut Default::default(),
)?;
check_unrec(&*txn.read(), &*channel.read(), h2.into());
crate::pristine::debug(
&*txn.read(),
&*channel.read(),
std::fs::File::create("alice-unrec.dot").unwrap(),
)?;
Ok(())
}
#[test]
fn zombie_dir() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
repo.add_file("a/b/c/d", b"a\nb\nc\nd\n".to_vec());
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
txn.write().add_file("a/b/c/d", 0)?;
let channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
record_all(&repo, &changes, &txn, &channel, "")?;
repo.remove_path("a/b/c/d", false)?;
let h1 = record_all_output(&repo, changes.clone(), &txn, &channel, "")?;
repo.remove_path("a/b", true)?;
let _h2 = record_all_output(&repo, changes.clone(), &txn, &channel, "")?;
output::output_repository_no_pending(&repo, &changes, &txn, &channel, "", true, None, 1, 0)?;
let files = repo.list_files();
assert_eq!(files, &["a"]);
debug!("files={:?}", files);
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h1,
0,
&mut Default::default(),
)?;
let _conflicts = output::output_repository_no_pending(
&repo, &changes, &txn, &channel, "", true, None, 1, 0,
)?
.into_iter()
.collect::<Vec<_>>();
let files = repo.list_files();
debug!("files={:?}", files);
assert_eq!(files, &["a", "a/b", "a/b/c", "a/b/c/d"]);
let (alive_, reachable_) = check_alive(&*txn.read(), &channel.read());
if !alive_.is_empty() {
panic!("alive: {:?}", alive_);
}
if !reachable_.is_empty() {
panic!("reachable: {:?}", reachable_);
}
txn.commit()?;
Ok(())
}
#[test]
fn nodep() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
repo.add_file("dir/file", b"a\nb\nc\nd\n".to_vec());
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
txn.write().add_file("dir/file", 0)?;
debug_inodes(&*txn.read());
let channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let h0 = record_all(&repo, &changes, &txn, &channel, "")?;
repo.write_file("dir/file", Inode::ROOT)?
.write_all(b"a\nx\nb\nd\n")?;
let h1 = record_all(&repo, &changes, &txn, &channel, "")?;
debug_inodes(&*txn.read());
match crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h0,
0,
&mut Default::default(),
) {
Err(crate::unrecord::UnrecordError::ChangeIsDependedUpon { .. }) => {}
_ => panic!("Should not be able to unrecord"),
}
debug_inodes(&*txn.read());
let channel2 = txn
.write()
.open_or_create_channel(&SmallString::from_str("main2"))?;
match crate::unrecord::unrecord(
&mut *txn.write(),
&channel2,
&changes,
&h0,
0,
&mut Default::default(),
) {
Err(crate::unrecord::UnrecordError::ChangeNotInChannel { .. }) => {}
_ => panic!("Should not be able to unrecord"),
}
for p in txn.read().log(&*channel.read(), 0).unwrap() {
debug!("p = {:?}", p);
}
debug_inodes(&*txn.read());
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h1,
0,
&mut Default::default(),
)?;
for p in txn.read().log(&*channel.read(), 0).unwrap() {
debug!("p = {:?}", p);
}
debug_inodes(&*txn.read());
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h0,
0,
&mut Default::default(),
)?;
output::output_repository_no_pending(&repo, &changes, &txn, &channel, "", true, None, 1, 0)?;
let mut files = repo.list_files();
files.sort();
assert_eq!(files, &["dir", "dir/file"]);
assert!(
crate::fs::iter_working_copy(&*txn.read(), Inode::ROOT)
.next()
.is_none()
);
txn.commit()?;
Ok(())
}
#[test]
fn file_del() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
let channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
repo.add_file("file", b"blabla".to_vec());
txn.write().add_file("file", 0)?;
let h0 = record_all(&repo, &changes, &txn, &channel, "")?;
repo.remove_path("file", false)?;
let h = record_all(&repo, &changes, &txn, &channel, "")?;
debug!("unrecord h");
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h,
0,
&mut Default::default(),
)?;
let conflicts = output::output_repository_no_pending(
&repo, &changes, &txn, &channel, "", true, None, 1, 0,
)?;
if !conflicts.is_empty() {
panic!("conflicts = {:#?}", conflicts);
}
assert_eq!(repo.list_files(), vec!["file"]);
debug!("unrecord h0");
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h0,
0,
&mut Default::default(),
)?;
let conflicts = output::output_repository_no_pending(
&repo, &changes, &txn, &channel, "", true, None, 1, 0,
)?;
if !conflicts.is_empty() {
panic!("conflicts = {:#?}", conflicts);
}
let files = repo.list_files();
assert_eq!(files, &["file"]);
txn.commit()?;
Ok(())
}
#[test]
fn self_context() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
let mut channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
repo.add_file("file", b"a\nb\n".to_vec());
txn.write().add_file("file", 0)?;
record_all(&repo, &changes, &txn, &channel, "")?;
let channel2 = txn
.write()
.fork(&channel, &SmallString::from_str("main2"))?;
repo.write_file("file", Inode::ROOT)?
.write_all(b"a\nx\nb\n")?;
record_all(&repo, &changes, &txn, &channel, "")?;
repo.write_file("file", Inode::ROOT)?
.write_all(b"a\ny\nb\n")?;
let b = record_all(&repo, &changes, &txn, &channel2, "")?;
apply::apply_change_arc(&changes, &txn, &channel, &b)?;
let conflicts = output::output_repository_no_pending(
&repo, &changes, &txn, &channel, "", true, None, 1, 0,
)?;
debug!("conflicts = {:#?}", conflicts);
let mut buf = Vec::new();
repo.read_file("file", &mut buf)?;
debug!("buf = {:?}", std::str::from_utf8(&buf));
assert_eq!(conflicts.len(), 1);
match conflicts.iter().next().unwrap() {
Conflict::Order { .. } => {}
ref c => panic!("c = {:?}", c),
}
let mut buf = Vec::new();
repo.read_file("file", &mut buf)?;
let conflict: Vec<_> = std::str::from_utf8(&buf)?.lines().collect();
{
let mut w = repo.write_file("file", Inode::ROOT)?;
for l in conflict.iter() {
if l.starts_with(">>>") {
writeln!(w, "bla\n{}\nbli", l)?
} else {
writeln!(w, "{}", l)?
}
}
}
let c = record_all(&repo, &changes, &txn, &channel, "")?;
crate::unrecord::unrecord(
&mut *txn.write(),
&mut channel,
&changes,
&c,
0,
&mut Default::default(),
)?;
let conflicts = output::output_repository_no_pending(
&repo, &changes, &txn, &channel, "", true, None, 1, 0,
)?;
debug!("conflicts = {:#?}", conflicts);
assert_eq!(conflicts.len(), 1);
match conflicts.iter().next().unwrap() {
Conflict::Order { .. } => {}
ref c => panic!("c = {:?}", c),
}
let mut buf = Vec::new();
repo.read_file("file", &mut buf)?;
let re = regex::bytes::Regex::new(r#" \[[^\]]*\]"#).unwrap();
let buf_ = re.replace_all(&buf, &[][..]);
let mut conflict: Vec<_> = std::str::from_utf8(&buf_)?.lines().collect();
conflict.sort();
assert_eq!(
conflict,
vec!["<<<<<<< 1", "======= 1", ">>>>>>> 1", "a", "b", "x", "y"]
);
txn.commit()?;
Ok(())
}
#[test]
fn rollback_lines() -> Result<(), anyhow::Error> {
rollback_(false)
}
#[test]
fn rollback_file() -> Result<(), anyhow::Error> {
rollback_(true)
}
fn rollback_(delete_file: bool) -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
let channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
repo.add_file("file", b"a\nb\nc\n".to_vec());
txn.write().add_file("file", 0)?;
record_all(&repo, &changes, &txn, &channel, "")?;
if delete_file {
repo.remove_path("file", false)?
} else {
repo.write_file("file", Inode::ROOT)?.write_all(b"a\nd\n")?;
}
let h_del = record_all(&repo, &changes, &txn, &channel, "")?;
let p_del = changes.get_change(&h_del)?;
debug!("p_del = {:#?}", p_del);
let mut p_inv = p_del.inverse(
&h_del,
crate::change::ChangeHeader {
authors: vec![],
message: "rollback".to_string(),
description: None,
timestamp: jiff::Timestamp::now(),
},
Vec::new(),
);
let h_inv = changes.save_change(&mut p_inv, |_, _| Ok::<_, anyhow::Error>(()))?;
apply::apply_change_arc(&changes, &txn, &channel, &h_inv)?;
let conflicts = output::output_repository_no_pending(
&repo, &changes, &txn, &channel, "", true, None, 1, 0,
)?;
if !conflicts.is_empty() {
panic!("conflicts = {:#?}", conflicts)
}
let mut buf = Vec::new();
repo.read_file("file", &mut buf)?;
assert_eq!(std::str::from_utf8(&buf), Ok("a\nb\nc\n"));
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h_inv,
0,
&mut Default::default(),
)?;
let conflicts = output::output_repository_no_pending(
&repo, &changes, &txn, &channel, "", true, None, 1, 0,
)?;
if !conflicts.is_empty() {
panic!("conflicts = {:#?}", conflicts)
}
let mut buf = Vec::new();
repo.read_file("file", &mut buf).unwrap();
if delete_file {
assert_eq!(std::str::from_utf8(&buf), Ok("a\nb\nc\n"));
} else {
assert_eq!(std::str::from_utf8(&buf), Ok("a\nd\n"));
}
txn.commit()?;
Ok(())
}
#[test]
fn double_test() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
let channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let channel2 = txn
.write()
.open_or_create_channel(&SmallString::from_str("main2"))?;
repo.add_file("file", b"blabla\nblibli\nblublu\n".to_vec());
txn.write().add_file("file", 0)?;
let h0 = record_all(&repo, &changes, &txn, &channel, "")?;
debug!("h0 = {:?}", h0);
apply::apply_change_arc(&changes, &txn, &channel2, &h0)?;
{
let mut w = repo.write_file("file", Inode::ROOT)?;
writeln!(w, "blabla\nblublu")?;
}
let h1 = record_all(&repo, &changes, &txn, &channel, "")?;
debug!("h1 = {:?}", h1);
let h2 = record_all(&repo, &changes, &txn, &channel2, "")?;
debug!("h2 = {:?}", h2);
debug!("applying");
apply::apply_change_arc(&changes, &txn, &channel, &h2)?;
debug!("unrecord h");
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h2,
0,
&mut Default::default(),
)?;
let conflicts = output::output_repository_no_pending(
&repo, &changes, &txn, &channel, "", true, None, 1, 0,
)?;
if !conflicts.is_empty() {
panic!("conflicts = {:#?}", conflicts);
}
txn.commit()?;
Ok(())
}
#[test]
fn double_convoluted() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
let mut channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let mut channel2 = txn
.write()
.open_or_create_channel(&SmallString::from_str("main2"))?;
repo.add_file("file", b"blabla\nblibli\nblublu\n".to_vec());
txn.write().add_file("file", 0)?;
let h0 = record_all(&repo, &changes, &txn, &channel, "")?;
debug!("h0 = {:?}", h0);
apply::apply_change_arc(&changes, &txn, &channel2, &h0)?;
{
let mut w = repo.write_file("file", Inode::ROOT)?;
write!(w, "blabla\nblibli\n")?;
}
let h1 = record_all(&repo, &changes, &txn, &channel, "")?;
debug!("h1 = {:?}", h1);
{
let mut w = repo.write_file("file", Inode::ROOT)?;
writeln!(w, "blabla")?;
}
let h2 = record_all(&repo, &changes, &txn, &channel2, "")?;
debug!("h2 = {:?}", h2);
debug!("applying");
apply::apply_change_arc(&changes, &txn, &channel, &h2)?;
debug!("unrecord h");
crate::unrecord::unrecord(
&mut *txn.write(),
&mut channel,
&changes,
&h2,
0,
&mut Default::default(),
)?;
let conflicts = output::output_repository_no_pending(
&repo, &changes, &txn, &channel, "", true, None, 1, 0,
)?;
if !conflicts.is_empty() {
panic!("conflicts = {:#?}", conflicts);
}
debug!("rolling back");
apply::apply_change_arc(&changes, &txn, &channel2, &h1)?;
let rollback = |h| {
let p = changes.get_change(&h).unwrap();
let mut p_inv = p.inverse(
&h,
crate::change::ChangeHeader {
authors: vec![],
message: "rollback".to_string(),
description: None,
timestamp: jiff::Timestamp::now(),
},
Vec::new(),
);
let h_inv = changes
.save_change(&mut p_inv, |_, _| Ok::<_, anyhow::Error>(()))
.unwrap();
h_inv
};
let mut h = h2;
for _i in 0..6 {
let r = rollback(h);
apply::apply_change_arc(&changes, &txn, &channel2, &r).unwrap();
h = r
}
crate::unrecord::unrecord(
&mut *txn.write(),
&mut channel2,
&changes,
&h1,
0,
&mut Default::default(),
)?;
let conflicts = output::output_repository_no_pending(
&repo, &changes, &txn, &channel, "", true, None, 1, 0,
)?;
if !conflicts.is_empty() {
panic!("conflicts = {:#?}", conflicts)
}
txn.commit()?;
Ok(())
}
#[test]
fn double_file() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
let channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let channel2 = txn
.write()
.open_or_create_channel(&SmallString::from_str("main2"))?;
repo.add_file("file", b"blabla\nblibli\nblublu\n".to_vec());
txn.write().add_file("file", 0)?;
let h0 = record_all(&repo, &changes, &txn, &channel, "")?;
debug!("h0 = {:?}", h0);
apply::apply_change_arc(&changes, &txn, &channel2, &h0)?;
repo.remove_path("file", false)?;
let h1 = record_all(&repo, &changes, &txn, &channel, "")?;
debug!("h1 = {:?}", h1);
let h2 = record_all(&repo, &changes, &txn, &channel2, "")?;
debug!("h2 = {:?}", h2);
debug!("applying");
apply::apply_change_arc(&changes, &txn, &channel, &h2)?;
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h1,
0,
&mut Default::default(),
)?;
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&h2,
0,
&mut Default::default(),
)?;
let txn = txn.read();
let mut inodes = txn.iter_inodes().unwrap();
let (x, _) = inodes.next().unwrap().unwrap();
assert!(x.is_root());
assert!(inodes.next().is_some());
assert!(inodes.next().is_none());
Ok(())
}
#[test]
fn fs_times() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
let channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
repo.add_file("file", b"blabla\nblibli\nblublu\n".to_vec());
repo.add_file("file2", b"blabla\nblibli\nblublu\n".to_vec());
txn.write().add_file("file", 0)?;
txn.write().add_file("file2", 0)?;
let h0 = record_all(&repo, &changes, &txn, &channel, "")?;
debug!("h0 = {:?}", h0);
repo.write_file("file", Inode::ROOT)?
.write_all(b"blabla\nblublu\n")?;
let h1 = record_all(&repo, &changes, &txn, &channel, "")?;
txn.write().touch_channel(&mut *channel.write(), None);
debug!("last {:?}", txn.read().last_modified(&channel.r.read()));
repo.write_file("file2", Inode::ROOT)?
.write_all(b"blabla\nblublu\n")?;
repo.touch(
"file2",
std::time::SystemTime::now() - std::time::Duration::from_hours(1),
)?;
let mut touched = crate::unrecord::TouchedInodes::new();
crate::unrecord::unrecord(&mut *txn.write(), &channel, &changes, &h1, 0, &mut touched)?;
crate::unrecord::touch_inodes(&mut *txn.write(), &repo, &touched)?;
debug!("last {:?}", txn.read().last_modified(&channel.r.read()));
info!("Final record");
let h1 = record_all(&repo, &changes, &txn, &channel, "")?;
let change1 = changes.get_change(&h1).unwrap();
assert_eq!(change1.changes.len(), 1);
Ok(())
}
use rand::{RngExt, SeedableRng};
use rand_chacha::ChaCha20Rng;
struct FuzzChan<T: crate::pristine::ChannelTxnT> {
channel: ChannelRef<T>,
applied: Vec<Hash>,
}
fn fuzz_render<T>(
changes: &changestore::memory::Memory,
txn: &ArcTxn<T>,
channel: &ChannelRef<T>,
) -> Result<(Vec<u8>, usize), anyhow::Error>
where
T: crate::pristine::ChannelMutTxnT
+ crate::pristine::TreeMutTxnT<TreeError = <T as crate::pristine::GraphTxnT>::GraphError>
+ Send
+ Sync
+ 'static,
T::Channel: Send + Sync + 'static,
{
let wc = working_copy::memory::Memory::new();
let conflicts =
output::output_repository_no_pending(&wc, changes, txn, channel, "", true, None, 1, 0)?;
let mut buf = Vec::new();
let _ = wc.read_file("file", &mut buf);
Ok((buf, conflicts.len()))
}
fn fuzz_deps_satisfied(changes: &changestore::memory::Memory, applied: &[Hash], c: &Hash) -> bool {
if applied.contains(c) {
return false;
}
match changes.get_change(c) {
Ok(ch) => ch.dependencies.iter().all(|d| applied.contains(d)),
Err(_) => false,
}
}
fn fuzz_has_dependent(changes: &changestore::memory::Memory, applied: &[Hash], c: &Hash) -> bool {
applied
.iter()
.any(|g| g != c && matches!(changes.get_change(g), Ok(gc) if gc.dependencies.contains(c)))
}
fn fuzz_valid_order(
changes: &changestore::memory::Memory,
subset: &[Hash],
reverse: bool,
) -> Vec<Hash> {
let mut order: Vec<Hash> = Vec::new();
let mut remaining: Vec<Hash> = subset.to_vec();
while !remaining.is_empty() {
let n = remaining.len();
let idxs: Vec<usize> = if reverse {
(0..n).rev().collect()
} else {
(0..n).collect()
};
let mut pick = None;
for i in idxs {
if fuzz_deps_satisfied(changes, &order, &remaining[i]) {
pick = Some(i);
break;
}
}
match pick {
Some(i) => order.push(remaining.remove(i)),
None => order.extend(remaining.drain(..)),
}
}
order
}
fn fuzz_apply_count<T>(
changes: &changestore::memory::Memory,
txn: &ArcTxn<T>,
order: &[Hash],
name: &str,
) -> Result<usize, anyhow::Error>
where
T: crate::pristine::MutTxnT
+ crate::pristine::ChannelMutTxnT
+ crate::pristine::TreeMutTxnT<TreeError = <T as crate::pristine::GraphTxnT>::GraphError>
+ Send
+ Sync
+ 'static,
T::Channel: Send + Sync + 'static,
{
let ch = txn
.write()
.open_or_create_channel(&SmallString::from_str(name))?;
for h in order {
apply::apply_change_arc(changes, txn, &ch, h)?;
}
Ok(fuzz_render(changes, txn, &ch)?.1)
}
fn fuzz_mutate(content: &[u8], rng: &mut ChaCha20Rng) -> Vec<u8> {
let mut lines: Vec<Vec<u8>> = content.split(|&b| b == b'\n').map(|s| s.to_vec()).collect();
if lines.last().map_or(false, |l| l.is_empty()) {
lines.pop();
}
let nops = rng.random_range(1..=3);
for _ in 0..nops {
if lines.is_empty() {
lines.push(format!("L{}", rng.random_range(0..6)).into_bytes());
continue;
}
match rng.random_range(0..3) {
0 => {
let p = rng.random_range(0..=lines.len());
lines.insert(p, format!("L{}", rng.random_range(0..6)).into_bytes());
}
1 => {
let p = rng.random_range(0..lines.len());
lines.remove(p);
}
_ => {
let p = rng.random_range(0..lines.len());
lines[p] = format!("L{}", rng.random_range(0..6)).into_bytes();
}
}
}
let mut out = Vec::new();
for l in &lines {
out.extend_from_slice(l);
out.push(b'\n');
}
out
}
fn fuzz_seed(seed: u64, nsteps: usize, verbose: bool) -> Result<(), anyhow::Error> {
if let Ok(t) = std::env::var("FUZZ_FIXED_TIME") {
let base: i64 = t.parse().unwrap_or(0);
crate::tests::fuzz_clock_set(base.wrapping_add((seed as i64).wrapping_mul(10_000_000)));
}
let mut rng = ChaCha20Rng::seed_from_u64(seed);
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
let changes = changestore::memory::Memory::new();
let base_wc = working_copy::memory::Memory::new();
base_wc.add_file("file", b"L0\nL1\nL2\nL3\n".to_vec());
txn.write().add_file("file", 0)?;
let c0 = txn
.write()
.open_or_create_channel(&SmallString::from_str("c0"))?;
let h0 = record_all(&base_wc, &changes, &txn, &c0, "")?;
let nchan = 3;
let mut chans: Vec<FuzzChan<_>> = Vec::new();
chans.push(FuzzChan {
channel: c0,
applied: vec![h0],
});
for i in 1..nchan {
let ch = txn.write().fork(
&chans[0].channel,
&SmallString::from_str(&format!("c{}", i)),
)?;
chans.push(FuzzChan {
channel: ch,
applied: vec![h0],
});
}
let mut oracle_ctr = 0u64;
for step in 0..nsteps {
match rng.random_range(0..100) {
op if op < 30 => {
let ci = rng.random_range(0..chans.len());
let wc = working_copy::memory::Memory::new();
output::output_repository_no_pending(
&wc,
&changes,
&txn,
&chans[ci].channel,
"",
true,
None,
1,
0,
)?;
let mut buf = Vec::new();
let _ = wc.read_file("file", &mut buf);
let newc = fuzz_mutate(&buf, &mut rng);
if newc == buf {
continue;
}
wc.write_file("file", Inode::ROOT)?.write_all(&newc)?;
let h = record_all(&wc, &changes, &txn, &chans[ci].channel, "")?;
chans[ci].applied.push(h);
if verbose {
eprintln!(
"[{step}] RECORD on c{ci} -> {} : {:?}",
h.to_base32(),
String::from_utf8_lossy(&newc)
);
}
}
op if op < 55 => {
let mut all: Vec<Hash> = Vec::new();
for c in &chans {
for h in &c.applied {
if !all.contains(h) {
all.push(*h);
}
}
}
if all.is_empty() {
continue;
}
let c = all[rng.random_range(0..all.len())];
let ci = rng.random_range(0..chans.len());
if fuzz_deps_satisfied(&changes, &chans[ci].applied, &c) {
apply::apply_change_arc(&changes, &txn, &chans[ci].channel, &c)?;
chans[ci].applied.push(c);
if verbose {
eprintln!("[{step}] APPLY {} to c{ci}", c.to_base32());
}
}
}
op if op < 80 => {
let mut all: Vec<Hash> = Vec::new();
for c in &chans {
for h in &c.applied {
if !all.contains(h) {
all.push(*h);
}
}
}
if all.is_empty() {
continue;
}
let c = all[rng.random_range(0..all.len())];
let ci = rng.random_range(0..chans.len());
if !fuzz_deps_satisfied(&changes, &chans[ci].applied, &c) {
continue;
}
let dump = std::env::var("FUZZ_DUMP").is_ok();
if dump {
crate::pristine::debug(
&*txn.read(),
&*chans[ci].channel.read(),
std::fs::File::create("rt-before.dot").unwrap(),
)?;
}
let before = fuzz_render(&changes, &txn, &chans[ci].channel)?;
apply::apply_change_arc(&changes, &txn, &chans[ci].channel, &c)?;
if dump {
crate::pristine::debug(
&*txn.read(),
&*chans[ci].channel.read(),
std::fs::File::create("rt-applied.dot").unwrap(),
)?;
}
crate::unrecord::unrecord(
&mut *txn.write(),
&chans[ci].channel,
&changes,
&c,
0,
&mut Default::default(),
)?;
let after = fuzz_render(&changes, &txn, &chans[ci].channel)?;
let canon = |o: &[u8]| -> Vec<Vec<u8>> {
let mut v: Vec<Vec<u8>> = o
.split(|&b| b == b'\n')
.filter(|l| {
!l.starts_with(b">>>>>>>")
&& !l.starts_with(b"=======")
&& !l.starts_with(b"<<<<<<<")
})
.map(|l| l.to_vec())
.collect();
v.sort();
v
};
if canon(&before.0) != canon(&after.0) {
if dump {
crate::pristine::debug(
&*txn.read(),
&*chans[ci].channel.read(),
std::fs::File::create("rt-after.dot").unwrap(),
)?;
}
anyhow::bail!(
"seed {seed} step {step}: apply/unrecord round-trip changed c{ci}\n\
change = {}\n\
before = {:?} ({} conflicts)\n\
after = {:?} ({} conflicts)",
c.to_base32(),
String::from_utf8_lossy(&before.0),
before.1,
String::from_utf8_lossy(&after.0),
after.1,
);
}
if verbose {
eprintln!("[{step}] PROBE round-trip {} on c{ci} ok", c.to_base32());
}
}
_ => {
let ci = rng.random_range(0..chans.len());
let cand: Vec<Hash> = chans[ci]
.applied
.iter()
.skip(1) .filter(|c| !fuzz_has_dependent(&changes, &chans[ci].applied, c))
.cloned()
.collect();
if cand.is_empty() {
continue;
}
let c = cand[rng.random_range(0..cand.len())];
crate::unrecord::unrecord(
&mut *txn.write(),
&chans[ci].channel,
&changes,
&c,
0,
&mut Default::default(),
)?;
chans[ci].applied.retain(|x| *x != c);
check_unrec(&*txn.read(), &*chans[ci].channel.read(), c);
oracle_ctr += 1;
let fresh =
txn.write()
.open_or_create_channel(&SmallString::from_str(&format!(
"oracle-{oracle_ctr}"
)))?;
if std::env::var("SPLIT_TRACE").is_ok() {
eprintln!("=== REPLAY START (step {step}) ===");
}
for h in chans[ci].applied.clone() {
apply::apply_change_arc(&changes, &txn, &fresh, &h)?;
}
let got = fuzz_render(&changes, &txn, &chans[ci].channel)?;
let exp = fuzz_render(&changes, &txn, &fresh)?;
let canon = |o: &[u8]| -> Vec<Vec<u8>> {
let mut v: Vec<Vec<u8>> = o
.split(|&b| b == b'\n')
.filter(|l| {
!l.starts_with(b">>>>>>>")
&& !l.starts_with(b"=======")
&& !l.starts_with(b"<<<<<<<")
})
.map(|l| l.to_vec())
.collect();
v.sort();
v
};
if got != exp {
eprintln!(
"[seed {seed} step {step}] unrecord divergence is {}",
if canon(&got.0) != canon(&exp.0) {
"CONTENT (real)"
} else {
"conflict-order only (benign)"
}
);
}
if std::env::var("FUZZ_COMMUTE").is_ok() {
let survivors = chans[ci].applied.clone();
let mut alt: Vec<Hash> = Vec::new();
let mut remaining = survivors.clone();
while !remaining.is_empty() {
let mut pick = None;
for i in (0..remaining.len()).rev() {
if fuzz_deps_satisfied(&changes, &alt, &remaining[i]) {
pick = Some(i);
break;
}
}
alt.push(remaining.remove(pick.expect("dep cycle")));
}
if alt != survivors {
oracle_ctr += 1;
let fresh2 = txn.write().open_or_create_channel(&SmallString::from_str(
&format!("commute-{oracle_ctr}"),
))?;
for h in &alt {
apply::apply_change_arc(&changes, &txn, &fresh2, h)?;
}
let exp2 = fuzz_render(&changes, &txn, &fresh2)?;
let canon = |o: &[u8]| -> Vec<Vec<u8>> {
let mut v: Vec<Vec<u8>> = o
.split(|&b| b == b'\n')
.filter(|l| {
!l.starts_with(b">>>>>>>")
&& !l.starts_with(b"=======")
&& !l.starts_with(b"<<<<<<<")
})
.map(|l| l.to_vec())
.collect();
v.sort();
v
};
if canon(&exp.0) != canon(&exp2.0) || exp.1 != exp2.1 {
let mut minimal = survivors.clone();
let mut tag = 1_000_000usize;
loop {
let mut reduced = false;
for i in 0..minimal.len() {
let removed = minimal[i];
let cand: Vec<Hash> = minimal
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(_, h)| *h)
.collect();
if cand.iter().any(|h| {
matches!(changes.get_change(h), Ok(c) if c.dependencies.contains(&removed))
}) {
continue;
}
let fwd = fuzz_valid_order(&changes, &cand, false);
let rev = fuzz_valid_order(&changes, &cand, true);
if fwd == rev {
continue;
}
tag += 1;
let ca = fuzz_apply_count(
&changes,
&txn,
&fwd,
&format!("shrA{tag}"),
)?;
tag += 1;
let cb = fuzz_apply_count(
&changes,
&txn,
&rev,
&format!("shrB{tag}"),
)?;
if ca != cb {
minimal = cand;
reduced = true;
break;
}
}
if !reduced {
break;
}
}
let mfwd = fuzz_valid_order(&changes, &minimal, false);
let mrev = fuzz_valid_order(&changes, &minimal, true);
tag += 1;
let fa = txn.write().open_or_create_channel(&SmallString::from_str(
&format!("mfa{tag}"),
))?;
for h in &mfwd {
apply::apply_change_arc(&changes, &txn, &fa, h)?;
}
let oa = fuzz_render(&changes, &txn, &fa)?;
tag += 1;
let fb = txn.write().open_or_create_channel(&SmallString::from_str(
&format!("mfb{tag}"),
))?;
for h in &mrev {
apply::apply_change_arc(&changes, &txn, &fb, h)?;
}
let ob = fuzz_render(&changes, &txn, &fb)?;
if std::env::var("FUZZ_DUMP").is_ok() {
crate::pristine::debug(
&*txn.read(),
&*fa.read(),
std::fs::File::create("commute-order1.dot").unwrap(),
)?;
crate::pristine::debug(
&*txn.read(),
&*fb.read(),
std::fs::File::create("commute-order2.dot").unwrap(),
)?;
}
anyhow::bail!(
"seed {seed} step {step}: APPLY NON-COMMUTATIVE on c{ci}\n\
minimal subset ({} changes): {:?}\n\
order-fwd {:?} ({} conflicts) = {:?}\n\
order-rev {:?} ({} conflicts) = {:?}",
minimal.len(),
minimal
.iter()
.map(|h| h.to_base32()[..8].to_string())
.collect::<Vec<_>>(),
mfwd.iter()
.map(|h| h.to_base32()[..8].to_string())
.collect::<Vec<_>>(),
oa.1,
String::from_utf8_lossy(&oa.0),
mrev.iter()
.map(|h| h.to_base32()[..8].to_string())
.collect::<Vec<_>>(),
ob.1,
String::from_utf8_lossy(&ob.0),
);
}
}
}
if canon(&got.0) != canon(&exp.0) {
if std::env::var("FUZZ_DUMP").is_ok() {
crate::pristine::debug(
&*txn.read(),
&*chans[ci].channel.read(),
std::fs::File::create("diverge-unrecorded.dot").unwrap(),
)?;
crate::pristine::debug(
&*txn.read(),
&*fresh.read(),
std::fs::File::create("diverge-replay.dot").unwrap(),
)?;
}
anyhow::bail!(
"seed {seed} step {step}: deep unrecord of {} on c{ci} diverged from replay\n\
unrecorded = channel {:?} ({} conflicts)\n\
replay = fresh {:?} ({} conflicts)",
c.to_base32(),
String::from_utf8_lossy(&got.0),
got.1,
String::from_utf8_lossy(&exp.0),
exp.1,
);
}
if verbose {
eprintln!("[{step}] DEEP unrecord {} on c{ci} ok", c.to_base32());
}
}
}
for (ci, c) in chans.iter().enumerate() {
let (alive, pseudo) = check_alive(&*txn.read(), &c.channel.read());
if !alive.is_empty() {
anyhow::bail!(
"seed {seed} step {step}: alive-but-unreachable vertices on c{ci}: {:?}",
alive
);
}
if !pseudo.is_empty() {
anyhow::bail!(
"seed {seed} step {step}: pseudo-only vertices on c{ci}: {:?}",
pseudo
);
}
}
if std::env::var("FUZZ_LOCALIZE").is_ok() {
let canon = |o: &[u8]| -> Vec<Vec<u8>> {
let mut v: Vec<Vec<u8>> = o
.split(|&b| b == b'\n')
.filter(|l| {
!l.starts_with(b">>>>>>>")
&& !l.starts_with(b"=======")
&& !l.starts_with(b"<<<<<<<")
})
.map(|l| l.to_vec())
.collect();
v.sort();
v
};
for ci in 0..chans.len() {
let applied = chans[ci].applied.clone();
let fresh = txn
.write()
.open_or_create_channel(&SmallString::from_str(&format!("loc-{step}-{ci}")))?;
for h in &applied {
apply::apply_change_arc(&changes, &txn, &fresh, h)?;
}
if std::env::var("FUZZ_DUMP_STEP")
.ok()
.and_then(|s| s.parse::<usize>().ok())
== Some(step)
{
crate::pristine::debug(
&*txn.read(),
&*chans[ci].channel.read(),
std::fs::File::create(format!("loc-chan{ci}.dot")).unwrap(),
)?;
crate::pristine::debug(
&*txn.read(),
&*fresh.read(),
std::fs::File::create(format!("loc-fresh{ci}.dot")).unwrap(),
)?;
}
let got = fuzz_render(&changes, &txn, &chans[ci].channel)?;
let exp = fuzz_render(&changes, &txn, &fresh)?;
if got.1 != exp.1 || canon(&got.0) != canon(&exp.0) {
anyhow::bail!(
"seed {seed} step {step}: c{ci} STRUCTURE diverged (conflicts {} vs {}, content {})",
got.1,
exp.1,
if canon(&got.0) != canon(&exp.0) {
"DIFFERS"
} else {
"same"
},
);
}
}
}
}
Ok(())
}
#[test]
fn fuzz_unrecord_smoke() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
for seed in 0..64 {
fuzz_seed(seed, 25, false)?;
}
Ok(())
}
#[test]
#[ignore]
fn fuzz_unrecord_zombies() {
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering::Relaxed};
use std::sync::{Arc, Mutex};
fn env_usize(k: &str, d: usize) -> usize {
std::env::var(k)
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(d)
}
fn env_u64(k: &str, d: u64) -> u64 {
std::env::var(k)
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(d)
}
let threads = env_usize(
"FUZZ_THREADS",
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4),
);
let nsteps = env_usize("FUZZ_STEPS", 40);
let start = env_u64("FUZZ_START", 0);
let nseeds = env_u64("FUZZ_SEEDS", u64::MAX);
let verbose = std::env::var("FUZZ_VERBOSE").is_ok();
let end = start.saturating_add(nseeds);
eprintln!("fuzzing unrecord: threads={threads} steps={nsteps} seeds=[{start}, {end})");
let next = Arc::new(AtomicU64::new(start));
let stop = Arc::new(AtomicBool::new(false));
let found: Arc<Mutex<Option<(u64, String)>>> = Arc::new(Mutex::new(None));
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|info| {
if std::env::var("SHOW_PANIC").is_ok() {
eprintln!("PANIC-LOC {:?}: {}", info.location(), info);
}
}));
let mut handles = Vec::new();
for _ in 0..threads {
let next = next.clone();
let stop = stop.clone();
let found = found.clone();
handles.push(std::thread::spawn(move || {
loop {
if stop.load(Relaxed) {
break;
}
let seed = next.fetch_add(1, Relaxed);
if seed >= end {
break;
}
if seed % 1000 == 0 {
eprintln!("... seed {seed}");
}
let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
fuzz_seed(seed, nsteps, verbose)
}));
let bad = match res {
Ok(Ok(())) => None,
Ok(Err(e)) => Some(format!("{e:#}")),
Err(p) => Some(format!(
"panic: {}",
p.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| p.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "<opaque>".to_string())
)),
};
if let Some(msg) = bad {
*found.lock().unwrap() = Some((seed, msg));
stop.store(true, Relaxed);
break;
}
}
}));
}
for h in handles {
let _ = h.join();
}
std::panic::set_hook(default_hook);
if let Some((seed, msg)) = found.lock().unwrap().take() {
panic!(
"\n==== FOUND COUNTEREXAMPLE ====\nseed = {seed}\n{msg}\n\n\
reproduce: FUZZ_START={seed} FUZZ_SEEDS=1 FUZZ_THREADS=1 FUZZ_VERBOSE=1 \
cargo test -p pijul-core --release fuzz_unrecord_zombies -- --ignored --nocapture\n"
);
}
}
#[test]
fn zombie_unrecord_phantom() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let changes = changestore::memory::Memory::new();
let repo_a = working_copy::memory::Memory::new();
repo_a.add_file("file", b"L0\nL1\nL2\nL3\n".to_vec());
let env_a = pristine::sanakirja::Pristine::new_anon()?;
let txn_a = env_a.arc_txn_begin().unwrap();
txn_a.write().add_file("file", 0)?;
let chan_a = txn_a
.write()
.open_or_create_channel(&SmallString::from_str("a"))?;
let h0 = record_all(&repo_a, &changes, &txn_a, &chan_a, "")?;
repo_a
.write_file("file", Inode::ROOT)?
.write_all(b"L0\nL5\nL2\nL3\n")?;
let _ha = record_all(&repo_a, &changes, &txn_a, &chan_a, "")?;
let repo_b = working_copy::memory::Memory::new();
repo_b.add_file("file", b"L0\nL1\nL2\nL3\n".to_vec());
let env_b = pristine::sanakirja::Pristine::new_anon()?;
let txn_b = env_b.arc_txn_begin().unwrap();
txn_b.write().add_file("file", 0)?;
let chan_b = txn_b
.write()
.open_or_create_channel(&SmallString::from_str("b"))?;
apply::apply_change_arc(&changes, &txn_b, &chan_b, &h0)?;
output::output_repository_no_pending(&repo_b, &changes, &txn_b, &chan_b, "", true, None, 1, 0)?;
repo_b
.write_file("file", Inode::ROOT)?
.write_all(b"L0\nL1\nL1\nL3\n")?;
let hb = record_all(&repo_b, &changes, &txn_b, &chan_b, "")?;
let before = fuzz_render(&changes, &txn_a, &chan_a)?;
assert_eq!(std::str::from_utf8(&before.0).unwrap(), "L0\nL5\nL2\nL3\n");
assert_eq!(before.1, 0);
crate::pristine::debug(
&*txn_a.read(),
&*chan_a.read(),
std::fs::File::create("phantom-before.dot").unwrap(),
)?;
apply::apply_change_arc(&changes, &txn_a, &chan_a, &hb)?;
crate::pristine::debug(
&*txn_a.read(),
&*chan_a.read(),
std::fs::File::create("phantom-applied.dot").unwrap(),
)?;
debug!("unrecording {:?}", hb);
crate::unrecord::unrecord(
&mut *txn_a.write(),
&chan_a,
&changes,
&hb,
0,
&mut Default::default(),
)?;
crate::pristine::debug(
&*txn_a.read(),
&*chan_a.read(),
std::fs::File::create("phantom-unrecorded.dot").unwrap(),
)?;
let after = fuzz_render(&changes, &txn_a, &chan_a)?;
assert_eq!(
std::str::from_utf8(&after.0).unwrap(),
"L0\nL5\nL2\nL3\n",
"unrecord left a phantom zombie conflict: {:?} ({} conflicts); \
see phantom-*.dot",
String::from_utf8_lossy(&after.0),
after.1,
);
assert_eq!(after.1, 0);
Ok(())
}
#[test]
fn unrecord_reorder_seed35() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
repo.add_file("file", b"L0\nL1\nL2\nL3\n".to_vec());
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
txn.write().add_file("file", 0)?;
let channel = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let _h0 = record_all(&repo, &changes, &txn, &channel, "")?;
repo.write_file("file", Inode::ROOT)?
.write_all(b"L0\nL2\nL3\n")?;
let _del_l1 = record_all(&repo, &changes, &txn, &channel, "")?;
repo.write_file("file", Inode::ROOT)?
.write_all(b"L0\nL4\n")?;
let mid = record_all(&repo, &changes, &txn, &channel, "")?;
repo.write_file("file", Inode::ROOT)?.write_all(b"L4\n")?;
let del_l0 = record_all(&repo, &changes, &txn, &channel, "")?;
crate::pristine::debug(
&*txn.read(),
&*channel.read(),
std::fs::File::create("seed35-before.dot").unwrap(),
)?;
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&mid,
0,
&mut Default::default(),
)?;
crate::unrecord::unrecord(
&mut *txn.write(),
&channel,
&changes,
&del_l0,
0,
&mut Default::default(),
)?;
crate::pristine::debug(
&*txn.read(),
&*channel.read(),
std::fs::File::create("seed35-unrecorded.dot").unwrap(),
)?;
let (buf, nconf) = fuzz_render(&changes, &txn, &channel)?;
assert_eq!(
std::str::from_utf8(&buf).unwrap(),
"L0\nL2\nL3\n",
"unrecord produced a reordering conflict ({} conflicts); see seed35-*.dot",
nconf,
);
assert_eq!(nconf, 0);
Ok(())
}
#[test]
fn unrecord_resolution_roundtrip() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let repo2 = working_copy::memory::Memory::new();
let repo3 = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
repo.add_file("file", b"a\nb\nc\nd\n".to_vec());
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
txn.write().add_file("file", 0)?;
let main = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let h0 = record_all(&repo, &changes, &txn, &main, "")?;
let c2 = txn.write().fork(&main, &SmallString::from_str("c2"))?;
output::output_repository_no_pending(&repo2, &changes, &txn, &c2, "", true, None, 1, 0)?;
repo.write_file("file", Inode::ROOT)?.write_all(b"a\nd\n")?;
let hdel = record_all(&repo, &changes, &txn, &main, "")?;
repo2
.write_file("file", Inode::ROOT)?
.write_all(b"a\nb\nx\nc\ny\nd\n")?;
let hins = record_all(&repo2, &changes, &txn, &c2, "")?;
apply::apply_change_arc(&changes, &txn, &main, &hins)?;
repo.write_file("file", Inode::ROOT)?
.write_all(b"a\nb\nx\nc\ny\nd\n")?;
let hres = record_all(&repo, &changes, &txn, &main, "")?;
let (res_out, res_conf) = fuzz_render(&changes, &txn, &main)?;
eprintln!(
"after resolution: {:?} ({} conf)",
String::from_utf8_lossy(&res_out),
res_conf
);
crate::unrecord::unrecord(
&mut *txn.write(),
&main,
&changes,
&hres,
0,
&mut Default::default(),
)?;
let (unrec_out, unrec_conf) = fuzz_render(&changes, &txn, &main)?;
let fresh = txn
.write()
.open_or_create_channel(&SmallString::from_str("fresh"))?;
apply::apply_change_arc(&changes, &txn, &fresh, &h0)?;
apply::apply_change_arc(&changes, &txn, &fresh, &hdel)?;
apply::apply_change_arc(&changes, &txn, &fresh, &hins)?;
let (oracle_out, oracle_conf) = fuzz_render(&changes, &txn, &fresh)?;
eprintln!(
"after unrecord: {:?} ({} conf)",
String::from_utf8_lossy(&unrec_out),
unrec_conf
);
eprintln!(
"oracle (fresh): {:?} ({} conf)",
String::from_utf8_lossy(&oracle_out),
oracle_conf
);
let _ = &repo3;
assert_eq!(
std::str::from_utf8(&unrec_out).unwrap(),
std::str::from_utf8(&oracle_out).unwrap(),
"unrecording the resolution did not restore the conflict"
);
Ok(())
}
#[test]
fn unrecord_resolution_double() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let repo2 = working_copy::memory::Memory::new();
let repo3 = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
repo.add_file("file", b"a\nb\nc\nd\n".to_vec());
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
txn.write().add_file("file", 0)?;
let main = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let h0 = record_all(&repo, &changes, &txn, &main, "")?;
let c2 = txn.write().fork(&main, &SmallString::from_str("c2"))?;
let c3 = txn.write().fork(&main, &SmallString::from_str("c3"))?;
output::output_repository_no_pending(&repo2, &changes, &txn, &c2, "", true, None, 1, 0)?;
output::output_repository_no_pending(&repo3, &changes, &txn, &c3, "", true, None, 1, 0)?;
repo.write_file("file", Inode::ROOT)?.write_all(b"a\nd\n")?;
let hdela = record_all(&repo, &changes, &txn, &main, "")?;
repo2
.write_file("file", Inode::ROOT)?
.write_all(b"a\nd\n")?;
let hdelb = record_all(&repo2, &changes, &txn, &c2, "")?;
repo3
.write_file("file", Inode::ROOT)?
.write_all(b"a\nb\nx\nc\ny\nd\n")?;
let hins = record_all(&repo3, &changes, &txn, &c3, "")?;
apply::apply_change_arc(&changes, &txn, &main, &hdelb)?;
apply::apply_change_arc(&changes, &txn, &main, &hins)?;
repo.write_file("file", Inode::ROOT)?
.write_all(b"a\nb\nx\nc\ny\nd\n")?;
let hres = record_all(&repo, &changes, &txn, &main, "")?;
crate::unrecord::unrecord(
&mut *txn.write(),
&main,
&changes,
&hres,
0,
&mut Default::default(),
)?;
let (unrec_out, unrec_conf) = fuzz_render(&changes, &txn, &main)?;
let fresh = txn
.write()
.open_or_create_channel(&SmallString::from_str("fresh"))?;
for h in [&h0, &hdela, &hdelb, &hins] {
apply::apply_change_arc(&changes, &txn, &fresh, h)?;
}
let (oracle_out, oracle_conf) = fuzz_render(&changes, &txn, &fresh)?;
eprintln!(
"after unrecord: {:?} ({} conf)",
String::from_utf8_lossy(&unrec_out),
unrec_conf
);
eprintln!(
"oracle (fresh): {:?} ({} conf)",
String::from_utf8_lossy(&oracle_out),
oracle_conf
);
assert_eq!(
std::str::from_utf8(&unrec_out).unwrap(),
std::str::from_utf8(&oracle_out).unwrap(),
"double-deletion resolution unrecord diverged"
);
Ok(())
}
#[test]
fn unrecord_resolution_split() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let repo2 = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
repo.add_file("file", b"a\nc\nc\nd\n".to_vec());
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
txn.write().add_file("file", 0)?;
let main = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let h0 = record_all(&repo, &changes, &txn, &main, "")?;
let c2 = txn.write().fork(&main, &SmallString::from_str("c2"))?;
output::output_repository_no_pending(&repo2, &changes, &txn, &c2, "", true, None, 1, 0)?;
repo.write_file("file", Inode::ROOT)?.write_all(b"a\nd\n")?;
let hdel = record_all(&repo, &changes, &txn, &main, "")?;
repo2
.write_file("file", Inode::ROOT)?
.write_all(b"a\nc\ny\nc\nd\n")?;
let hins = record_all(&repo2, &changes, &txn, &c2, "")?;
apply::apply_change_arc(&changes, &txn, &main, &hins)?;
let (conf_out, conf_n) = fuzz_render(&changes, &txn, &main)?;
eprintln!(
"conflict: {:?} ({} conf)",
String::from_utf8_lossy(&conf_out),
conf_n
);
repo.write_file("file", Inode::ROOT)?
.write_all(b"a\nc\ny\nc\nd\n")?;
let hres = record_all(&repo, &changes, &txn, &main, "")?;
crate::unrecord::unrecord(
&mut *txn.write(),
&main,
&changes,
&hres,
0,
&mut Default::default(),
)?;
let (unrec_out, unrec_conf) = fuzz_render(&changes, &txn, &main)?;
let fresh = txn
.write()
.open_or_create_channel(&SmallString::from_str("fresh"))?;
for h in [&h0, &hdel, &hins] {
apply::apply_change_arc(&changes, &txn, &fresh, h)?;
}
let (oracle_out, oracle_conf) = fuzz_render(&changes, &txn, &fresh)?;
eprintln!(
"after unrecord: {:?} ({} conf)",
String::from_utf8_lossy(&unrec_out),
unrec_conf
);
eprintln!(
"oracle (fresh): {:?} ({} conf)",
String::from_utf8_lossy(&oracle_out),
oracle_conf
);
assert_eq!(
std::str::from_utf8(&unrec_out).unwrap(),
std::str::from_utf8(&oracle_out).unwrap(),
"split resolution unrecord diverged"
);
Ok(())
}
#[test]
fn unrecord_nested_zombie_insert() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let repo2 = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
repo.add_file("file", b"a\nb\nc\n".to_vec());
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
txn.write().add_file("file", 0)?;
let main = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let h0 = record_all(&repo, &changes, &txn, &main, "")?;
let c2 = txn.write().fork(&main, &SmallString::from_str("c2"))?;
output::output_repository_no_pending(&repo2, &changes, &txn, &c2, "", true, None, 1, 0)?;
repo.write_file("file", Inode::ROOT)?.write_all(b"a\nc\n")?;
let hdel = record_all(&repo, &changes, &txn, &main, "")?;
repo2
.write_file("file", Inode::ROOT)?
.write_all(b"a\nb\nX\nc\n")?;
let hins_x = record_all(&repo2, &changes, &txn, &c2, "")?;
repo2
.write_file("file", Inode::ROOT)?
.write_all(b"a\nb\nX\nY\nc\n")?;
let hins_y = record_all(&repo2, &changes, &txn, &c2, "")?;
apply::apply_change_arc(&changes, &txn, &main, &hins_x)?;
apply::apply_change_arc(&changes, &txn, &main, &hins_y)?;
let (z, zc) = fuzz_render(&changes, &txn, &main)?;
eprintln!(
"zombie state: {:?} ({} conf)",
String::from_utf8_lossy(&z),
zc
);
crate::unrecord::unrecord(
&mut *txn.write(),
&main,
&changes,
&hdel,
0,
&mut Default::default(),
)?;
let (unrec_out, unrec_conf) = fuzz_render(&changes, &txn, &main)?;
let fresh = txn
.write()
.open_or_create_channel(&SmallString::from_str("fresh"))?;
for h in [&h0, &hins_x, &hins_y] {
apply::apply_change_arc(&changes, &txn, &fresh, h)?;
}
let (oracle_out, oracle_conf) = fuzz_render(&changes, &txn, &fresh)?;
eprintln!(
"after unrecord: {:?} ({} conf)",
String::from_utf8_lossy(&unrec_out),
unrec_conf
);
eprintln!(
"oracle (fresh): {:?} ({} conf)",
String::from_utf8_lossy(&oracle_out),
oracle_conf
);
assert_eq!(
std::str::from_utf8(&unrec_out).unwrap(),
std::str::from_utf8(&oracle_out).unwrap(),
"nested-zombie-insert unrecord diverged"
);
Ok(())
}
#[test]
fn unrecord_nested_double() -> Result<(), anyhow::Error> {
env_logger::try_init().unwrap_or(());
let repo = working_copy::memory::Memory::new();
let repo2 = working_copy::memory::Memory::new();
let repo3 = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
repo.add_file("file", b"a\nb\nc\nd\n".to_vec());
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
txn.write().add_file("file", 0)?;
let main = txn
.write()
.open_or_create_channel(&SmallString::from_str("main"))?;
let h0 = record_all(&repo, &changes, &txn, &main, "")?;
let c2 = txn.write().fork(&main, &SmallString::from_str("c2"))?;
let c3 = txn.write().fork(&main, &SmallString::from_str("c3"))?;
output::output_repository_no_pending(&repo2, &changes, &txn, &c2, "", true, None, 1, 0)?;
output::output_repository_no_pending(&repo3, &changes, &txn, &c3, "", true, None, 1, 0)?;
repo.write_file("file", Inode::ROOT)?.write_all(b"a\nd\n")?;
let hdela = record_all(&repo, &changes, &txn, &main, "")?;
repo2
.write_file("file", Inode::ROOT)?
.write_all(b"a\nd\n")?;
let hdelb = record_all(&repo2, &changes, &txn, &c2, "")?;
repo3
.write_file("file", Inode::ROOT)?
.write_all(b"a\nb\nX\nc\nd\n")?;
let hins_x = record_all(&repo3, &changes, &txn, &c3, "")?;
repo3
.write_file("file", Inode::ROOT)?
.write_all(b"a\nb\nX\nY\nc\nd\n")?;
let hins_y = record_all(&repo3, &changes, &txn, &c3, "")?;
for h in [&hdelb, &hins_x, &hins_y] {
apply::apply_change_arc(&changes, &txn, &main, h)?;
}
crate::unrecord::unrecord(
&mut *txn.write(),
&main,
&changes,
&hdela,
0,
&mut Default::default(),
)?;
let (unrec_out, _) = fuzz_render(&changes, &txn, &main)?;
let fresh = txn
.write()
.open_or_create_channel(&SmallString::from_str("fresh"))?;
for h in [&h0, &hdelb, &hins_x, &hins_y] {
apply::apply_change_arc(&changes, &txn, &fresh, h)?;
}
let (oracle_out, _) = fuzz_render(&changes, &txn, &fresh)?;
eprintln!("after unrecord: {:?}", String::from_utf8_lossy(&unrec_out));
eprintln!("oracle (fresh): {:?}", String::from_utf8_lossy(&oracle_out));
assert_eq!(
canon_content(&unrec_out),
canon_content(&oracle_out),
"nested + double-deletion unrecord changed the content\nunrec = {:?}\noracle = {:?}",
String::from_utf8_lossy(&unrec_out),
String::from_utf8_lossy(&oracle_out),
);
Ok(())
}
#[test]
fn unrecord_zombie_marking_57() -> Result<(), anyhow::Error> {
let repo = working_copy::memory::Memory::new();
let changes = changestore::memory::Memory::new();
repo.add_file("file", b"L0\nL1\nL2\nL3\n".to_vec());
let env = pristine::sanakirja::Pristine::new_anon()?;
let txn = env.arc_txn_begin().unwrap();
txn.write().add_file("file", 0)?;
let c0 = txn
.write()
.open_or_create_channel(&SmallString::from_str("c0"))?;
let h0 = record_all(&repo, &changes, &txn, &c0, "")?;
macro_rules! edit_record {
($ch:expr, $content:expr) => {{
let wc = working_copy::memory::Memory::new();
output::output_repository_no_pending(&wc, &changes, &txn, $ch, "", true, None, 1, 0)?;
wc.write_file("file", Inode::ROOT)?.write_all($content)?;
record_all(&wc, &changes, &txn, $ch, "")?
}};
}
let c2 = txn.write().fork(&c0, &SmallString::from_str("c2"))?;
let hh = edit_record!(&c2, b"L1\nL3\n");
let hco = edit_record!(&c0, b"L2\nL2\nL3\n");
let hoz = edit_record!(&c0, b"L2\n");
apply::apply_change_arc(&changes, &txn, &c0, &hh)?;
crate::unrecord::unrecord(
&mut *txn.write(),
&c0,
&changes,
&hoz,
0,
&mut Default::default(),
)?;
let (unrec, uc) = fuzz_render(&changes, &txn, &c0)?;
let fresh = txn
.write()
.open_or_create_channel(&SmallString::from_str("fresh"))?;
for h in [&h0, &hco, &hh] {
apply::apply_change_arc(&changes, &txn, &fresh, h)?;
}
let (rep, rc) = fuzz_render(&changes, &txn, &fresh)?;
eprintln!(
"unrecord: {:?} ({uc} conflicts)",
String::from_utf8_lossy(&unrec)
);
eprintln!(
"replay: {:?} ({rc} conflicts)",
String::from_utf8_lossy(&rep)
);
assert_eq!(
canon_content(&unrec),
canon_content(&rep),
"unrecord changed the content\nunrec = {:?}\nreplay = {:?}",
String::from_utf8_lossy(&unrec),
String::from_utf8_lossy(&rep),
);
Ok(())
}