362 lines · 12.1 KB · Rust
RawDownload
// 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 testutils::TestResult; use crate::common::CommandOutput;use crate::common::TestEnvironment;use crate::common::TestWorkDir; #[test]fn test_concurrent_operation_divergence() -> TestResult {    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: beccd11ae047, 4c146e78e102    [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=801df1b86ae7"]);    insta::assert_snapshot!(output, @r#"    ------- stderr -------    Error: No operation ID matching "801df1b86ae7"    [EOF]    [exit status: 1]    "#);     // We should be informed about the concurrent modification    let output = get_log_output(&work_dir);    insta::assert_snapshot!(output, @"    @  message 1    │ ○  message 2    ├─╯    ◆    [EOF]    ------- stderr -------    Concurrent modification detected, resolving automatically.    [EOF]    ");    Ok(())} #[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, @"    @  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, @"    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, @"    @  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() -> TestResult {    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" ++ attributes"#;    let output = work_dir.run_jj(["op", "log", "-T", template]);    insta::assert_snapshot!(output, @"    @  a93d75bba01d    │  commit c91a0909a9d3f3d8392ba9fab88f4b40fc0810ee    │  args: jj commit -m 'new child1'    ○  03987d7eadb6    │  snapshot working copy    │  args: jj commit -m 'new child1'    ○  a07484ee6114    │  commit 9af4c151edead0304de97ce3a0b414552921a425    │  args: jj commit -m initial    ○  ca41cb820724    │  snapshot working copy    │  args: jj commit -m initial    ○  e39dc288903d    │  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], @"a93d75bba01d");    insta::assert_snapshot!(op_id_before_snapshot[..12], @"a07484ee6114");     // 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),    )?;    work_dir.write_file("child2", "");    let output = work_dir.run_jj(["describe", "-m", "new child2"]);    insta::assert_snapshot!(output, @"    ------- 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, @"    @  new child2    │  A child2    ○  new child1    │  A child1    ○  initial    │  A base    ◆    [EOF]    ");    Ok(())} #[test]fn test_git_head_race_condition() -> TestResult {    // 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()?;     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:?}"    );    Ok(())} #[must_use]fn get_log_output(work_dir: &TestWorkDir) -> CommandOutput {    work_dir.run_jj(["log", "-T", "description"])}