267 lines · 9.9 KB · Rust
RawDownload
// Copyright 2025 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::sync::Arc; use assert_matches::assert_matches;use jj_lib::backend::CommitId;use jj_lib::bisect::BisectionResult;use jj_lib::bisect::Bisector;use jj_lib::bisect::Evaluation;use jj_lib::bisect::NextStep;use jj_lib::repo::Repo;use jj_lib::revset::ResolvedRevsetExpression;use testutils::TestRepo;use testutils::write_random_commit;use testutils::write_random_commit_with_parents; fn test_bisection<'a>(    repo: &dyn Repo,    input_range: &Arc<ResolvedRevsetExpression>,    results: impl IntoIterator<Item = (&'a CommitId, Evaluation)>,) -> BisectionResult {    let mut bisector = Bisector::new(repo, input_range.clone()).unwrap();    let mut iter = results.into_iter().enumerate();    loop {        match bisector.next_step().unwrap() {            NextStep::Evaluate(commit) => {                let (i, (expected_id, result)) =                    iter.next().expect("More commits than expected were tested");                assert_eq!(                    commit.id(),                    expected_id,                    "Attempt to test unexpected commit at iteration {i}"                );                bisector.mark(commit.id().clone(), result);            }            NextStep::Done(bisection_result) => {                assert!(iter.next().is_none(), "Finished earlier than expected");                return bisection_result;            }        }    }} #[test]fn test_bisect_empty_input() {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     let input_range = ResolvedRevsetExpression::none();    let expected_tests = [];    let result = test_bisection(repo.as_ref(), &input_range, expected_tests);    assert_matches!(result, BisectionResult::Indeterminate);} #[test]fn test_bisect_linear() {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     let mut tx = repo.start_transaction();    let root_commit = repo.store().root_commit();    let commit1 = write_random_commit(tx.repo_mut());    let commit2 = write_random_commit_with_parents(tx.repo_mut(), &[&commit1]);    let commit3 = write_random_commit_with_parents(tx.repo_mut(), &[&commit2]);    let commit4 = write_random_commit_with_parents(tx.repo_mut(), &[&commit3]);    let commit5 = write_random_commit_with_parents(tx.repo_mut(), &[&commit4]);    let commit6 = write_random_commit_with_parents(tx.repo_mut(), &[&commit5]);    let commit7 = write_random_commit_with_parents(tx.repo_mut(), &[&commit6]);     let input_range = ResolvedRevsetExpression::commit(commit7.id().clone()).ancestors();     // Root commit is the first bad commit    let expected_tests = [        (commit3.id(), Evaluation::Bad),        (commit1.id(), Evaluation::Bad),        (root_commit.id(), Evaluation::Bad),    ];    let result = test_bisection(tx.repo(), &input_range, expected_tests);    assert_eq!(result, BisectionResult::Found(vec![root_commit.clone()]));     // Commit 1 is the first bad commit    let expected_tests = [        (commit3.id(), Evaluation::Bad),        (commit1.id(), Evaluation::Bad),        (root_commit.id(), Evaluation::Good),    ];    let result = test_bisection(tx.repo(), &input_range, expected_tests);    assert_eq!(result, BisectionResult::Found(vec![commit1.clone()]));     // Commit 3 is the first bad commit    let expected_tests = [        (commit3.id(), Evaluation::Bad),        (commit1.id(), Evaluation::Good),        (commit2.id(), Evaluation::Good),    ];    let result = test_bisection(tx.repo(), &input_range, expected_tests);    assert_eq!(result, BisectionResult::Found(vec![commit3.clone()]));     // Commit 5 is the first bad commit    let expected_tests = [        (commit3.id(), Evaluation::Good),        (commit5.id(), Evaluation::Bad),        (commit4.id(), Evaluation::Good),    ];    let result = test_bisection(tx.repo(), &input_range, expected_tests);    assert_eq!(result, BisectionResult::Found(vec![commit5.clone()]));     // Commit 7 is the first bad commit    let expected_tests = [        (commit3.id(), Evaluation::Good),        (commit5.id(), Evaluation::Good),        (commit6.id(), Evaluation::Good),    ];    let result = test_bisection(tx.repo(), &input_range, expected_tests);    assert_eq!(result, BisectionResult::Found(vec![commit7.clone()]));     // Commit 2 is the first bad commit but commit 3 is skipped    let expected_tests = [        (commit3.id(), Evaluation::Skip),        (commit2.id(), Evaluation::Bad),        (root_commit.id(), Evaluation::Good),        (commit1.id(), Evaluation::Good),    ];    let result = test_bisection(tx.repo(), &input_range, expected_tests);    assert_eq!(result, BisectionResult::Found(vec![commit2.clone()]));     // Commit 4 is the first bad commit but commit 3 is skipped    let expected_tests = [        (commit3.id(), Evaluation::Skip),        (commit2.id(), Evaluation::Good),        (commit5.id(), Evaluation::Bad),        (commit4.id(), Evaluation::Bad),    ];    let result = test_bisection(tx.repo(), &input_range, expected_tests);    // TODO: Indicate in the result that we're unsure if commit4 was the first bad    // commit because the commit before it in the set was skipped.    assert_eq!(result, BisectionResult::Found(vec![commit4.clone()]));     // Commit 7 is the first bad commit but commits before 6 were skipped    // TODO: Avoid testing every commit near first skipped commit. Test e.g. commit    // 1 and commit 5 once we see that commit 3 was indeterminate.    let expected_tests = [        (commit3.id(), Evaluation::Skip),        (commit2.id(), Evaluation::Skip),        (commit4.id(), Evaluation::Skip),        (commit1.id(), Evaluation::Skip),        (commit5.id(), Evaluation::Skip),        (root_commit.id(), Evaluation::Skip),        (commit6.id(), Evaluation::Good),    ];    let result = test_bisection(tx.repo(), &input_range, expected_tests);    assert_eq!(result, BisectionResult::Found(vec![commit7.clone()]));     // Gaps in the input range are allowed    let input_range = ResolvedRevsetExpression::commits(vec![        commit7.id().clone(),        commit4.id().clone(),        commit2.id().clone(),        commit1.id().clone(),    ]);    // Commit 4 is the first bad commit    let expected_tests = [        (commit2.id(), Evaluation::Good),        (commit4.id(), Evaluation::Bad),    ];    let result = test_bisection(tx.repo(), &input_range, expected_tests);    assert_eq!(result, BisectionResult::Found(vec![commit4.clone()]));} #[test]fn test_bisect_nonlinear() {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     // 7    // |\    // 5 6    // | |    // 3 4    // | |    // 1 2    // |/    // 0    let mut tx = repo.start_transaction();    let root_commit = repo.store().root_commit();    let commit1 = write_random_commit(tx.repo_mut());    let commit2 = write_random_commit(tx.repo_mut());    let commit3 = write_random_commit_with_parents(tx.repo_mut(), &[&commit1]);    let commit4 = write_random_commit_with_parents(tx.repo_mut(), &[&commit2]);    let commit5 = write_random_commit_with_parents(tx.repo_mut(), &[&commit3]);    let commit6 = write_random_commit_with_parents(tx.repo_mut(), &[&commit4]);    let commit7 = write_random_commit_with_parents(tx.repo_mut(), &[&commit5, &commit6]);     let input_range = ResolvedRevsetExpression::commit(commit7.id().clone()).ancestors();     // Root commit is the first bad commit    let expected_tests = [        (commit3.id(), Evaluation::Bad),        (root_commit.id(), Evaluation::Bad),    ];    let result = test_bisection(tx.repo(), &input_range, expected_tests);    assert_eq!(result, BisectionResult::Found(vec![root_commit.clone()]));     // Commit 3 is the first bad commit    let expected_tests = [        (commit3.id(), Evaluation::Bad),        (root_commit.id(), Evaluation::Good),        (commit1.id(), Evaluation::Good),    ];    let result = test_bisection(tx.repo(), &input_range, expected_tests);    assert_eq!(result, BisectionResult::Found(vec![commit3.clone()]));     // Commit 4 is the first bad commit    let expected_tests = [        (commit3.id(), Evaluation::Good),        (commit4.id(), Evaluation::Bad),        (commit2.id(), Evaluation::Good),    ];    let result = test_bisection(tx.repo(), &input_range, expected_tests);    assert_eq!(result, BisectionResult::Found(vec![commit4.clone()]));     // Commit 6 is the first bad commit    let expected_tests = [        (commit3.id(), Evaluation::Good),        (commit4.id(), Evaluation::Good),        (commit5.id(), Evaluation::Good),        (commit6.id(), Evaluation::Bad),    ];    let result = test_bisection(tx.repo(), &input_range, expected_tests);    assert_eq!(result, BisectionResult::Found(vec![commit6.clone()]));} #[test]fn test_bisect_disjoint_sets() {    let test_repo = TestRepo::init();    let repo = &test_repo.repo;     // 1 2    // |/    // 0    let mut tx = repo.start_transaction();    let commit1 = write_random_commit(tx.repo_mut());    let commit2 = write_random_commit(tx.repo_mut());     let input_range =        ResolvedRevsetExpression::commits(vec![commit1.id().clone(), commit2.id().clone()]);     // Both commit 1 and commit 2 are (implicitly) the first bad commits    let expected_tests = [];    let result = test_bisection(tx.repo(), &input_range, expected_tests);    assert_eq!(        result,        BisectionResult::Found(vec![commit2.clone(), commit1.clone()])    );}