jj/lib/tests/test_transaction.rs
Martin von Zweigbergk 9ffd35caf8 transaction: when checking out open commit with conflicts, create child commit
I've been confused twice that rebasing an open commit so it results in
conflicts doesn't show the conflicts in the log output. That's because
we create a successor instead if a commit with conflicts is open. I
guess I thought it would be expected that a child commit was not
created. Since it seems surprising in practice, let's change it and
we'll see if the new behavior is more or less surprising.
2021-01-22 11:41:52 -08:00

493 lines
19 KiB
Rust

// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use jujube_lib::commit_builder::CommitBuilder;
use jujube_lib::repo::Repo;
use jujube_lib::repo_path::FileRepoPath;
use jujube_lib::store::{Conflict, ConflictId, ConflictPart, TreeValue};
use jujube_lib::store_wrapper::StoreWrapper;
use jujube_lib::testutils;
use std::sync::Arc;
use test_case::test_case;
// TODO Many of the tests here are not run with Git because they end up creating
// two commits with the same contents.
#[test_case(false ; "local store")]
// #[test_case(true ; "git store")]
fn test_checkout_open(use_git: bool) {
// Test that Transaction::check_out() uses the requested commit if it's open
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
let mut tx = repo.start_transaction("test");
let requested_checkout = testutils::create_random_commit(&settings, &repo)
.set_open(true)
.write_to_transaction(&mut tx);
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let mut tx = repo.start_transaction("test");
let actual_checkout = tx.check_out(&settings, &requested_checkout);
assert_eq!(actual_checkout.id(), requested_checkout.id());
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
assert_eq!(repo.view().checkout(), actual_checkout.id());
}
#[test_case(false ; "local store")]
// #[test_case(true ; "git store")]
fn test_checkout_closed(use_git: bool) {
// Test that Transaction::check_out() creates a child if the requested commit is
// closed
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
let mut tx = repo.start_transaction("test");
let requested_checkout = testutils::create_random_commit(&settings, &repo)
.set_open(false)
.write_to_transaction(&mut tx);
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let mut tx = repo.start_transaction("test");
let actual_checkout = tx.check_out(&settings, &requested_checkout);
assert_eq!(actual_checkout.tree().id(), requested_checkout.tree().id());
assert_eq!(actual_checkout.parents().len(), 1);
assert_eq!(actual_checkout.parents()[0].id(), requested_checkout.id());
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
assert_eq!(repo.view().checkout(), actual_checkout.id());
}
#[test_case(false ; "local store")]
// #[test_case(true ; "git store")]
fn test_checkout_open_with_conflict(use_git: bool) {
// Test that Transaction::check_out() creates a child if the requested
// commit is open and has conflicts
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
let store = repo.store();
let file_path = FileRepoPath::from("file");
let conflict_id = write_conflict(store, &file_path);
let mut tree_builder = repo
.store()
.tree_builder(repo.store().empty_tree_id().clone());
tree_builder.set(file_path.to_repo_path(), TreeValue::Conflict(conflict_id));
let tree_id = tree_builder.write_tree();
let mut tx = repo.start_transaction("test");
let requested_checkout = CommitBuilder::for_new_commit(&settings, store, tree_id)
.set_open(true)
.write_to_transaction(&mut tx);
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let mut tx = repo.start_transaction("test");
let actual_checkout = tx.check_out(&settings, &requested_checkout);
let file_value = actual_checkout.tree().path_value(&file_path.to_repo_path());
match file_value {
Some(TreeValue::Normal {
id: _,
executable: false,
}) => {}
_ => panic!("unexpected tree value: {:?}", file_value),
}
assert_eq!(actual_checkout.parents().len(), 1);
assert_eq!(actual_checkout.parents()[0].id(), requested_checkout.id());
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
assert_eq!(repo.view().checkout(), actual_checkout.id());
}
#[test_case(false ; "local store")]
// #[test_case(true ; "git store")]
fn test_checkout_closed_with_conflict(use_git: bool) {
// Test that Transaction::check_out() creates a child if the requested commit is
// closed and has conflicts
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
let store = repo.store();
let file_path = FileRepoPath::from("file");
let conflict_id = write_conflict(store, &file_path);
let mut tree_builder = repo
.store()
.tree_builder(repo.store().empty_tree_id().clone());
tree_builder.set(file_path.to_repo_path(), TreeValue::Conflict(conflict_id));
let tree_id = tree_builder.write_tree();
let mut tx = repo.start_transaction("test");
let requested_checkout = CommitBuilder::for_new_commit(&settings, store, tree_id)
.set_open(false)
.write_to_transaction(&mut tx);
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let mut tx = repo.start_transaction("test");
let actual_checkout = tx.check_out(&settings, &requested_checkout);
let file_value = actual_checkout.tree().path_value(&file_path.to_repo_path());
match file_value {
Some(TreeValue::Normal {
id: _,
executable: false,
}) => {}
_ => panic!("unexpected tree value: {:?}", file_value),
}
assert_eq!(actual_checkout.parents().len(), 1);
assert_eq!(actual_checkout.parents()[0].id(), requested_checkout.id());
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
assert_eq!(repo.view().checkout(), actual_checkout.id());
}
fn write_conflict(store: &Arc<StoreWrapper>, file_path: &FileRepoPath) -> ConflictId {
let file_id1 = testutils::write_file(store, &file_path, "a\n");
let file_id2 = testutils::write_file(store, &file_path, "b\n");
let file_id3 = testutils::write_file(store, &file_path, "c\n");
let conflict = Conflict {
removes: vec![ConflictPart {
value: TreeValue::Normal {
id: file_id1,
executable: false,
},
}],
adds: vec![
ConflictPart {
value: TreeValue::Normal {
id: file_id2,
executable: false,
},
},
ConflictPart {
value: TreeValue::Normal {
id: file_id3,
executable: false,
},
},
],
};
store.write_conflict(&conflict).unwrap()
}
#[test_case(false ; "local store")]
// #[test_case(true ; "git store")]
fn test_checkout_previous_not_empty(use_git: bool) {
// Test that Transaction::check_out() does not usually prune the previous
// commit.
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
let mut tx = repo.start_transaction("test");
let old_checkout = testutils::create_random_commit(&settings, &repo)
.set_open(true)
.write_to_transaction(&mut tx);
tx.check_out(&settings, &old_checkout);
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let mut tx = repo.start_transaction("test");
let new_checkout = testutils::create_random_commit(&settings, &repo)
.set_open(true)
.write_to_transaction(&mut tx);
tx.check_out(&settings, &new_checkout);
assert!(!tx.as_repo().evolution().is_obsolete(old_checkout.id()));
tx.discard();
}
#[test_case(false ; "local store")]
// #[test_case(true ; "git store")]
fn test_checkout_previous_empty(use_git: bool) {
// Test that Transaction::check_out() prunes the previous commit if it was
// empty.
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
let mut tx = repo.start_transaction("test");
let old_checkout = CommitBuilder::for_open_commit(
&settings,
repo.store(),
repo.store().root_commit_id().clone(),
repo.store().empty_tree_id().clone(),
)
.write_to_transaction(&mut tx);
tx.check_out(&settings, &old_checkout);
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let mut tx = repo.start_transaction("test");
let new_checkout = testutils::create_random_commit(&settings, &repo)
.set_open(true)
.write_to_transaction(&mut tx);
tx.check_out(&settings, &new_checkout);
assert!(tx.as_repo().evolution().is_obsolete(old_checkout.id()));
tx.discard();
}
#[test_case(false ; "local store")]
// #[test_case(true ; "git store")]
fn test_checkout_previous_empty_and_obsolete(use_git: bool) {
// Test that Transaction::check_out() does not unnecessarily prune the previous
// commit if it was empty but already obsolete.
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
let mut tx = repo.start_transaction("test");
let old_checkout = CommitBuilder::for_open_commit(
&settings,
repo.store(),
repo.store().root_commit_id().clone(),
repo.store().empty_tree_id().clone(),
)
.write_to_transaction(&mut tx);
let successor = CommitBuilder::for_rewrite_from(&settings, repo.store(), &old_checkout)
.write_to_transaction(&mut tx);
tx.check_out(&settings, &old_checkout);
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let mut tx = repo.start_transaction("test");
let new_checkout = testutils::create_random_commit(&settings, &repo)
.set_open(true)
.write_to_transaction(&mut tx);
tx.check_out(&settings, &new_checkout);
let successors = tx.as_repo().evolution().successors(old_checkout.id());
assert_eq!(successors.len(), 1);
assert_eq!(successors.iter().next().unwrap(), successor.id());
tx.discard();
}
#[test_case(false ; "local store")]
// #[test_case(true ; "git store")]
fn test_checkout_previous_empty_and_pruned(use_git: bool) {
// Test that Transaction::check_out() does not unnecessarily prune the previous
// commit if it was empty but already obsolete.
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
let mut tx = repo.start_transaction("test");
let old_checkout = testutils::create_random_commit(&settings, &repo)
.set_open(true)
.set_pruned(true)
.write_to_transaction(&mut tx);
tx.check_out(&settings, &old_checkout);
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let mut tx = repo.start_transaction("test");
let new_checkout = testutils::create_random_commit(&settings, &repo)
.set_open(true)
.write_to_transaction(&mut tx);
tx.check_out(&settings, &new_checkout);
assert!(tx
.as_repo()
.evolution()
.successors(old_checkout.id())
.is_empty());
tx.discard();
}
#[test_case(false ; "local store")]
// #[test_case(true ; "git store")]
fn test_add_head_success(use_git: bool) {
// Test that Transaction::add_head() adds the head, and that it's still there
// after commit.
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
// Create a commit outside of the repo by using a temporary transaction. Then
// add that as a head.
let mut tx = repo.start_transaction("test");
let new_commit =
testutils::create_random_commit(&settings, &repo).write_to_transaction(&mut tx);
tx.discard();
let mut tx = repo.start_transaction("test");
assert!(!tx.as_repo().view().heads().contains(new_commit.id()));
tx.add_head(&new_commit);
assert!(tx.as_repo().view().heads().contains(new_commit.id()));
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
assert!(repo.view().heads().contains(new_commit.id()));
}
#[test_case(false ; "local store")]
// #[test_case(true ; "git store")]
fn test_add_head_ancestor(use_git: bool) {
// Test that Transaction::add_head() does not add a head if it's an ancestor of
// an existing head.
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
let mut tx = repo.start_transaction("test");
let commit1 = testutils::create_random_commit(&settings, &repo).write_to_transaction(&mut tx);
let commit2 = testutils::create_random_commit(&settings, &repo)
.set_parents(vec![commit1.id().clone()])
.write_to_transaction(&mut tx);
let _commit3 = testutils::create_random_commit(&settings, &repo)
.set_parents(vec![commit2.id().clone()])
.write_to_transaction(&mut tx);
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let mut tx = repo.start_transaction("test");
tx.add_head(&commit1);
assert!(!tx.as_repo().view().heads().contains(commit1.id()));
tx.discard();
}
#[test_case(false ; "local store")]
// #[test_case(true ; "git store")]
fn test_remove_head(use_git: bool) {
// Test that Transaction::remove_head() removes the head, and that it's still
// removed after commit.
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
let mut tx = repo.start_transaction("test");
let commit1 = testutils::create_random_commit(&settings, &repo).write_to_transaction(&mut tx);
let commit2 = testutils::create_random_commit(&settings, &repo)
.set_parents(vec![commit1.id().clone()])
.write_to_transaction(&mut tx);
let commit3 = testutils::create_random_commit(&settings, &repo)
.set_parents(vec![commit2.id().clone()])
.write_to_transaction(&mut tx);
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let mut tx = repo.start_transaction("test");
assert!(tx.as_repo().view().heads().contains(commit3.id()));
tx.remove_head(&commit3);
let heads = tx.as_repo().view().heads().clone();
assert!(!heads.contains(commit3.id()));
assert!(!heads.contains(commit2.id()));
assert!(!heads.contains(commit1.id()));
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let heads = repo.view().heads().clone();
assert!(!heads.contains(commit3.id()));
assert!(!heads.contains(commit2.id()));
assert!(!heads.contains(commit1.id()));
}
#[test_case(false ; "local store")]
// #[test_case(true ; "git store")]
fn test_remove_head_ancestor_git_ref(use_git: bool) {
// Test that Transaction::remove_head() does not leave the view with a git ref
// pointing to a commit that's not reachable by any head.
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
let mut tx = repo.start_transaction("test");
let commit1 = testutils::create_random_commit(&settings, &repo).write_to_transaction(&mut tx);
let commit2 = testutils::create_random_commit(&settings, &repo)
.set_parents(vec![commit1.id().clone()])
.write_to_transaction(&mut tx);
let commit3 = testutils::create_random_commit(&settings, &repo)
.set_parents(vec![commit2.id().clone()])
.write_to_transaction(&mut tx);
tx.insert_git_ref("refs/heads/main".to_string(), commit1.id().clone());
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let mut tx = repo.start_transaction("test");
let heads = tx.as_repo().view().heads().clone();
assert!(heads.contains(commit3.id()));
tx.remove_head(&commit3);
let heads = tx.as_repo().view().heads().clone();
assert!(!heads.contains(commit3.id()));
assert!(!heads.contains(commit2.id()));
assert!(heads.contains(commit1.id()));
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let heads = repo.view().heads().clone();
assert!(!heads.contains(commit3.id()));
assert!(!heads.contains(commit2.id()));
assert!(heads.contains(commit1.id()));
}
#[test_case(false ; "local store")]
// #[test_case(true ; "git store")]
fn test_add_public_head(use_git: bool) {
// Test that Transaction::add_public_head() adds the head, and that it's still
// there after commit.
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
let mut tx = repo.start_transaction("test");
let commit1 = testutils::create_random_commit(&settings, &repo).write_to_transaction(&mut tx);
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let mut tx = repo.start_transaction("test");
assert!(!tx.as_repo().view().public_heads().contains(commit1.id()));
tx.add_public_head(&commit1);
assert!(tx.as_repo().view().public_heads().contains(commit1.id()));
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
assert!(repo.view().public_heads().contains(commit1.id()));
}
#[test_case(false ; "local store")]
// #[test_case(true ; "git store")]
fn test_add_public_head_ancestor(use_git: bool) {
// Test that Transaction::add_public_head() does not add a public head if it's
// an ancestor of an existing public head.
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
let mut tx = repo.start_transaction("test");
let commit1 = testutils::create_random_commit(&settings, &repo).write_to_transaction(&mut tx);
let commit2 = testutils::create_random_commit(&settings, &repo)
.set_parents(vec![commit1.id().clone()])
.write_to_transaction(&mut tx);
tx.add_public_head(&commit2);
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let mut tx = repo.start_transaction("test");
assert!(!tx.as_repo().view().public_heads().contains(commit1.id()));
tx.add_public_head(&commit1);
assert!(!tx.as_repo().view().public_heads().contains(commit1.id()));
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
assert!(!repo.view().public_heads().contains(commit1.id()));
}
#[test_case(false ; "local store")]
// #[test_case(true ; "git store")]
fn test_remove_public_head(use_git: bool) {
// Test that Transaction::remove_public_head() removes the head, and that it's
// still removed after commit.
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
let mut tx = repo.start_transaction("test");
let commit1 = testutils::create_random_commit(&settings, &repo).write_to_transaction(&mut tx);
tx.add_public_head(&commit1);
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
let mut tx = repo.start_transaction("test");
assert!(tx.as_repo().view().public_heads().contains(commit1.id()));
tx.remove_public_head(&commit1);
assert!(!tx.as_repo().view().public_heads().contains(commit1.id()));
tx.commit();
Arc::get_mut(&mut repo).unwrap().reload();
assert!(!repo.view().public_heads().contains(commit1.id()));
}