jj/lib/tests/test_default_revset_graph_iterator.rs
Yuya Nishihara 0fcc13a6f4 revset: make resolve() return different type describing evaluation plan
New ResolvedExpression enum ensures that the evaluation engine doesn't have
to know the symbol resolution details. In this commit, I've moved Filter
and NotIn resolution to resolve_visibility(). Implicit All/VisibleHeads
resolution will be migrated later.

It's tempting to combine resolve_symbols() and resolve_visibility() to get
rid of panic!()s, but the resolution might have to be two passes to first
resolve&collect explicit commit ids, and then substitute "all()" with
"(:visible_heads())|commit_id|..". It's also possible to apply some tree
transformation after symbol resolution.
2023-04-10 00:39:58 +09:00

337 lines
13 KiB
Rust

// Copyright 2021 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;
use jujutsu_lib::commit::Commit;
use jujutsu_lib::default_index_store::ReadonlyIndexImpl;
use jujutsu_lib::default_revset_engine::{evaluate, RevsetImpl};
use jujutsu_lib::repo::Repo;
use jujutsu_lib::revset::{ResolvedExpression, RevsetGraphEdge};
use test_case::test_case;
use testutils::{CommitGraphBuilder, TestRepo};
fn revset_for_commits<'index>(repo: &'index dyn Repo, commits: &[&Commit]) -> RevsetImpl<'index> {
let index = repo
.index()
.as_any()
.downcast_ref::<ReadonlyIndexImpl>()
.unwrap();
let expression =
ResolvedExpression::Commits(commits.iter().map(|commit| commit.id().clone()).collect());
evaluate(
&expression,
repo.store(),
index,
index.as_composite(),
&repo.view().heads().iter().cloned().collect_vec(),
)
.unwrap()
}
fn direct(commit: &Commit) -> RevsetGraphEdge {
RevsetGraphEdge::direct(commit.id().clone())
}
fn indirect(commit: &Commit) -> RevsetGraphEdge {
RevsetGraphEdge::indirect(commit.id().clone())
}
fn missing(commit: &Commit) -> RevsetGraphEdge {
RevsetGraphEdge::missing(commit.id().clone())
}
#[test_case(false ; "keep transitive edges")]
#[test_case(true ; "skip transitive edges")]
fn test_graph_iterator_linearized(skip_transitive_edges: bool) {
let settings = testutils::user_settings();
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
// Tests that a fork and a merge becomes a single edge:
// D
// |\ D
// b c => :
// |/ A
// A ~
// |
// root
let mut tx = repo.start_transaction(&settings, "test");
let mut graph_builder = CommitGraphBuilder::new(&settings, tx.mut_repo());
let commit_a = graph_builder.initial_commit();
let commit_b = graph_builder.commit_with_parents(&[&commit_a]);
let commit_c = graph_builder.commit_with_parents(&[&commit_a]);
let commit_d = graph_builder.commit_with_parents(&[&commit_b, &commit_c]);
let repo = tx.commit();
let root_commit = repo.store().root_commit();
let revset = revset_for_commits(repo.as_ref(), &[&commit_a, &commit_d]);
let commits = revset
.iter_graph_impl()
.set_skip_transitive_edges(skip_transitive_edges)
.collect_vec();
assert_eq!(commits.len(), 2);
assert_eq!(commits[0].0, *commit_d.id());
assert_eq!(commits[1].0, *commit_a.id());
assert_eq!(commits[0].1, vec![indirect(&commit_a)]);
assert_eq!(commits[1].1, vec![missing(&root_commit)]);
}
#[test_case(false ; "keep transitive edges")]
#[test_case(true ; "skip transitive edges")]
fn test_graph_iterator_virtual_octopus(skip_transitive_edges: bool) {
let settings = testutils::user_settings();
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
// Tests that merges outside the set can result in more parent edges than there
// was in the input:
//
// F
// |\
// d e F
// |\|\ => /|\
// A B C A B C
// \|/ ~ ~ ~
// root
let mut tx = repo.start_transaction(&settings, "test");
let mut graph_builder = CommitGraphBuilder::new(&settings, tx.mut_repo());
let commit_a = graph_builder.initial_commit();
let commit_b = graph_builder.initial_commit();
let commit_c = graph_builder.initial_commit();
let commit_d = graph_builder.commit_with_parents(&[&commit_a, &commit_b]);
let commit_e = graph_builder.commit_with_parents(&[&commit_b, &commit_c]);
let commit_f = graph_builder.commit_with_parents(&[&commit_d, &commit_e]);
let repo = tx.commit();
let root_commit = repo.store().root_commit();
let revset = revset_for_commits(repo.as_ref(), &[&commit_a, &commit_b, &commit_c, &commit_f]);
let commits = revset
.iter_graph_impl()
.set_skip_transitive_edges(skip_transitive_edges)
.collect_vec();
assert_eq!(commits.len(), 4);
assert_eq!(commits[0].0, *commit_f.id());
assert_eq!(commits[1].0, *commit_c.id());
assert_eq!(commits[2].0, *commit_b.id());
assert_eq!(commits[3].0, *commit_a.id());
assert_eq!(
commits[0].1,
vec![
indirect(&commit_c),
indirect(&commit_b),
indirect(&commit_a),
]
);
assert_eq!(commits[1].1, vec![missing(&root_commit)]);
assert_eq!(commits[2].1, vec![missing(&root_commit)]);
assert_eq!(commits[3].1, vec![missing(&root_commit)]);
}
#[test_case(false ; "keep transitive edges")]
#[test_case(true ; "skip transitive edges")]
fn test_graph_iterator_simple_fork(skip_transitive_edges: bool) {
let settings = testutils::user_settings();
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
// Tests that the branch with "C" gets emitted correctly:
// E
// |
// d
// | C E C
// |/ |/
// b => A
// | ~
// A
// |
// root
let mut tx = repo.start_transaction(&settings, "test");
let mut graph_builder = CommitGraphBuilder::new(&settings, tx.mut_repo());
let commit_a = graph_builder.initial_commit();
let commit_b = graph_builder.commit_with_parents(&[&commit_a]);
let commit_c = graph_builder.commit_with_parents(&[&commit_b]);
let commit_d = graph_builder.commit_with_parents(&[&commit_b]);
let commit_e = graph_builder.commit_with_parents(&[&commit_d]);
let repo = tx.commit();
let root_commit = repo.store().root_commit();
let revset = revset_for_commits(repo.as_ref(), &[&commit_a, &commit_c, &commit_e]);
let commits = revset
.iter_graph_impl()
.set_skip_transitive_edges(skip_transitive_edges)
.collect_vec();
assert_eq!(commits.len(), 3);
assert_eq!(commits[0].0, *commit_e.id());
assert_eq!(commits[1].0, *commit_c.id());
assert_eq!(commits[2].0, *commit_a.id());
assert_eq!(commits[0].1, vec![indirect(&commit_a)]);
assert_eq!(commits[1].1, vec![indirect(&commit_a)]);
assert_eq!(commits[2].1, vec![missing(&root_commit)]);
}
#[test_case(false ; "keep transitive edges")]
#[test_case(true ; "skip transitive edges")]
fn test_graph_iterator_multiple_missing(skip_transitive_edges: bool) {
let settings = testutils::user_settings();
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
// Tests that we get missing edges to "a" and "c" and not just one missing edge
// to the root.
// F
// / \ F
// d e => /|\
// |\ /| ~ B ~
// a B c ~
// \|/
// root
let mut tx = repo.start_transaction(&settings, "test");
let mut graph_builder = CommitGraphBuilder::new(&settings, tx.mut_repo());
let commit_a = graph_builder.initial_commit();
let commit_b = graph_builder.initial_commit();
let commit_c = graph_builder.initial_commit();
let commit_d = graph_builder.commit_with_parents(&[&commit_a, &commit_b]);
let commit_e = graph_builder.commit_with_parents(&[&commit_b, &commit_c]);
let commit_f = graph_builder.commit_with_parents(&[&commit_d, &commit_e]);
let repo = tx.commit();
let root_commit = repo.store().root_commit();
let revset = revset_for_commits(repo.as_ref(), &[&commit_b, &commit_f]);
let commits = revset
.iter_graph_impl()
.set_skip_transitive_edges(skip_transitive_edges)
.collect_vec();
assert_eq!(commits.len(), 2);
assert_eq!(commits[0].0, *commit_f.id());
assert_eq!(commits[1].0, *commit_b.id());
assert_eq!(
commits[0].1,
vec![missing(&commit_c), indirect(&commit_b), missing(&commit_a),]
);
assert_eq!(commits[1].1, vec![missing(&root_commit)]);
}
#[test_case(false ; "keep transitive edges")]
#[test_case(true ; "skip transitive edges")]
fn test_graph_iterator_edge_to_ancestor(skip_transitive_edges: bool) {
let settings = testutils::user_settings();
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
// Tests that we get both an edge from F to D and to D's ancestor C if we keep
// transitive edges and only the edge from F to D if we skip transitive
// edges:
// F F
// |\ |\
// D e D :
// |\| => |\:
// b C ~ C
// | ~
// a
// |
// root
let mut tx = repo.start_transaction(&settings, "test");
let mut graph_builder = CommitGraphBuilder::new(&settings, tx.mut_repo());
let commit_a = graph_builder.initial_commit();
let commit_b = graph_builder.initial_commit();
let commit_c = graph_builder.commit_with_parents(&[&commit_a]);
let commit_d = graph_builder.commit_with_parents(&[&commit_b, &commit_c]);
let commit_e = graph_builder.commit_with_parents(&[&commit_c]);
let commit_f = graph_builder.commit_with_parents(&[&commit_d, &commit_e]);
let repo = tx.commit();
let revset = revset_for_commits(repo.as_ref(), &[&commit_c, &commit_d, &commit_f]);
let commits = revset
.iter_graph_impl()
.set_skip_transitive_edges(skip_transitive_edges)
.collect_vec();
assert_eq!(commits.len(), 3);
assert_eq!(commits[0].0, *commit_f.id());
assert_eq!(commits[1].0, *commit_d.id());
assert_eq!(commits[2].0, *commit_c.id());
if skip_transitive_edges {
assert_eq!(commits[0].1, vec![direct(&commit_d)]);
} else {
assert_eq!(commits[0].1, vec![direct(&commit_d), indirect(&commit_c),]);
}
assert_eq!(commits[1].1, vec![direct(&commit_c), missing(&commit_b),]);
assert_eq!(commits[2].1, vec![missing(&commit_a)]);
}
#[test_case(false ; "keep transitive edges")]
#[test_case(true ; "skip transitive edges")]
fn test_graph_iterator_edge_escapes_from_(skip_transitive_edges: bool) {
let settings = testutils::user_settings();
let test_repo = TestRepo::init(true);
let repo = &test_repo.repo;
// Tests a more complex case for skipping transitive edges.
// J
// /|
// | i J
// | |\ /:
// | | H | H
// G | | G :
// | e f => : D
// | \|\ :/
// | D | A
// \ / c |
// b / root
// |/
// A
// |
// root
let mut tx = repo.start_transaction(&settings, "test");
let mut graph_builder = CommitGraphBuilder::new(&settings, tx.mut_repo());
let commit_a = graph_builder.initial_commit();
let commit_b = graph_builder.commit_with_parents(&[&commit_a]);
let commit_c = graph_builder.commit_with_parents(&[&commit_a]);
let commit_d = graph_builder.commit_with_parents(&[&commit_b]);
let commit_e = graph_builder.commit_with_parents(&[&commit_d]);
let commit_f = graph_builder.commit_with_parents(&[&commit_d, &commit_c]);
let commit_g = graph_builder.commit_with_parents(&[&commit_b]);
let commit_h = graph_builder.commit_with_parents(&[&commit_f]);
let commit_i = graph_builder.commit_with_parents(&[&commit_e, &commit_h]);
let commit_j = graph_builder.commit_with_parents(&[&commit_g, &commit_i]);
let repo = tx.commit();
let root_commit = repo.store().root_commit();
let revset = revset_for_commits(
repo.as_ref(),
&[&commit_a, &commit_d, &commit_g, &commit_h, &commit_j],
);
let commits = revset
.iter_graph_impl()
.set_skip_transitive_edges(skip_transitive_edges)
.collect_vec();
assert_eq!(commits.len(), 5);
assert_eq!(commits[0].0, *commit_j.id());
assert_eq!(commits[1].0, *commit_h.id());
assert_eq!(commits[2].0, *commit_g.id());
assert_eq!(commits[3].0, *commit_d.id());
assert_eq!(commits[4].0, *commit_a.id());
if skip_transitive_edges {
assert_eq!(commits[0].1, vec![indirect(&commit_h), direct(&commit_g),]);
assert_eq!(commits[1].1, vec![indirect(&commit_d)]);
} else {
assert_eq!(
commits[0].1,
vec![indirect(&commit_h), direct(&commit_g), indirect(&commit_d),]
);
assert_eq!(commits[1].1, vec![indirect(&commit_d), indirect(&commit_a)]);
}
assert_eq!(commits[2].1, vec![indirect(&commit_a)]);
assert_eq!(commits[3].1, vec![indirect(&commit_a)]);
assert_eq!(commits[4].1, vec![missing(&root_commit)]);
}