##// END OF EJS Templates
rust-discovery: implementing and exposing stats()...
Georges Racinet -
r42357:1b0be75c default
parent child Browse files
Show More
@@ -1,196 +1,209 b''
1 1 // discovery.rs
2 2 //
3 3 // Copyright 2019 Georges Racinet <georges.racinet@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Discovery operations
9 9 //!
10 10 //! This is a Rust counterpart to the `partialdiscovery` class of
11 11 //! `mercurial.setdiscovery`
12 12
13 13 use super::{Graph, GraphError, Revision};
14 14 use crate::ancestors::MissingAncestors;
15 15 use crate::dagops;
16 16 use std::collections::HashSet;
17 17
18 18 pub struct PartialDiscovery<G: Graph + Clone> {
19 19 target_heads: Option<Vec<Revision>>,
20 20 graph: G, // plays the role of self._repo
21 21 common: MissingAncestors<G>,
22 22 undecided: Option<HashSet<Revision>>,
23 23 missing: HashSet<Revision>,
24 24 }
25 25
26 pub struct DiscoveryStats {
27 pub undecided: Option<usize>,
28 }
29
26 30 impl<G: Graph + Clone> PartialDiscovery<G> {
27 31 /// Create a PartialDiscovery object, with the intent
28 32 /// of comparing our `::<target_heads>` revset to the contents of another
29 33 /// repo.
30 34 ///
31 35 /// For now `target_heads` is passed as a vector, and will be used
32 36 /// at the first call to `ensure_undecided()`.
33 37 ///
34 38 /// If we want to make the signature more flexible,
35 39 /// we'll have to make it a type argument of `PartialDiscovery` or a trait
36 40 /// object since we'll keep it in the meanwhile
37 41 pub fn new(graph: G, target_heads: Vec<Revision>) -> Self {
38 42 PartialDiscovery {
39 43 undecided: None,
40 44 target_heads: Some(target_heads),
41 45 graph: graph.clone(),
42 46 common: MissingAncestors::new(graph, vec![]),
43 47 missing: HashSet::new(),
44 48 }
45 49 }
46 50
47 51 /// Register revisions known as being common
48 52 pub fn add_common_revisions(
49 53 &mut self,
50 54 common: impl IntoIterator<Item = Revision>,
51 55 ) -> Result<(), GraphError> {
52 56 self.common.add_bases(common);
53 57 if let Some(ref mut undecided) = self.undecided {
54 58 self.common.remove_ancestors_from(undecided)?;
55 59 }
56 60 Ok(())
57 61 }
58 62
59 63 /// Register revisions known as being missing
60 64 pub fn add_missing_revisions(
61 65 &mut self,
62 66 missing: impl IntoIterator<Item = Revision>,
63 67 ) -> Result<(), GraphError> {
64 68 self.ensure_undecided()?;
65 69 let range = dagops::range(
66 70 &self.graph,
67 71 missing,
68 72 self.undecided.as_ref().unwrap().iter().cloned(),
69 73 )?;
70 74 let undecided_mut = self.undecided.as_mut().unwrap();
71 75 for missrev in range {
72 76 self.missing.insert(missrev);
73 77 undecided_mut.remove(&missrev);
74 78 }
75 79 Ok(())
76 80 }
77 81
78 82 /// Do we have any information about the peer?
79 83 pub fn has_info(&self) -> bool {
80 84 self.common.has_bases()
81 85 }
82 86
83 87 /// Did we acquire full knowledge of our Revisions that the peer has?
84 88 pub fn is_complete(&self) -> bool {
85 89 self.undecided.as_ref().map_or(false, |s| s.is_empty())
86 90 }
87 91
88 92 /// Return the heads of the currently known common set of revisions.
89 93 ///
90 94 /// If the discovery process is not complete (see `is_complete()`), the
91 95 /// caller must be aware that this is an intermediate state.
92 96 ///
93 97 /// On the other hand, if it is complete, then this is currently
94 98 /// the only way to retrieve the end results of the discovery process.
95 99 ///
96 100 /// We may introduce in the future an `into_common_heads` call that
97 101 /// would be more appropriate for normal Rust callers, dropping `self`
98 102 /// if it is complete.
99 103 pub fn common_heads(&self) -> Result<HashSet<Revision>, GraphError> {
100 104 self.common.bases_heads()
101 105 }
102 106
103 107 /// Force first computation of `self.undecided`
104 108 ///
105 109 /// After this, `self.undecided.as_ref()` and `.as_mut()` can be
106 110 /// unwrapped to get workable immutable or mutable references without
107 111 /// any panic.
108 112 ///
109 113 /// This is an imperative call instead of an access with added lazyness
110 114 /// to reduce easily the scope of mutable borrow for the caller,
111 115 /// compared to undecided(&'a mut self) -> &'a… that would keep it
112 116 /// as long as the resulting immutable one.
113 117 fn ensure_undecided(&mut self) -> Result<(), GraphError> {
114 118 if self.undecided.is_some() {
115 119 return Ok(());
116 120 }
117 121 let tgt = self.target_heads.take().unwrap();
118 122 self.undecided =
119 123 Some(self.common.missing_ancestors(tgt)?.into_iter().collect());
120 124 Ok(())
121 125 }
126
127 /// Provide statistics about the current state of the discovery process
128 pub fn stats(&self) -> DiscoveryStats {
129 DiscoveryStats {
130 undecided: self.undecided.as_ref().map(|s| s.len()),
131 }
132 }
122 133 }
123 134
124 135 #[cfg(test)]
125 136 mod tests {
126 137 use super::*;
127 138 use crate::testing::SampleGraph;
128 139
129 140 /// A PartialDiscovery as for pushing all the heads of `SampleGraph`
130 141 fn full_disco() -> PartialDiscovery<SampleGraph> {
131 142 PartialDiscovery::new(SampleGraph, vec![10, 11, 12, 13])
132 143 }
133 144
134 145 fn sorted_undecided(
135 146 disco: &PartialDiscovery<SampleGraph>,
136 147 ) -> Vec<Revision> {
137 148 let mut as_vec: Vec<Revision> =
138 149 disco.undecided.as_ref().unwrap().iter().cloned().collect();
139 150 as_vec.sort();
140 151 as_vec
141 152 }
142 153
143 154 fn sorted_missing(disco: &PartialDiscovery<SampleGraph>) -> Vec<Revision> {
144 155 let mut as_vec: Vec<Revision> =
145 156 disco.missing.iter().cloned().collect();
146 157 as_vec.sort();
147 158 as_vec
148 159 }
149 160
150 161 fn sorted_common_heads(
151 162 disco: &PartialDiscovery<SampleGraph>,
152 163 ) -> Result<Vec<Revision>, GraphError> {
153 164 let mut as_vec: Vec<Revision> =
154 165 disco.common_heads()?.iter().cloned().collect();
155 166 as_vec.sort();
156 167 Ok(as_vec)
157 168 }
158 169
159 170 #[test]
160 171 fn test_add_common_get_undecided() -> Result<(), GraphError> {
161 172 let mut disco = full_disco();
162 173 assert_eq!(disco.undecided, None);
163 174 assert!(!disco.has_info());
175 assert_eq!(disco.stats().undecided, None);
164 176
165 177 disco.add_common_revisions(vec![11, 12])?;
166 178 assert!(disco.has_info());
167 179 assert!(!disco.is_complete());
168 180 assert!(disco.missing.is_empty());
169 181
170 182 // add_common_revisions did not trigger a premature computation
171 183 // of `undecided`, let's check that and ask for them
172 184 assert_eq!(disco.undecided, None);
173 185 disco.ensure_undecided()?;
174 186 assert_eq!(sorted_undecided(&disco), vec![5, 8, 10, 13]);
187 assert_eq!(disco.stats().undecided, Some(4));
175 188 Ok(())
176 189 }
177 190
178 191 /// in this test, we pretend that our peer misses exactly (8+10)::
179 192 /// and we're comparing all our repo to it (as in a bare push)
180 193 #[test]
181 194 fn test_discovery() -> Result<(), GraphError> {
182 195 let mut disco = full_disco();
183 196 disco.add_common_revisions(vec![11, 12])?;
184 197 disco.add_missing_revisions(vec![8, 10])?;
185 198 assert_eq!(sorted_undecided(&disco), vec![5]);
186 199 assert_eq!(sorted_missing(&disco), vec![8, 10, 13]);
187 200 assert!(!disco.is_complete());
188 201
189 202 disco.add_common_revisions(vec![5])?;
190 203 assert_eq!(sorted_undecided(&disco), vec![]);
191 204 assert_eq!(sorted_missing(&disco), vec![8, 10, 13]);
192 205 assert!(disco.is_complete());
193 206 assert_eq!(sorted_common_heads(&disco)?, vec![5, 11, 12]);
194 207 Ok(())
195 208 }
196 209 }
@@ -1,114 +1,125 b''
1 1 // discovery.rs
2 2 //
3 3 // Copyright 2018 Georges Racinet <gracinet@anybox.fr>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Bindings for the `hg::discovery` module provided by the
9 9 //! `hg-core` crate. From Python, this will be seen as `rustext.discovery`
10 10 //!
11 11 //! # Classes visible from Python:
12 12 //! - [`PartialDiscover`] is the Rust implementation of
13 13 //! `mercurial.setdiscovery.partialdiscovery`.
14 14
15 15 use crate::conversion::{py_set, rev_pyiter_collect};
16 16 use cindex::Index;
17 use cpython::{ObjectProtocol, PyDict, PyModule, PyObject, PyResult, Python};
17 use cpython::{
18 ObjectProtocol, PyDict, PyModule, PyObject, PyResult, Python, ToPyObject,
19 };
18 20 use exceptions::GraphError;
19 21 use hg::discovery::PartialDiscovery as CorePartialDiscovery;
20 22 use hg::Revision;
21 23
22 24 use std::cell::RefCell;
23 25
24 26 py_class!(pub class PartialDiscovery |py| {
25 27 data inner: RefCell<Box<CorePartialDiscovery<Index>>>;
26 28
27 29 def __new__(
28 30 _cls,
29 31 index: PyObject,
30 32 targetheads: PyObject
31 33 ) -> PyResult<PartialDiscovery> {
32 34 Self::create_instance(
33 35 py,
34 36 RefCell::new(Box::new(CorePartialDiscovery::new(
35 37 Index::new(py, index)?,
36 38 rev_pyiter_collect(py, &targetheads)?,
37 39 )))
38 40 )
39 41 }
40 42
41 43 def addcommons(&self, commons: PyObject) -> PyResult<PyObject> {
42 44 let mut inner = self.inner(py).borrow_mut();
43 45 let commons_vec: Vec<Revision> = rev_pyiter_collect(py, &commons)?;
44 46 inner.add_common_revisions(commons_vec)
45 47 .map_err(|e| GraphError::pynew(py, e))?;
46 48 Ok(py.None())
47 49 }
48 50
49 51 def addmissings(&self, missings: PyObject) -> PyResult<PyObject> {
50 52 let mut inner = self.inner(py).borrow_mut();
51 53 let missings_vec: Vec<Revision> = rev_pyiter_collect(py, &missings)?;
52 54 inner.add_missing_revisions(missings_vec)
53 55 .map_err(|e| GraphError::pynew(py, e))?;
54 56 Ok(py.None())
55 57 }
56 58
57 59 def addinfo(&self, sample: PyObject) -> PyResult<PyObject> {
58 60 let mut missing: Vec<Revision> = Vec::new();
59 61 let mut common: Vec<Revision> = Vec::new();
60 62 for info in sample.iter(py)? { // info is a pair (Revision, bool)
61 63 let mut revknown = info?.iter(py)?;
62 64 let rev: Revision = revknown.next().unwrap()?.extract(py)?;
63 65 let known: bool = revknown.next().unwrap()?.extract(py)?;
64 66 if known {
65 67 common.push(rev);
66 68 } else {
67 69 missing.push(rev);
68 70 }
69 71 }
70 72 let mut inner = self.inner(py).borrow_mut();
71 73 inner.add_common_revisions(common)
72 74 .map_err(|e| GraphError::pynew(py, e))?;
73 75 inner.add_missing_revisions(missing)
74 76 .map_err(|e| GraphError::pynew(py, e))?;
75 77 Ok(py.None())
76 78 }
77 79
78 80 def hasinfo(&self) -> PyResult<bool> {
79 81 Ok(self.inner(py).borrow().has_info())
80 82 }
81 83
82 84 def iscomplete(&self) -> PyResult<bool> {
83 85 Ok(self.inner(py).borrow().is_complete())
84 86 }
85 87
88 def stats(&self) -> PyResult<PyDict> {
89 let stats = self.inner(py).borrow().stats();
90 let as_dict: PyDict = PyDict::new(py);
91 as_dict.set_item(py, "undecided",
92 stats.undecided.map(|l| l.to_py_object(py))
93 .unwrap_or_else(|| py.None()))?;
94 Ok(as_dict)
95 }
96
86 97 def commonheads(&self) -> PyResult<PyObject> {
87 98 py_set(
88 99 py,
89 100 &self.inner(py).borrow().common_heads()
90 101 .map_err(|e| GraphError::pynew(py, e))?
91 102 )
92 103 }
93 104 });
94 105
95 106 /// Create the module, with __package__ given from parent
96 107 pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> {
97 108 let dotted_name = &format!("{}.discovery", package);
98 109 let m = PyModule::new(py, dotted_name)?;
99 110 m.add(py, "__package__", package)?;
100 111 m.add(
101 112 py,
102 113 "__doc__",
103 114 "Discovery of common node sets - Rust implementation",
104 115 )?;
105 116 m.add_class::<PartialDiscovery>(py)?;
106 117
107 118 let sys = PyModule::import(py, "sys")?;
108 119 let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?;
109 120 sys_modules.set_item(py, dotted_name, &m)?;
110 121 // Example C code (see pyexpat.c and import.c) will "give away the
111 122 // reference", but we won't because it will be consumed once the
112 123 // Rust PyObject is dropped.
113 124 Ok(m)
114 125 }
@@ -1,103 +1,111 b''
1 1 from __future__ import absolute_import
2 2 import unittest
3 3
4 4 try:
5 5 from mercurial import rustext
6 6 rustext.__name__ # trigger immediate actual import
7 7 except ImportError:
8 8 rustext = None
9 9 else:
10 10 # this would fail already without appropriate ancestor.__package__
11 11 from mercurial.rustext.discovery import (
12 12 PartialDiscovery,
13 13 )
14 14
15 15 try:
16 16 from mercurial.cext import parsers as cparsers
17 17 except ImportError:
18 18 cparsers = None
19 19
20 20 # picked from test-parse-index2, copied rather than imported
21 21 # so that it stays stable even if test-parse-index2 changes or disappears.
22 22 data_non_inlined = (
23 23 b'\x00\x00\x00\x01\x00\x00\x00\x00\x00\x01D\x19'
24 24 b'\x00\x07e\x12\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff'
25 25 b'\xff\xff\xff\xff\xd1\xf4\xbb\xb0\xbe\xfc\x13\xbd\x8c\xd3\x9d'
26 26 b'\x0f\xcd\xd9;\x8c\x07\x8cJ/\x00\x00\x00\x00\x00\x00\x00\x00\x00'
27 27 b'\x00\x00\x00\x00\x00\x00\x01D\x19\x00\x00\x00\x00\x00\xdf\x00'
28 28 b'\x00\x01q\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\xff'
29 29 b'\xff\xff\xff\xc1\x12\xb9\x04\x96\xa4Z1t\x91\xdfsJ\x90\xf0\x9bh'
30 30 b'\x07l&\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
31 31 b'\x00\x01D\xf8\x00\x00\x00\x00\x01\x1b\x00\x00\x01\xb8\x00\x00'
32 32 b'\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01\xff\xff\xff\xff\x02\n'
33 33 b'\x0e\xc6&\xa1\x92\xae6\x0b\x02i\xfe-\xe5\xbao\x05\xd1\xe7\x00'
34 34 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01F'
35 35 b'\x13\x00\x00\x00\x00\x01\xec\x00\x00\x03\x06\x00\x00\x00\x01'
36 36 b'\x00\x00\x00\x03\x00\x00\x00\x02\xff\xff\xff\xff\x12\xcb\xeby1'
37 37 b'\xb6\r\x98B\xcb\x07\xbd`\x8f\x92\xd9\xc4\x84\xbdK\x00\x00\x00'
38 38 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00'
39 39 )
40 40
41 41
42 42 @unittest.skipIf(rustext is None or cparsers is None,
43 43 "rustext or the C Extension parsers module "
44 44 "discovery relies on is not available")
45 45 class rustdiscoverytest(unittest.TestCase):
46 46 """Test the correctness of binding to Rust code.
47 47
48 48 This test is merely for the binding to Rust itself: extraction of
49 49 Python variable, giving back the results etc.
50 50
51 51 It is not meant to test the algorithmic correctness of the provided
52 52 methods. Hence the very simple embedded index data is good enough.
53 53
54 54 Algorithmic correctness is asserted by the Rust unit tests.
55 55 """
56 56
57 57 def parseindex(self):
58 58 return cparsers.parse_index2(data_non_inlined, False)[0]
59 59
60 60 def testindex(self):
61 61 idx = self.parseindex()
62 62 # checking our assumptions about the index binary data:
63 63 self.assertEqual({i: (r[5], r[6]) for i, r in enumerate(idx)},
64 64 {0: (-1, -1),
65 65 1: (0, -1),
66 66 2: (1, -1),
67 67 3: (2, -1)})
68 68
69 69 def testaddcommonsmissings(self):
70 70 idx = self.parseindex()
71 71 disco = PartialDiscovery(idx, [3])
72 72 self.assertFalse(disco.hasinfo())
73 73 self.assertFalse(disco.iscomplete())
74 74
75 75 disco.addcommons([1])
76 76 self.assertTrue(disco.hasinfo())
77 77 self.assertFalse(disco.iscomplete())
78 78
79 79 disco.addmissings([2])
80 80 self.assertTrue(disco.hasinfo())
81 81 self.assertTrue(disco.iscomplete())
82 82
83 83 self.assertEqual(disco.commonheads(), {1})
84 84
85 def testaddmissingsstats(self):
86 idx = self.parseindex()
87 disco = PartialDiscovery(idx, [3])
88 self.assertIsNone(disco.stats()['undecided'], None)
89
90 disco.addmissings([2])
91 self.assertEqual(disco.stats()['undecided'], 2)
92
85 93 def testaddinfocommonfirst(self):
86 94 idx = self.parseindex()
87 95 disco = PartialDiscovery(idx, [3])
88 96 disco.addinfo([(1, True), (2, False)])
89 97 self.assertTrue(disco.hasinfo())
90 98 self.assertTrue(disco.iscomplete())
91 99 self.assertEqual(disco.commonheads(), {1})
92 100
93 101 def testaddinfomissingfirst(self):
94 102 idx = self.parseindex()
95 103 disco = PartialDiscovery(idx, [3])
96 104 disco.addinfo([(2, False), (1, True)])
97 105 self.assertTrue(disco.hasinfo())
98 106 self.assertTrue(disco.iscomplete())
99 107 self.assertEqual(disco.commonheads(), {1})
100 108
101 109 if __name__ == '__main__':
102 110 import silenttestrunner
103 111 silenttestrunner.main(__name__)
General Comments 0
You need to be logged in to leave comments. Login now