Show More
@@ -1,92 +1,124 b'' | |||
|
1 | 1 | // ancestors.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::ancestors module provided by the |
|
9 | 9 | //! `hg-core` crate. From Python, this will be seen as `rustext.ancestor` |
|
10 | 10 | use cindex::Index; |
|
11 | 11 | use cpython::{ |
|
12 | 12 | ObjectProtocol, PyClone, PyDict, PyModule, PyObject, PyResult, Python, |
|
13 | 13 | }; |
|
14 | 14 | use exceptions::GraphError; |
|
15 | 15 | use hg; |
|
16 | use hg::AncestorsIterator as CoreIterator; | |
|
17 | 16 | use hg::Revision; |
|
17 | use hg::{AncestorsIterator as CoreIterator, LazyAncestors as CoreLazy}; | |
|
18 | 18 | use std::cell::RefCell; |
|
19 | 19 | |
|
20 | 20 | /// Utility function to convert a Python iterable into a Vec<Revision> |
|
21 | 21 | /// |
|
22 | 22 | /// We need this to feed to AncestorIterators constructors because |
|
23 | 23 | /// a PyErr can arise at each step of iteration, whereas our inner objects |
|
24 | 24 | /// expect iterables over Revision, not over some Result<Revision, PyErr> |
|
25 | 25 | fn reviter_to_revvec(py: Python, revs: PyObject) -> PyResult<Vec<Revision>> { |
|
26 | 26 | revs.iter(py)? |
|
27 | 27 | .map(|r| r.and_then(|o| o.extract::<Revision>(py))) |
|
28 | 28 | .collect() |
|
29 | 29 | } |
|
30 | 30 | |
|
31 | 31 | py_class!(class AncestorsIterator |py| { |
|
32 | 32 | // TODO RW lock ? |
|
33 | 33 | data inner: RefCell<Box<CoreIterator<Index>>>; |
|
34 | 34 | |
|
35 | 35 | def __next__(&self) -> PyResult<Option<Revision>> { |
|
36 | 36 | match self.inner(py).borrow_mut().next() { |
|
37 | 37 | Some(Err(e)) => Err(GraphError::pynew(py, e)), |
|
38 | 38 | None => Ok(None), |
|
39 | 39 | Some(Ok(r)) => Ok(Some(r)), |
|
40 | 40 | } |
|
41 | 41 | } |
|
42 | 42 | |
|
43 | 43 | def __contains__(&self, rev: Revision) -> PyResult<bool> { |
|
44 | 44 | self.inner(py).borrow_mut().contains(rev).map_err(|e| GraphError::pynew(py, e)) |
|
45 | 45 | } |
|
46 | 46 | |
|
47 | 47 | def __iter__(&self) -> PyResult<Self> { |
|
48 | 48 | Ok(self.clone_ref(py)) |
|
49 | 49 | } |
|
50 | 50 | |
|
51 | 51 | def __new__(_cls, index: PyObject, initrevs: PyObject, stoprev: Revision, |
|
52 | 52 | inclusive: bool) -> PyResult<AncestorsIterator> { |
|
53 | 53 | let initvec = reviter_to_revvec(py, initrevs)?; |
|
54 | 54 | let ait = match hg::AncestorsIterator::new(Index::new(py, index)?, |
|
55 | 55 | initvec, stoprev, |
|
56 | 56 | inclusive) { |
|
57 | 57 | Ok(ait) => ait, |
|
58 | 58 | Err(e) => { |
|
59 | 59 | return Err(GraphError::pynew(py, e)); |
|
60 | 60 | } |
|
61 | 61 | }; |
|
62 | 62 | AncestorsIterator::from_inner(py, ait) |
|
63 | 63 | } |
|
64 | 64 | |
|
65 | 65 | }); |
|
66 | 66 | |
|
67 | 67 | impl AncestorsIterator { |
|
68 | 68 | pub fn from_inner(py: Python, ait: CoreIterator<Index>) -> PyResult<Self> { |
|
69 | 69 | Self::create_instance(py, RefCell::new(Box::new(ait))) |
|
70 | 70 | } |
|
71 | 71 | } |
|
72 | 72 | |
|
73 | py_class!(class LazyAncestors |py| { | |
|
74 | data inner: RefCell<Box<CoreLazy<Index>>>; | |
|
75 | ||
|
76 | def __contains__(&self, rev: Revision) -> PyResult<bool> { | |
|
77 | self.inner(py) | |
|
78 | .borrow_mut() | |
|
79 | .contains(rev) | |
|
80 | .map_err(|e| GraphError::pynew(py, e)) | |
|
81 | } | |
|
82 | ||
|
83 | def __iter__(&self) -> PyResult<AncestorsIterator> { | |
|
84 | AncestorsIterator::from_inner(py, self.inner(py).borrow().iter()) | |
|
85 | } | |
|
86 | ||
|
87 | def __bool__(&self) -> PyResult<bool> { | |
|
88 | Ok(!self.inner(py).borrow().is_empty()) | |
|
89 | } | |
|
90 | ||
|
91 | def __new__(_cls, index: PyObject, initrevs: PyObject, stoprev: Revision, | |
|
92 | inclusive: bool) -> PyResult<Self> { | |
|
93 | let initvec = reviter_to_revvec(py, initrevs)?; | |
|
94 | ||
|
95 | let lazy = | |
|
96 | CoreLazy::new(Index::new(py, index)?, initvec, stoprev, inclusive) | |
|
97 | .map_err(|e| GraphError::pynew(py, e))?; | |
|
98 | ||
|
99 | Self::create_instance(py, RefCell::new(Box::new(lazy))) | |
|
100 | } | |
|
101 | ||
|
102 | }); | |
|
103 | ||
|
73 | 104 | /// Create the module, with __package__ given from parent |
|
74 | 105 | pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> { |
|
75 | 106 | let dotted_name = &format!("{}.ancestor", package); |
|
76 | 107 | let m = PyModule::new(py, dotted_name)?; |
|
77 | 108 | m.add(py, "__package__", package)?; |
|
78 | 109 | m.add( |
|
79 | 110 | py, |
|
80 | 111 | "__doc__", |
|
81 | 112 | "Generic DAG ancestor algorithms - Rust implementation", |
|
82 | 113 | )?; |
|
83 | 114 | m.add_class::<AncestorsIterator>(py)?; |
|
115 | m.add_class::<LazyAncestors>(py)?; | |
|
84 | 116 | |
|
85 | 117 | let sys = PyModule::import(py, "sys")?; |
|
86 | 118 | let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?; |
|
87 | 119 | sys_modules.set_item(py, dotted_name, &m)?; |
|
88 | 120 | // Example C code (see pyexpat.c and import.c) will "give away the |
|
89 | 121 | // reference", but we won't because it will be consumed once the |
|
90 | 122 | // Rust PyObject is dropped. |
|
91 | 123 | Ok(m) |
|
92 | 124 | } |
@@ -1,107 +1,141 b'' | |||
|
1 | 1 | from __future__ import absolute_import |
|
2 | 2 | import sys |
|
3 | 3 | import unittest |
|
4 | 4 | |
|
5 | 5 | try: |
|
6 | 6 | from mercurial import rustext |
|
7 | 7 | rustext.__name__ # trigger immediate actual import |
|
8 | 8 | except ImportError: |
|
9 | 9 | rustext = None |
|
10 | 10 | else: |
|
11 | 11 | # this would fail already without appropriate ancestor.__package__ |
|
12 |
from mercurial.rustext.ancestor import |
|
|
12 | from mercurial.rustext.ancestor import ( | |
|
13 | AncestorsIterator, | |
|
14 | LazyAncestors | |
|
15 | ) | |
|
13 | 16 | |
|
14 | 17 | try: |
|
15 | 18 | from mercurial.cext import parsers as cparsers |
|
16 | 19 | except ImportError: |
|
17 | 20 | cparsers = None |
|
18 | 21 | |
|
19 | 22 | # picked from test-parse-index2, copied rather than imported |
|
20 | 23 | # so that it stays stable even if test-parse-index2 changes or disappears. |
|
21 | 24 | data_non_inlined = ( |
|
22 | 25 | b'\x00\x00\x00\x01\x00\x00\x00\x00\x00\x01D\x19' |
|
23 | 26 | b'\x00\x07e\x12\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff' |
|
24 | 27 | b'\xff\xff\xff\xff\xd1\xf4\xbb\xb0\xbe\xfc\x13\xbd\x8c\xd3\x9d' |
|
25 | 28 | b'\x0f\xcd\xd9;\x8c\x07\x8cJ/\x00\x00\x00\x00\x00\x00\x00\x00\x00' |
|
26 | 29 | b'\x00\x00\x00\x00\x00\x00\x01D\x19\x00\x00\x00\x00\x00\xdf\x00' |
|
27 | 30 | b'\x00\x01q\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\xff' |
|
28 | 31 | b'\xff\xff\xff\xc1\x12\xb9\x04\x96\xa4Z1t\x91\xdfsJ\x90\xf0\x9bh' |
|
29 | 32 | b'\x07l&\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |
|
30 | 33 | b'\x00\x01D\xf8\x00\x00\x00\x00\x01\x1b\x00\x00\x01\xb8\x00\x00' |
|
31 | 34 | b'\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01\xff\xff\xff\xff\x02\n' |
|
32 | 35 | b'\x0e\xc6&\xa1\x92\xae6\x0b\x02i\xfe-\xe5\xbao\x05\xd1\xe7\x00' |
|
33 | 36 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01F' |
|
34 | 37 | b'\x13\x00\x00\x00\x00\x01\xec\x00\x00\x03\x06\x00\x00\x00\x01' |
|
35 | 38 | b'\x00\x00\x00\x03\x00\x00\x00\x02\xff\xff\xff\xff\x12\xcb\xeby1' |
|
36 | 39 | b'\xb6\r\x98B\xcb\x07\xbd`\x8f\x92\xd9\xc4\x84\xbdK\x00\x00\x00' |
|
37 | 40 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x00' |
|
38 | 41 | ) |
|
39 | 42 | |
|
40 | 43 | |
|
41 | 44 | @unittest.skipIf(rustext is None or cparsers is None, |
|
42 | 45 | "rustext or the C Extension parsers module " |
|
43 | 46 | "ancestor relies on is not available") |
|
44 | 47 | class rustancestorstest(unittest.TestCase): |
|
45 | 48 | """Test the correctness of binding to Rust code. |
|
46 | 49 | |
|
47 | 50 | This test is merely for the binding to Rust itself: extraction of |
|
48 | 51 | Python variable, giving back the results etc. |
|
49 | 52 | |
|
50 | 53 | It is not meant to test the algorithmic correctness of the operations |
|
51 | 54 | on ancestors it provides. Hence the very simple embedded index data is |
|
52 | 55 | good enough. |
|
53 | 56 | |
|
54 | 57 | Algorithmic correctness is asserted by the Rust unit tests. |
|
55 | 58 | """ |
|
56 | 59 | |
|
57 | 60 | def parseindex(self): |
|
58 | 61 | return cparsers.parse_index2(data_non_inlined, False)[0] |
|
59 | 62 | |
|
60 | 63 | def testiteratorrevlist(self): |
|
61 | 64 | idx = self.parseindex() |
|
62 | 65 | # checking test assumption about the index binary data: |
|
63 | 66 | self.assertEqual({i: (r[5], r[6]) for i, r in enumerate(idx)}, |
|
64 | 67 | {0: (-1, -1), |
|
65 | 68 | 1: (0, -1), |
|
66 | 69 | 2: (1, -1), |
|
67 | 70 | 3: (2, -1)}) |
|
68 | 71 | ait = AncestorsIterator(idx, [3], 0, True) |
|
69 | 72 | self.assertEqual([r for r in ait], [3, 2, 1, 0]) |
|
70 | 73 | |
|
71 | 74 | ait = AncestorsIterator(idx, [3], 0, False) |
|
72 | 75 | self.assertEqual([r for r in ait], [2, 1, 0]) |
|
73 | 76 | |
|
77 | def testlazyancestors(self): | |
|
78 | idx = self.parseindex() | |
|
79 | start_count = sys.getrefcount(idx) # should be 2 (see Python doc) | |
|
80 | self.assertEqual({i: (r[5], r[6]) for i, r in enumerate(idx)}, | |
|
81 | {0: (-1, -1), | |
|
82 | 1: (0, -1), | |
|
83 | 2: (1, -1), | |
|
84 | 3: (2, -1)}) | |
|
85 | lazy = LazyAncestors(idx, [3], 0, True) | |
|
86 | # we have two more references to the index: | |
|
87 | # - in its inner iterator for __contains__ and __bool__ | |
|
88 | # - in the LazyAncestors instance itself (to spawn new iterators) | |
|
89 | self.assertEqual(sys.getrefcount(idx), start_count + 2) | |
|
90 | ||
|
91 | self.assertTrue(2 in lazy) | |
|
92 | self.assertTrue(bool(lazy)) | |
|
93 | self.assertEqual(list(lazy), [3, 2, 1, 0]) | |
|
94 | # a second time to validate that we spawn new iterators | |
|
95 | self.assertEqual(list(lazy), [3, 2, 1, 0]) | |
|
96 | ||
|
97 | # now let's watch the refcounts closer | |
|
98 | ait = iter(lazy) | |
|
99 | self.assertEqual(sys.getrefcount(idx), start_count + 3) | |
|
100 | del ait | |
|
101 | self.assertEqual(sys.getrefcount(idx), start_count + 2) | |
|
102 | del lazy | |
|
103 | self.assertEqual(sys.getrefcount(idx), start_count) | |
|
104 | ||
|
105 | # let's check bool for an empty one | |
|
106 | self.assertFalse(LazyAncestors(idx, [0], 0, False)) | |
|
107 | ||
|
74 | 108 | def testrefcount(self): |
|
75 | 109 | idx = self.parseindex() |
|
76 | 110 | start_count = sys.getrefcount(idx) |
|
77 | 111 | |
|
78 | 112 | # refcount increases upon iterator init... |
|
79 | 113 | ait = AncestorsIterator(idx, [3], 0, True) |
|
80 | 114 | self.assertEqual(sys.getrefcount(idx), start_count + 1) |
|
81 | 115 | self.assertEqual(next(ait), 3) |
|
82 | 116 | |
|
83 | 117 | # and decreases once the iterator is removed |
|
84 | 118 | del ait |
|
85 | 119 | self.assertEqual(sys.getrefcount(idx), start_count) |
|
86 | 120 | |
|
87 | 121 | # and removing ref to the index after iterator init is no issue |
|
88 | 122 | ait = AncestorsIterator(idx, [3], 0, True) |
|
89 | 123 | del idx |
|
90 |
self.assertEqual( |
|
|
124 | self.assertEqual(list(ait), [3, 2, 1, 0]) | |
|
91 | 125 | |
|
92 | 126 | def testgrapherror(self): |
|
93 | 127 | data = (data_non_inlined[:64 + 27] + |
|
94 | 128 | b'\xf2' + |
|
95 | 129 | data_non_inlined[64 + 28:]) |
|
96 | 130 | idx = cparsers.parse_index2(data, False)[0] |
|
97 | 131 | with self.assertRaises(rustext.GraphError) as arc: |
|
98 | 132 | AncestorsIterator(idx, [1], -1, False) |
|
99 | 133 | exc = arc.exception |
|
100 | 134 | self.assertIsInstance(exc, ValueError) |
|
101 | 135 | # rust-cpython issues appropriate str instances for Python 2 and 3 |
|
102 | 136 | self.assertEqual(exc.args, ('ParentOutOfRange', 1)) |
|
103 | 137 | |
|
104 | 138 | |
|
105 | 139 | if __name__ == '__main__': |
|
106 | 140 | import silenttestrunner |
|
107 | 141 | silenttestrunner.main(__name__) |
General Comments 0
You need to be logged in to leave comments.
Login now