forked from mirrors/jj
We don't seem to have any tests that our protection from undetected changes caused by writes happening right after checkout, so let's add one. The test case loops 100 times and each iteration fails slightly more than 80% of the time on my machine (if I remove the protection in `TreeState::update_file_state()`), so it seems quite good at triggering the race.
187 lines
7.2 KiB
Rust
187 lines
7.2 KiB
Rust
// Copyright 2020 The Jujutsu Authors
|
|
//
|
|
// 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::cmp::max;
|
|
use std::thread;
|
|
|
|
use assert_matches::assert_matches;
|
|
use jj_lib::backend::TreeId;
|
|
use jj_lib::op_store::OperationId;
|
|
use jj_lib::repo::{Repo, StoreFactories};
|
|
use jj_lib::repo_path::RepoPath;
|
|
use jj_lib::working_copy::{CheckoutError, SnapshotOptions};
|
|
use jj_lib::workspace::Workspace;
|
|
use testutils::{write_working_copy_file, TestWorkspace};
|
|
|
|
#[test]
|
|
fn test_concurrent_checkout() {
|
|
// Test that we error out if a concurrent checkout is detected (i.e. if the
|
|
// working-copy commit changed on disk after we read it).
|
|
let settings = testutils::user_settings();
|
|
let mut test_workspace1 = TestWorkspace::init(&settings, true);
|
|
let repo1 = test_workspace1.repo.clone();
|
|
let workspace1_root = test_workspace1.workspace.workspace_root().clone();
|
|
|
|
let tree_id1 = testutils::create_random_tree(&repo1);
|
|
let tree_id2 = testutils::create_random_tree(&repo1);
|
|
let tree_id3 = testutils::create_random_tree(&repo1);
|
|
let tree1 = repo1
|
|
.store()
|
|
.get_tree(&RepoPath::root(), &tree_id1)
|
|
.unwrap();
|
|
let tree2 = repo1
|
|
.store()
|
|
.get_tree(&RepoPath::root(), &tree_id2)
|
|
.unwrap();
|
|
let tree3 = repo1
|
|
.store()
|
|
.get_tree(&RepoPath::root(), &tree_id3)
|
|
.unwrap();
|
|
|
|
// Check out tree1
|
|
let wc1 = test_workspace1.workspace.working_copy_mut();
|
|
// The operation ID is not correct, but that doesn't matter for this test
|
|
wc1.check_out(repo1.op_id().clone(), None, &tree1).unwrap();
|
|
|
|
// Check out tree2 from another process (simulated by another workspace
|
|
// instance)
|
|
let mut workspace2 =
|
|
Workspace::load(&settings, &workspace1_root, &StoreFactories::default()).unwrap();
|
|
workspace2
|
|
.working_copy_mut()
|
|
.check_out(repo1.op_id().clone(), Some(&tree_id1), &tree2)
|
|
.unwrap();
|
|
|
|
// Checking out another tree (via the first repo instance) should now fail.
|
|
assert_matches!(
|
|
wc1.check_out(repo1.op_id().clone(), Some(&tree_id1), &tree3),
|
|
Err(CheckoutError::ConcurrentCheckout)
|
|
);
|
|
|
|
// Check that the tree2 is still checked out on disk.
|
|
let workspace3 =
|
|
Workspace::load(&settings, &workspace1_root, &StoreFactories::default()).unwrap();
|
|
assert_eq!(
|
|
workspace3.working_copy().current_tree_id().unwrap(),
|
|
&tree_id2
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_checkout_parallel() {
|
|
// Test that concurrent checkouts by different processes (simulated by using
|
|
// different repo instances) is safe.
|
|
let settings = testutils::user_settings();
|
|
let mut test_workspace = TestWorkspace::init(&settings, true);
|
|
let repo = &test_workspace.repo;
|
|
let workspace_root = test_workspace.workspace.workspace_root().clone();
|
|
|
|
let num_threads = max(num_cpus::get(), 4);
|
|
let mut tree_ids = vec![];
|
|
for i in 0..num_threads {
|
|
let path = RepoPath::from_internal_string(format!("file{i}").as_str());
|
|
let tree = testutils::create_tree(repo, &[(&path, "contents")]);
|
|
tree_ids.push(tree.id().clone());
|
|
}
|
|
|
|
// Create another tree just so we can test the update stats reliably from the
|
|
// first update
|
|
let tree = testutils::create_tree(
|
|
repo,
|
|
&[(&RepoPath::from_internal_string("other file"), "contents")],
|
|
);
|
|
test_workspace
|
|
.workspace
|
|
.working_copy_mut()
|
|
.check_out(repo.op_id().clone(), None, &tree)
|
|
.unwrap();
|
|
|
|
thread::scope(|s| {
|
|
for tree_id in &tree_ids {
|
|
let op_id = repo.op_id().clone();
|
|
let tree_ids = tree_ids.clone();
|
|
let tree_id = tree_id.clone();
|
|
let settings = settings.clone();
|
|
let workspace_root = workspace_root.clone();
|
|
s.spawn(move || {
|
|
let mut workspace =
|
|
Workspace::load(&settings, &workspace_root, &StoreFactories::default())
|
|
.unwrap();
|
|
let tree = workspace
|
|
.repo_loader()
|
|
.store()
|
|
.get_tree(&RepoPath::root(), &tree_id)
|
|
.unwrap();
|
|
// The operation ID is not correct, but that doesn't matter for this test
|
|
let stats = workspace
|
|
.working_copy_mut()
|
|
.check_out(op_id, None, &tree)
|
|
.unwrap();
|
|
assert_eq!(stats.updated_files, 0);
|
|
assert_eq!(stats.added_files, 1);
|
|
assert_eq!(stats.removed_files, 1);
|
|
// Check that the working copy contains one of the trees. We may see a
|
|
// different tree than the one we just checked out, but since
|
|
// write_tree() should take the same lock as check_out(), write_tree()
|
|
// should never produce a different tree.
|
|
let mut locked_wc = workspace.working_copy_mut().start_mutation().unwrap();
|
|
let new_tree_id = locked_wc
|
|
.snapshot(SnapshotOptions::empty_for_test())
|
|
.unwrap();
|
|
locked_wc.discard();
|
|
assert!(tree_ids.contains(&new_tree_id));
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_racy_checkout() {
|
|
let settings = testutils::user_settings();
|
|
let mut test_workspace = TestWorkspace::init(&settings, true);
|
|
let repo = &test_workspace.repo;
|
|
let workspace_root = test_workspace.workspace.workspace_root().clone();
|
|
|
|
let path = RepoPath::from_internal_string("file");
|
|
let tree = testutils::create_tree(repo, &[(&path, "1")]);
|
|
|
|
fn snapshot(workspace: &mut Workspace) -> TreeId {
|
|
let mut locked_wc = workspace.working_copy_mut().start_mutation().unwrap();
|
|
let tree_id = locked_wc
|
|
.snapshot(SnapshotOptions::empty_for_test())
|
|
.unwrap();
|
|
locked_wc.finish(OperationId::from_hex("abc123")).unwrap();
|
|
tree_id
|
|
}
|
|
|
|
let mut num_matches = 0;
|
|
for _ in 0..100 {
|
|
let wc = test_workspace.workspace.working_copy_mut();
|
|
wc.check_out(repo.op_id().clone(), None, &tree).unwrap();
|
|
assert_eq!(
|
|
std::fs::read(path.to_fs_path(&workspace_root)).unwrap(),
|
|
b"1".to_vec()
|
|
);
|
|
// A file written right after checkout (hopefully, from the test's perspective,
|
|
// within the file system timestamp granularity) is detected as changed.
|
|
write_working_copy_file(&workspace_root, &path, "x");
|
|
let modified_tree_id = snapshot(&mut test_workspace.workspace);
|
|
if modified_tree_id == *tree.id() {
|
|
num_matches += 1;
|
|
}
|
|
// Reset the state for the next round
|
|
write_working_copy_file(&workspace_root, &path, "1");
|
|
}
|
|
assert_eq!(num_matches, 0);
|
|
}
|