rust/hg-core/src/ancestors.rs
changeset 40271 dbc28c91f7ff
child 40300 72b94f946e90
equal deleted inserted replaced
40270:8783710b1d58 40271:dbc28c91f7ff
       
     1 // ancestors.rs
       
     2 //
       
     3 // Copyright 2018 Georges Racinet <gracinet@anybox.fr>
       
     4 //
       
     5 // This software may be used and distributed according to the terms of the
       
     6 // GNU General Public License version 2 or any later version.
       
     7 
       
     8 //! Rust versions of generic DAG ancestors algorithms for Mercurial
       
     9 
       
    10 use super::{Graph, GraphError, Revision, NULL_REVISION};
       
    11 use std::collections::{BinaryHeap, HashSet};
       
    12 
       
    13 /// Iterator over the ancestors of a given list of revisions
       
    14 /// This is a generic type, defined and implemented for any Graph, so that
       
    15 /// it's easy to
       
    16 ///
       
    17 /// - unit test in pure Rust
       
    18 /// - bind to main Mercurial code, potentially in several ways and have these
       
    19 ///   bindings evolve over time
       
    20 pub struct AncestorsIterator<G: Graph> {
       
    21     graph: G,
       
    22     visit: BinaryHeap<Revision>,
       
    23     seen: HashSet<Revision>,
       
    24     stoprev: Revision,
       
    25 }
       
    26 
       
    27 impl<G: Graph> AncestorsIterator<G> {
       
    28     /// Constructor.
       
    29     ///
       
    30     /// if `inclusive` is true, then the init revisions are emitted in
       
    31     /// particular, otherwise iteration starts from their parents.
       
    32     pub fn new<I>(
       
    33         graph: G,
       
    34         initrevs: I,
       
    35         stoprev: Revision,
       
    36         inclusive: bool,
       
    37     ) -> Result<Self, GraphError>
       
    38     where
       
    39         I: IntoIterator<Item = Revision>,
       
    40     {
       
    41         let filtered_initrevs = initrevs.into_iter().filter(|&r| r >= stoprev);
       
    42         if inclusive {
       
    43             let visit: BinaryHeap<Revision> = filtered_initrevs.collect();
       
    44             let seen = visit.iter().map(|&x| x).collect();
       
    45             return Ok(AncestorsIterator {
       
    46                 visit: visit,
       
    47                 seen: seen,
       
    48                 stoprev: stoprev,
       
    49                 graph: graph,
       
    50             });
       
    51         }
       
    52         let mut this = AncestorsIterator {
       
    53             visit: BinaryHeap::new(),
       
    54             seen: HashSet::new(),
       
    55             stoprev: stoprev,
       
    56             graph: graph,
       
    57         };
       
    58         this.seen.insert(NULL_REVISION);
       
    59         for rev in filtered_initrevs {
       
    60             this.conditionally_push_parents(rev)?;
       
    61         }
       
    62         Ok(this)
       
    63     }
       
    64 
       
    65     #[inline]
       
    66     fn conditionally_push_rev(&mut self, rev: Revision) {
       
    67         if self.stoprev <= rev && !self.seen.contains(&rev) {
       
    68             self.seen.insert(rev);
       
    69             self.visit.push(rev);
       
    70         }
       
    71     }
       
    72 
       
    73     #[inline]
       
    74     fn conditionally_push_parents(
       
    75         &mut self,
       
    76         rev: Revision,
       
    77     ) -> Result<(), GraphError> {
       
    78         let parents = self.graph.parents(rev)?;
       
    79         self.conditionally_push_rev(parents.0);
       
    80         self.conditionally_push_rev(parents.1);
       
    81         Ok(())
       
    82     }
       
    83 }
       
    84 
       
    85 /// Main implementation.
       
    86 ///
       
    87 /// The algorithm is the same as in `_lazyancestorsiter()` from `ancestors.py`
       
    88 /// with a few non crucial differences:
       
    89 ///
       
    90 /// - there's no filtering of invalid parent revisions. Actually, it should be
       
    91 ///   consistent and more efficient to filter them from the end caller.
       
    92 /// - we don't use the equivalent of `heapq.heapreplace()`, but we should, for
       
    93 ///   the same reasons (using `peek_mut`)
       
    94 /// - we don't have the optimization for adjacent revs (case where p1 == rev-1)
       
    95 /// - we save a few pushes by comparing with `stoprev` before pushing
       
    96 ///
       
    97 /// Error treatment:
       
    98 /// We swallow the possible GraphError of conditionally_push_parents() to
       
    99 /// respect the Iterator trait in a simple manner: never emitting parents
       
   100 /// for the returned revision. We finds this good enough for now, because:
       
   101 ///
       
   102 /// - there's a good chance that invalid revisionss are fed from the start,
       
   103 ///   and `new()` doesn't swallow the error result.
       
   104 /// - this is probably what the Python implementation produces anyway, due
       
   105 ///   to filtering at each step, and Python code is currently the only
       
   106 ///   concrete caller we target, so we shouldn't need a finer error treatment
       
   107 ///   for the time being.
       
   108 impl<G: Graph> Iterator for AncestorsIterator<G> {
       
   109     type Item = Revision;
       
   110 
       
   111     fn next(&mut self) -> Option<Revision> {
       
   112         let current = match self.visit.pop() {
       
   113             None => {
       
   114                 return None;
       
   115             }
       
   116             Some(i) => i,
       
   117         };
       
   118         self.conditionally_push_parents(current).unwrap_or(());
       
   119         Some(current)
       
   120     }
       
   121 }
       
   122 
       
   123 #[cfg(test)]
       
   124 mod tests {
       
   125 
       
   126     use super::*;
       
   127 
       
   128     #[derive(Clone, Debug)]
       
   129     struct Stub;
       
   130 
       
   131     /// This is the same as the dict from test-ancestors.py
       
   132     impl Graph for Stub {
       
   133         fn parents(
       
   134             &self,
       
   135             rev: Revision,
       
   136         ) -> Result<(Revision, Revision), GraphError> {
       
   137             match rev {
       
   138                 0 => Ok((-1, -1)),
       
   139                 1 => Ok((0, -1)),
       
   140                 2 => Ok((1, -1)),
       
   141                 3 => Ok((1, -1)),
       
   142                 4 => Ok((2, -1)),
       
   143                 5 => Ok((4, -1)),
       
   144                 6 => Ok((4, -1)),
       
   145                 7 => Ok((4, -1)),
       
   146                 8 => Ok((-1, -1)),
       
   147                 9 => Ok((6, 7)),
       
   148                 10 => Ok((5, -1)),
       
   149                 11 => Ok((3, 7)),
       
   150                 12 => Ok((9, -1)),
       
   151                 13 => Ok((8, -1)),
       
   152                 r => Err(GraphError::ParentOutOfRange(r)),
       
   153             }
       
   154         }
       
   155     }
       
   156 
       
   157     fn list_ancestors<G: Graph>(
       
   158         graph: G,
       
   159         initrevs: Vec<Revision>,
       
   160         stoprev: Revision,
       
   161         inclusive: bool,
       
   162     ) -> Vec<Revision> {
       
   163         AncestorsIterator::new(graph, initrevs, stoprev, inclusive)
       
   164             .unwrap()
       
   165             .collect()
       
   166     }
       
   167 
       
   168     #[test]
       
   169     /// Same tests as test-ancestor.py, without membership
       
   170     /// (see also test-ancestor.py.out)
       
   171     fn test_list_ancestor() {
       
   172         assert_eq!(list_ancestors(Stub, vec![], 0, false), vec![]);
       
   173         assert_eq!(
       
   174             list_ancestors(Stub, vec![11, 13], 0, false),
       
   175             vec![8, 7, 4, 3, 2, 1, 0]
       
   176         );
       
   177         assert_eq!(list_ancestors(Stub, vec![1, 3], 0, false), vec![1, 0]);
       
   178         assert_eq!(
       
   179             list_ancestors(Stub, vec![11, 13], 0, true),
       
   180             vec![13, 11, 8, 7, 4, 3, 2, 1, 0]
       
   181         );
       
   182         assert_eq!(list_ancestors(Stub, vec![11, 13], 6, false), vec![8, 7]);
       
   183         assert_eq!(
       
   184             list_ancestors(Stub, vec![11, 13], 6, true),
       
   185             vec![13, 11, 8, 7]
       
   186         );
       
   187         assert_eq!(list_ancestors(Stub, vec![11, 13], 11, true), vec![13, 11]);
       
   188         assert_eq!(list_ancestors(Stub, vec![11, 13], 12, true), vec![13]);
       
   189         assert_eq!(
       
   190             list_ancestors(Stub, vec![10, 1], 0, true),
       
   191             vec![10, 5, 4, 2, 1, 0]
       
   192         );
       
   193     }
       
   194 
       
   195     #[test]
       
   196     /// Corner case that's not directly in test-ancestors.py, but
       
   197     /// that happens quite often, as demonstrated by running the whole
       
   198     /// suite.
       
   199     /// For instance, run tests/test-obsolete-checkheads.t
       
   200     fn test_nullrev_input() {
       
   201         let mut iter =
       
   202             AncestorsIterator::new(Stub, vec![-1], 0, false).unwrap();
       
   203         assert_eq!(iter.next(), None)
       
   204     }
       
   205 
       
   206     /// A corrupted Graph, supporting error handling tests
       
   207     struct Corrupted;
       
   208 
       
   209     impl Graph for Corrupted {
       
   210         fn parents(
       
   211             &self,
       
   212             rev: Revision,
       
   213         ) -> Result<(Revision, Revision), GraphError> {
       
   214             match rev {
       
   215                 1 => Ok((0, -1)),
       
   216                 r => Err(GraphError::ParentOutOfRange(r)),
       
   217             }
       
   218         }
       
   219     }
       
   220 
       
   221     #[test]
       
   222     fn test_initrev_out_of_range() {
       
   223         // inclusive=false looks up initrev's parents right away
       
   224         match AncestorsIterator::new(Stub, vec![25], 0, false) {
       
   225             Ok(_) => panic!("Should have been ParentOutOfRange"),
       
   226             Err(e) => assert_eq!(e, GraphError::ParentOutOfRange(25)),
       
   227         }
       
   228     }
       
   229 
       
   230     #[test]
       
   231     fn test_next_out_of_range() {
       
   232         // inclusive=false looks up initrev's parents right away
       
   233         let mut iter =
       
   234             AncestorsIterator::new(Corrupted, vec![1], 0, false).unwrap();
       
   235         assert_eq!(iter.next(), Some(0));
       
   236         assert_eq!(iter.next(), None);
       
   237     }
       
   238 }