##// END OF EJS Templates
rust-cpython: binding for LazyAncestors...
Georges Racinet -
r41149:b31a41f2 default
parent child Browse files
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 AncestorsIterator
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([r for r in ait], [3, 2, 1, 0])
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