jj/lib/tests/test_bad_locking.rs
Martin von Zweigbergk f1666375bd repo: replace Repo trait by enum with readonly and mutable variants
I want to keep the index updated within the transaction. I tried doing
that by adding a `trait Index`, implemented by `ReadonlyIndex` and
`MutableIndex`. However, `ReadonlyRepo::index` is of type
`Mutex<Option<Arc<IndexFile>>>` (because it is lazily initialized),
and we cannot get a `&dyn Index` that lives long enough to be returned
from a `Repo::index()` from that. It seems the best solution is to
instead create an `Index` enum (instead of a trait), with one readonly
and one mutable variant. This commit starts the migration to that
design by replacing the `Repo` trait by an enum. I never intended for
there there to be more implementations of `Repo` than `ReadonlyRepo`
and `MutableRepo` anyway.
2021-02-13 08:31:23 -08:00

179 lines
7.5 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 std::path::PathBuf;
use tempfile::TempDir;
use jujube_lib::repo::ReadonlyRepo;
use jujube_lib::testutils;
use jujube_lib::view::View;
use std::sync::Arc;
use test_case::test_case;
fn copy_directory(src: &PathBuf, dst: &PathBuf) {
std::fs::create_dir(dst).ok();
for entry in std::fs::read_dir(src).unwrap() {
let child_src = entry.unwrap().path();
let base_name = child_src.file_name().unwrap();
let child_dst = dst.join(base_name);
if child_src.is_dir() {
copy_directory(&child_src, &child_dst)
} else {
std::fs::copy(&child_src, &child_dst).unwrap();
}
}
}
fn merge_directories(left: &PathBuf, base: &PathBuf, right: &PathBuf, output: &PathBuf) {
std::fs::create_dir(output).ok();
let mut sub_dirs = vec![];
// Walk the left side and copy to the output
for entry in std::fs::read_dir(left).unwrap() {
let path = entry.unwrap().path();
let base_name = path.file_name().unwrap();
let child_left = left.join(base_name);
let child_output = output.join(base_name);
if child_left.is_dir() {
sub_dirs.push(base_name.to_os_string());
} else {
std::fs::copy(&child_left, &child_output).unwrap();
}
}
// Walk the base and find files removed in the right side, then remove them in
// the output
for entry in std::fs::read_dir(base).unwrap() {
let path = entry.unwrap().path();
let base_name = path.file_name().unwrap();
let child_base = base.join(base_name);
let child_right = right.join(base_name);
let child_output = output.join(base_name);
if child_base.is_dir() {
sub_dirs.push(base_name.to_os_string());
} else if !child_right.exists() {
std::fs::remove_file(child_output).ok();
}
}
// Walk the right side and find files added in the right side, then add them in
// the output
for entry in std::fs::read_dir(right).unwrap() {
let path = entry.unwrap().path();
let base_name = path.file_name().unwrap();
let child_base = base.join(base_name);
let child_right = right.join(base_name);
let child_output = output.join(base_name);
if child_right.is_dir() {
sub_dirs.push(base_name.to_os_string());
} else if !child_base.exists() {
// This overwrites the left side if that's been written. That's fine, since the
// point of the test is that it should be okay for either side to win.
std::fs::copy(&child_right, &child_output).unwrap();
}
}
// Do the merge in subdirectories
for base_name in sub_dirs {
let child_base = base.join(&base_name);
let child_right = right.join(&base_name);
let child_left = left.join(&base_name);
let child_output = output.join(&base_name);
merge_directories(&child_left, &child_base, &child_right, &child_output);
}
}
#[test_case(false ; "local store")]
#[test_case(true ; "git store")]
fn test_bad_locking_children(use_git: bool) {
// Test that two new commits created on separate machines are both visible (not
// lost due to lack of locking)
let settings = testutils::user_settings();
let (_temp_dir, repo) = testutils::init_repo(&settings, use_git);
let initial = testutils::create_random_commit(&settings, &repo)
.set_parents(vec![repo.store().root_commit_id().clone()])
.write_to_new_transaction(&repo, "test");
// Simulate a write of a commit that happens on one machine
let machine1_path = TempDir::new().unwrap().into_path();
copy_directory(repo.working_copy_path(), &machine1_path);
let machine1_repo = ReadonlyRepo::load(&settings, machine1_path).unwrap();
let child1 = testutils::create_random_commit(&settings, &machine1_repo)
.set_parents(vec![initial.id().clone()])
.write_to_new_transaction(&machine1_repo, "test");
// Simulate a write of a commit that happens on another machine
let machine2_path = TempDir::new().unwrap().into_path();
copy_directory(repo.working_copy_path(), &machine2_path);
let machine2_repo = ReadonlyRepo::load(&settings, machine2_path).unwrap();
let child2 = testutils::create_random_commit(&settings, &machine2_repo)
.set_parents(vec![initial.id().clone()])
.write_to_new_transaction(&machine2_repo, "test");
// Simulate that the distributed file system now has received the changes from
// both machines
let merged_path = TempDir::new().unwrap().into_path();
merge_directories(
machine1_repo.working_copy_path(),
repo.working_copy_path(),
machine2_repo.working_copy_path(),
&merged_path,
);
let merged_repo = ReadonlyRepo::load(&settings, merged_path).unwrap();
assert!(merged_repo.view().heads().contains(child1.id()));
assert!(merged_repo.view().heads().contains(child2.id()));
let op_head_id = merged_repo.view().base_op_head_id().clone();
let op_head = merged_repo
.view()
.op_store()
.read_operation(&op_head_id)
.unwrap();
assert_eq!(op_head.parents.len(), 2);
}
#[test_case(false ; "local store")]
#[test_case(true ; "git store")]
fn test_bad_locking_interrupted(use_git: bool) {
// Test that an interrupted update of the op-heads resulting in on op-head
// that's a descendant of the other is resolved without creating a new
// operation.
let settings = testutils::user_settings();
let (_temp_dir, mut repo) = testutils::init_repo(&settings, use_git);
let initial = testutils::create_random_commit(&settings, &repo)
.set_parents(vec![repo.store().root_commit_id().clone()])
.write_to_new_transaction(&repo, "test");
Arc::get_mut(&mut repo).unwrap().reload();
// Simulate a crash that resulted in the old op-head left in place. We simulate
// it somewhat hackily by copying the view/op_heads/ directory before the
// operation and then copying that back afterwards, leaving the existing
// op-head(s) in place.
let op_heads_dir = repo.repo_path().join("view").join("op_heads");
let backup_path = TempDir::new().unwrap().into_path();
copy_directory(&op_heads_dir, &backup_path);
let mut tx = repo.start_transaction("test");
testutils::create_random_commit(&settings, &repo)
.set_parents(vec![initial.id().clone()])
.write_to_transaction(&mut tx);
let op_head_id = tx.commit().id().clone();
copy_directory(&backup_path, &op_heads_dir);
// Reload the repo and check that only the new head is present.
let reloaded_repo = ReadonlyRepo::load(&settings, repo.working_copy_path().clone()).unwrap();
assert_eq!(reloaded_repo.view().base_op_head_id(), &op_head_id);
// Reload once more to make sure that the view/op_heads/ directory was updated
// correctly.
let reloaded_repo = ReadonlyRepo::load(&settings, repo.working_copy_path().clone()).unwrap();
assert_eq!(reloaded_repo.view().base_op_head_id(), &op_head_id);
}