mirror of
https://github.com/jj-vcs/jj.git
synced 2025-12-23 06:01:01 +00:00
Running "cargo insta test --workspace", results in numerous warnings: Snapshot test passes but the existing value is in a legacy format. Please run `cargo insta test --force-update-snapshots` to update to a newer format. This commit is the result of running the suggested command.
363 lines
12 KiB
Rust
363 lines
12 KiB
Rust
// Copyright 2022 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 itertools::Itertools as _;
|
|
|
|
use crate::common::CommandOutput;
|
|
use crate::common::TestEnvironment;
|
|
use crate::common::TestWorkDir;
|
|
|
|
#[test]
|
|
fn test_concurrent_operation_divergence() {
|
|
let test_env = TestEnvironment::default();
|
|
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
|
|
let work_dir = test_env.work_dir("repo");
|
|
|
|
work_dir.run_jj(["describe", "-m", "message 1"]).success();
|
|
work_dir
|
|
.run_jj(["describe", "-m", "message 2", "--at-op", "@-"])
|
|
.success();
|
|
|
|
// "--at-op=@" disables op heads merging, and prints head operation ids.
|
|
let output = work_dir.run_jj(["op", "log", "--at-op=@"]);
|
|
insta::assert_snapshot!(output, @r#"
|
|
------- stderr -------
|
|
Error: The "@" expression resolved to more than one operation
|
|
Hint: Try specifying one of the operations by ID: b2cffe4f3026, d8ced2ea64a8
|
|
[EOF]
|
|
[exit status: 1]
|
|
"#);
|
|
|
|
// "op log --at-op" should work without merging the head operations
|
|
let output = work_dir.run_jj(["op", "log", "--at-op=d8ced2ea64a8"]);
|
|
insta::assert_snapshot!(output, @r"
|
|
@ d8ced2ea64a8 test-username@host.example.com 2001-02-03 04:05:09.000 +07:00 - 2001-02-03 04:05:09.000 +07:00
|
|
│ describe commit e8849ae12c709f2321908879bc724fdb2ab8a781
|
|
│ args: jj describe -m 'message 2' --at-op @-
|
|
○ 8f47435a3990 test-username@host.example.com 2001-02-03 04:05:07.000 +07:00 - 2001-02-03 04:05:07.000 +07:00
|
|
│ add workspace 'default'
|
|
○ 000000000000 root()
|
|
[EOF]
|
|
");
|
|
|
|
// We should be informed about the concurrent modification
|
|
let output = get_log_output(&work_dir);
|
|
insta::assert_snapshot!(output, @r"
|
|
@ message 1
|
|
│ ○ message 2
|
|
├─╯
|
|
◆
|
|
[EOF]
|
|
------- stderr -------
|
|
Concurrent modification detected, resolving automatically.
|
|
[EOF]
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn test_concurrent_operations_auto_rebase() {
|
|
let test_env = TestEnvironment::default();
|
|
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
|
|
let work_dir = test_env.work_dir("repo");
|
|
|
|
work_dir.write_file("file", "contents");
|
|
work_dir.run_jj(["describe", "-m", "initial"]).success();
|
|
work_dir.run_jj(["describe", "-m", "rewritten"]).success();
|
|
work_dir
|
|
.run_jj(["new", "--at-op=@-", "-m", "new child"])
|
|
.success();
|
|
|
|
// We should be informed about the concurrent modification
|
|
let output = get_log_output(&work_dir);
|
|
insta::assert_snapshot!(output, @r"
|
|
○ new child
|
|
@ rewritten
|
|
◆
|
|
[EOF]
|
|
------- stderr -------
|
|
Concurrent modification detected, resolving automatically.
|
|
Rebased 1 descendant commits onto commits rewritten by other operation
|
|
[EOF]
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn test_concurrent_operations_wc_modified() {
|
|
let test_env = TestEnvironment::default();
|
|
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
|
|
let work_dir = test_env.work_dir("repo");
|
|
|
|
work_dir.write_file("file", "contents\n");
|
|
work_dir.run_jj(["describe", "-m", "initial"]).success();
|
|
work_dir.run_jj(["new", "-m", "new child1"]).success();
|
|
work_dir
|
|
.run_jj(["new", "--at-op=@-", "-m", "new child2"])
|
|
.success();
|
|
work_dir.write_file("file", "modified\n");
|
|
|
|
// We should be informed about the concurrent modification
|
|
let output = get_log_output(&work_dir);
|
|
insta::assert_snapshot!(output, @r"
|
|
@ new child1
|
|
│ ○ new child2
|
|
├─╯
|
|
○ initial
|
|
◆
|
|
[EOF]
|
|
------- stderr -------
|
|
Concurrent modification detected, resolving automatically.
|
|
[EOF]
|
|
");
|
|
let output = work_dir.run_jj(["diff", "--git"]);
|
|
insta::assert_snapshot!(output, @r"
|
|
diff --git a/file b/file
|
|
index 12f00e90b6..2e0996000b 100644
|
|
--- a/file
|
|
+++ b/file
|
|
@@ -1,1 +1,1 @@
|
|
-contents
|
|
+modified
|
|
[EOF]
|
|
");
|
|
|
|
// The working copy should be committed after merging the operations
|
|
let output = work_dir.run_jj(["op", "log", "-Tdescription"]);
|
|
insta::assert_snapshot!(output, @r"
|
|
@ snapshot working copy
|
|
○ reconcile divergent operations
|
|
├─╮
|
|
○ │ new empty commit
|
|
│ ○ new empty commit
|
|
├─╯
|
|
○ describe commit 9a462e35578a347e6a3951bf7a58ad7146959a8b
|
|
○ snapshot working copy
|
|
○ add workspace 'default'
|
|
○
|
|
[EOF]
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn test_concurrent_snapshot_wc_reloadable() {
|
|
let test_env = TestEnvironment::default();
|
|
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
|
|
let work_dir = test_env.work_dir("repo");
|
|
let op_heads_dir = work_dir
|
|
.root()
|
|
.join(".jj")
|
|
.join("repo")
|
|
.join("op_heads")
|
|
.join("heads");
|
|
|
|
work_dir.write_file("base", "");
|
|
work_dir.run_jj(["commit", "-m", "initial"]).success();
|
|
|
|
// Create new commit and checkout it.
|
|
work_dir.write_file("child1", "");
|
|
work_dir.run_jj(["commit", "-m", "new child1"]).success();
|
|
|
|
let template = r#"id.short() ++ "\n" ++ description ++ "\n" ++ tags"#;
|
|
let output = work_dir.run_jj(["op", "log", "-T", template]);
|
|
insta::assert_snapshot!(output, @r"
|
|
@ a631dcf37fea
|
|
│ commit c91a0909a9d3f3d8392ba9fab88f4b40fc0810ee
|
|
│ args: jj commit -m 'new child1'
|
|
○ 2b8e6f8683dc
|
|
│ snapshot working copy
|
|
│ args: jj commit -m 'new child1'
|
|
○ 2e1c4ffb74ca
|
|
│ commit 9af4c151edead0304de97ce3a0b414552921a425
|
|
│ args: jj commit -m initial
|
|
○ cfe73d1664ae
|
|
│ snapshot working copy
|
|
│ args: jj commit -m initial
|
|
○ 8f47435a3990
|
|
│ add workspace 'default'
|
|
○ 000000000000
|
|
|
|
[EOF]
|
|
");
|
|
let template = r#"id ++ "\n""#;
|
|
let output = work_dir.run_jj(["op", "log", "--no-graph", "-T", template]);
|
|
let [op_id_after_snapshot, _, op_id_before_snapshot] =
|
|
output.stdout.raw().lines().next_array().unwrap();
|
|
insta::assert_snapshot!(op_id_after_snapshot[..12], @"a631dcf37fea");
|
|
insta::assert_snapshot!(op_id_before_snapshot[..12], @"2e1c4ffb74ca");
|
|
|
|
// Simulate a concurrent operation that began from the "initial" operation
|
|
// (before the "child1" snapshot) but finished after the "child1"
|
|
// snapshot and commit.
|
|
std::fs::rename(
|
|
op_heads_dir.join(op_id_after_snapshot),
|
|
op_heads_dir.join(op_id_before_snapshot),
|
|
)
|
|
.unwrap();
|
|
work_dir.write_file("child2", "");
|
|
let output = work_dir.run_jj(["describe", "-m", "new child2"]);
|
|
insta::assert_snapshot!(output, @r"
|
|
------- stderr -------
|
|
Working copy (@) now at: kkmpptxz 493da83e new child2
|
|
Parent commit (@-) : rlvkpnrz 15bd889d new child1
|
|
[EOF]
|
|
");
|
|
|
|
// Since the repo can be reloaded before snapshotting, "child2" should be
|
|
// a child of "child1", not of "initial".
|
|
let output = work_dir.run_jj(["log", "-T", "description", "-s"]);
|
|
insta::assert_snapshot!(output, @r"
|
|
@ new child2
|
|
│ A child2
|
|
○ new child1
|
|
│ A child1
|
|
○ initial
|
|
│ A base
|
|
◆
|
|
[EOF]
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn test_git_head_race_condition() {
|
|
// Test for race condition where concurrent jj processes create divergent
|
|
// operations when importing/exporting Git HEAD. This test spawns two
|
|
// processes in parallel: one running `jj debug snapshot` repeatedly
|
|
// (which imports Git HEAD) and another running `jj next/prev` repeatedly
|
|
// (which exports Git HEAD). Without the fix, this would create divergent
|
|
// operations.
|
|
|
|
let test_env = TestEnvironment::default();
|
|
test_env
|
|
.run_jj_in(".", ["git", "init", "--colocate", "repo"])
|
|
.success();
|
|
let work_dir = test_env.work_dir("repo");
|
|
|
|
// Create a large initial working copy to make snapshotting slower
|
|
for j in 0..200 {
|
|
let filename = format!("file_{j}");
|
|
let content = format!("content for file {j}\n").repeat(100);
|
|
work_dir.write_file(&filename, &content);
|
|
}
|
|
work_dir.run_jj(["commit", "-m", "initial"]).success();
|
|
|
|
// Remember the initial commit for later verification
|
|
let initial_commit = work_dir
|
|
.run_jj(["log", "--no-graph", "-T=commit_id", "-r=@-"])
|
|
.success()
|
|
.stdout
|
|
.into_raw();
|
|
let initial_commit = initial_commit.trim().to_owned();
|
|
|
|
// Create additional commits to iterate through with jj next/prev
|
|
for i in 0..10 {
|
|
for j in 0..50 {
|
|
let filename = format!("file_{i}_{j}");
|
|
let content = format!("content for commit {i} file {j}\n").repeat(100);
|
|
work_dir.write_file(&filename, &content);
|
|
}
|
|
work_dir
|
|
.run_jj(["commit", "-m", &format!("commit {i}")])
|
|
.success();
|
|
}
|
|
|
|
// Extract environment from TestEnvironment to use in spawned processes
|
|
let base_cmd = test_env.new_jj_cmd();
|
|
let jj_bin = base_cmd.get_program().to_owned();
|
|
let base_env: Vec<_> = base_cmd
|
|
.get_envs()
|
|
.filter_map(|(k, v)| v.map(|v| (k.to_owned(), v.to_owned())))
|
|
// Filter out timestamp and randomness seed so each command gets different values
|
|
.filter(|(k, _)| k != "JJ_TIMESTAMP" && k != "JJ_OP_TIMESTAMP" && k != "JJ_RANDOMNESS_SEED")
|
|
.collect();
|
|
|
|
let repo_path = work_dir.root();
|
|
let duration = std::time::Duration::from_secs(5);
|
|
let start = std::time::Instant::now();
|
|
|
|
std::thread::scope(|s| {
|
|
s.spawn(|| {
|
|
while start.elapsed() < duration {
|
|
// Snapshot repeatedly without delay
|
|
let mut cmd = std::process::Command::new(&jj_bin);
|
|
cmd.current_dir(repo_path);
|
|
cmd.args(["debug", "snapshot"]);
|
|
for (key, value) in &base_env {
|
|
cmd.env(key, value);
|
|
}
|
|
let output = cmd.output().expect("Failed to spawn jj");
|
|
assert!(output.status.success(), "jj debug snapshot failed");
|
|
}
|
|
});
|
|
|
|
s.spawn(|| {
|
|
let mut count = 0;
|
|
while start.elapsed() < duration {
|
|
// Go through commit history back and forth
|
|
let direction = if count % 20 < 10 { "prev" } else { "next" };
|
|
let mut cmd = std::process::Command::new(&jj_bin);
|
|
cmd.current_dir(repo_path);
|
|
cmd.arg(direction);
|
|
for (key, value) in &base_env {
|
|
cmd.env(key, value);
|
|
}
|
|
let output = cmd.output().expect("Failed to spawn jj");
|
|
assert!(output.status.success(), "jj {direction} failed");
|
|
count += 1;
|
|
}
|
|
});
|
|
});
|
|
|
|
const IMPORT_GIT_HEAD: &str = "import git head";
|
|
|
|
// Check for concurrent operations
|
|
let output = work_dir.run_jj(["op", "log", "-T", "description", "--ignore-working-copy"]);
|
|
let concurrent_count = output
|
|
.stdout
|
|
.raw()
|
|
.lines()
|
|
.filter(|line| line.contains(IMPORT_GIT_HEAD))
|
|
.count();
|
|
|
|
if concurrent_count > 0 {
|
|
eprintln!("Found {concurrent_count} concurrent operations:");
|
|
eprintln!("{}", output.stdout.raw());
|
|
panic!("Race condition detected: {concurrent_count} concurrent operations found");
|
|
}
|
|
|
|
// Verify the operation description for importing Git HEAD hasn't changed
|
|
// First ensure we're not already on the initial commit (prev/next loop may have
|
|
// ended there)
|
|
work_dir.run_jj(["new"]).success();
|
|
// Use git to checkout the initial commit, then trigger import
|
|
std::process::Command::new("git")
|
|
.current_dir(work_dir.root())
|
|
.args(["checkout", "-q", &initial_commit])
|
|
.status()
|
|
.unwrap();
|
|
|
|
let last_op = work_dir
|
|
.run_jj(["op", "log", "-T", "description", "--limit=1"])
|
|
.success()
|
|
.stdout
|
|
.into_raw();
|
|
|
|
assert!(
|
|
last_op.contains(IMPORT_GIT_HEAD),
|
|
"Expected last operation to contain '{IMPORT_GIT_HEAD}', got: {last_op:?}"
|
|
);
|
|
}
|
|
|
|
#[must_use]
|
|
fn get_log_output(work_dir: &TestWorkDir) -> CommandOutput {
|
|
work_dir.run_jj(["log", "-T", "description"])
|
|
}
|