use super::*;
use crate::recovery_files::file_identity_of;
use crate::recovery_files::tests::lock_identity_of;
use crate::test_support::{fixture, git, git_context, git_text, remove, repository};
use crate::transaction_journal::{Base, LandingOperation, LockIdentity};
use crate::transaction_journal_encode::encode_ref_updated;
use crate::transaction_journal_encode::tests::{sample_lock, sample_preparing};
const ID: &str = "0b6c2c1e-6f5b-4d0e-9a55-3f5d8e2f6a10";
fn identity(path: &Path) -> FileIdentity {
let (device, inode, _) = file_identity_of(path).expect("identity");
return FileIdentity { device, inode };
}
fn preparing_for(repo: &Path) -> PreparingRecord {
let mut record: PreparingRecord = sample_preparing();
record.transaction_id = String::from(ID);
record.target_ref = String::from("refs/heads/main");
record.real_index_path = repo
.join(".git")
.join("index")
.to_str()
.expect("UTF-8")
.to_string();
return record;
}
fn landing_for(directory: &Path, new_oid: &str, lock: LockIdentity) -> LandingRecord {
return LandingRecord {
attempt: 1,
operation: LandingOperation::Commit,
expected_old: Base::Unborn,
new_oid: Some(String::from(new_oid)),
landed_tree_oid: String::from("4b825dc642cb6eb9a060e54bf8d69288fbee4904"),
pre_landing_index: identity(directory.join("pre-landing-1.index").as_path()),
post_index: identity(directory.join("post-1.index").as_path()),
lock,
pack_name: None,
added_paths: Vec::new(),
selected_worktree_paths: Vec::new(),
};
}
struct Scene {
root: PathBuf,
repo: PathBuf,
directory: PathBuf,
first: String,
second: String,
}
fn scene(name: &str) -> Scene {
let root: PathBuf = fixture(name);
let repo: PathBuf = repository(root.as_path(), "repo");
let first: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
let tree: String = git_text(repo.as_path(), &["rev-parse", "HEAD^{tree}"]);
let second: String = git_text(
repo.as_path(),
&[
"commit-tree",
"-p",
first.as_str(),
"-m",
"landed",
tree.as_str(),
],
);
let directory: PathBuf = root.join("transaction");
std::fs::create_dir(&directory).expect("transaction");
std::fs::write(directory.join("pre-landing-1.index"), b"pre").expect("pre-landing");
std::fs::write(directory.join("post-1.index"), b"post").expect("post");
return Scene {
root,
repo,
directory,
first,
second,
};
}
#[test]
fn markers_prove_a_landing() {
let scene: Scene = scene("completion-marker");
let preparing: PreparingRecord = preparing_for(scene.repo.as_path());
let landing: LandingRecord = landing_for(
scene.directory.as_path(),
scene.second.as_str(),
sample_lock(),
);
let context: GitContext = git_context();
std::fs::write(
scene.directory.join(REF_UPDATED_FILENAME),
encode_ref_updated(scene.second.as_str()),
)
.expect("marker");
assert!(
commit_landed(
&context,
scene.repo.as_path(),
scene.directory.as_path(),
&preparing,
&landing
)
.expect("landed")
);
std::fs::write(
scene.directory.join(REF_UPDATED_FILENAME),
encode_ref_updated(scene.first.as_str()),
)
.expect("other marker");
assert_eq!(
commit_landed(
&context,
scene.repo.as_path(),
scene.directory.as_path(),
&preparing,
&landing
)
.expect_err("contradiction")
.0,
format!(
"Landed marker names another commit than the landing record: {}",
scene.directory.display()
)
);
let mut without: LandingRecord = landing.clone();
without.new_oid = None;
assert!(
commit_landed(
&context,
scene.repo.as_path(),
scene.directory.as_path(),
&preparing,
&without
)
.is_err()
);
remove(scene.root.as_path());
}
#[test]
fn reflog_nonces_prove_a_landing() {
let scene: Scene = scene("completion-reflog");
let preparing: PreparingRecord = preparing_for(scene.repo.as_path());
let landing: LandingRecord = landing_for(
scene.directory.as_path(),
scene.second.as_str(),
sample_lock(),
);
let context: GitContext = git_context();
let directory: &Path = scene.directory.as_path();
assert!(
!commit_landed(
&context,
scene.repo.as_path(),
directory,
&preparing,
&landing
)
.expect("not landed")
);
git(
scene.repo.as_path(),
&["update-ref", "refs/heads/main", scene.second.as_str()],
);
let reachable: RecoveryError = commit_landed(
&context,
scene.repo.as_path(),
directory,
&preparing,
&landing,
)
.expect_err("no nonce");
assert_eq!(
reachable.0,
format!(
"refs/heads/main contains the transaction commit {} but its reflog lacks the nonce entry; recovery retained at {}",
scene.second,
directory.display()
)
);
git(
scene.repo.as_path(),
&["update-ref", "refs/heads/main", scene.first.as_str()],
);
let action: String = format!("{}: landed", nonce_subject_prefix(ID));
git(
scene.repo.as_path(),
&[
"update-ref",
"-m",
action.as_str(),
"refs/heads/main",
scene.second.as_str(),
],
);
assert!(
commit_landed(
&context,
scene.repo.as_path(),
directory,
&preparing,
&landing
)
.expect("landed")
);
let mut other: LandingRecord = landing.clone();
other.new_oid = Some(scene.first.clone());
assert_eq!(
commit_landed(
&context,
scene.repo.as_path(),
directory,
&preparing,
&other
)
.expect_err("other commit")
.0,
format!(
"refs/heads/main reflog nonce names {}, not the recorded {}; recovery retained at {}",
scene.second,
scene.first,
directory.display()
)
);
git(
scene.repo.as_path(),
&[
"update-ref",
"-m",
action.as_str(),
"refs/heads/main",
scene.first.as_str(),
],
);
assert_eq!(
commit_landed(
&context,
scene.repo.as_path(),
directory,
&preparing,
&landing
)
.expect_err("several")
.0,
format!(
"refs/heads/main reflog names several commits for transaction {ID}; recovery retained at {}",
directory.display()
)
);
remove(scene.root.as_path());
}
#[test]
fn unreadable_reflogs_fail_closed() {
let scene: Scene = scene("completion-unreadable");
let mut preparing: PreparingRecord = preparing_for(scene.repo.as_path());
preparing.target_ref = String::from("refs/heads/none");
let landing: LandingRecord = landing_for(
scene.directory.as_path(),
scene.second.as_str(),
sample_lock(),
);
assert!(
commit_landed(
&git_context(),
scene.repo.as_path(),
scene.directory.as_path(),
&preparing,
&landing
)
.expect_err("unreadable")
.0
.starts_with("refs/heads/none reflog is unreadable")
);
remove(scene.root.as_path());
}
#[test]
fn indexes_install_from_the_pre_landing_state() {
let scene: Scene = scene("completion-install");
let preparing: PreparingRecord = preparing_for(scene.repo.as_path());
let real_index: PathBuf = PathBuf::from(preparing.real_index_path.clone());
std::fs::write(&real_index, b"pre").expect("pre-landing real index");
let lock: PathBuf = PathBuf::from(format!("{}.lock", preparing.real_index_path));
std::fs::write(&lock, b"").expect("owned lock");
let landing: LandingRecord = landing_for(
scene.directory.as_path(),
scene.second.as_str(),
lock_identity_of(lock.as_path()),
);
assert_eq!(
complete_index(scene.directory.as_path(), &preparing, &landing).expect("install"),
IndexCompletion::Installed
);
assert_eq!(std::fs::read(&real_index).expect("installed"), b"post");
assert!(!lock.exists());
assert!(
scene
.directory
.join("pre-landing-1.index.recovery")
.exists()
);
assert!(scene.directory.join("post-1.index.recovery").exists());
remove(scene.root.as_path());
}
#[test]
fn installed_or_changed_indexes_are_recognized() {
let scene: Scene = scene("completion-installed");
let preparing: PreparingRecord = preparing_for(scene.repo.as_path());
let real_index: PathBuf = PathBuf::from(preparing.real_index_path.clone());
let lock: PathBuf = PathBuf::from(format!("{}.lock", preparing.real_index_path));
std::fs::write(&lock, b"").expect("owned lock");
let landing: LandingRecord = landing_for(
scene.directory.as_path(),
scene.second.as_str(),
lock_identity_of(lock.as_path()),
);
std::fs::write(&real_index, b"post").expect("already installed");
assert_eq!(
complete_index(scene.directory.as_path(), &preparing, &landing).expect("recognized"),
IndexCompletion::AlreadyInstalled
);
assert!(!lock.exists());
std::fs::write(&real_index, b"someone else").expect("changed index");
std::fs::write(scene.directory.join(INDEX_INSTALLED_FILENAME), b"").expect("marker");
assert_eq!(
complete_index(scene.directory.as_path(), &preparing, &landing).expect("marker"),
IndexCompletion::AlreadyInstalled
);
std::fs::remove_file(scene.directory.join(INDEX_INSTALLED_FILENAME)).expect("remove marker");
assert_eq!(
complete_index(scene.directory.as_path(), &preparing, &landing)
.expect_err("changed")
.0,
format!(
"Real index no longer matches the pre-landing snapshot; recovery retained at {}",
scene.directory.display()
)
);
std::fs::remove_file(scene.directory.join("post-1.index")).expect("remove post");
std::fs::write(scene.directory.join("post-1.index"), b"post").expect("replaced post");
assert!(complete_index(scene.directory.as_path(), &preparing, &landing).is_err());
remove(scene.root.as_path());
}