##// END OF EJS Templates
rust-pyo3: dagop submodule implementation...
Georges Racinet -
r53310:20fe0bf9 default
parent child Browse files
Show More
@@ -1,22 +1,73
1 // dagops.rs
1 // dagops.rs
2 //
2 //
3 // Copyright 2024 Georges Racinet <georges.racinet@cloudcrane.io>
3 // Copyright 2024 Georges Racinet <georges.racinet@cloudcrane.io>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
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.
6 // GNU General Public License version 2 or any later version.
7
7
8 //! Bindings for the `hg::dagops` module provided by the
8 //! Bindings for the `hg::dagops` module provided by the
9 //! `hg-core` package.
9 //! `hg-core` package.
10 //!
10 //!
11 //! From Python, this will be seen as `mercurial.pyo3-rustext.dagop`
11 //! From Python, this will be seen as `mercurial.pyo3-rustext.dagop`
12 use pyo3::prelude::*;
12 use pyo3::prelude::*;
13
13
14 use std::collections::HashSet;
15
16 use hg::{dagops, Revision};
17
18 use crate::convert_cpython::{from_cpython_pyerr, proxy_index_extract};
19 use crate::exceptions::GraphError;
20 use crate::revision::{rev_pyiter_collect, PyRevision};
14 use crate::util::new_submodule;
21 use crate::util::new_submodule;
15
22
23 /// Using the the `index_proxy`, return heads out of any Python iterable of
24 /// Revisions
25 ///
26 /// This is the Rust counterpart for `mercurial.dagop.headrevs`
27 #[pyfunction]
28 pub fn headrevs(
29 index_proxy: &Bound<'_, PyAny>,
30 revs: &Bound<'_, PyAny>,
31 ) -> PyResult<HashSet<PyRevision>> {
32 let (py, py_leaked) = proxy_index_extract(index_proxy)?;
33 // Safety: we don't leak the "faked" reference out of `UnsafePyLeaked`
34 let index = &*unsafe {
35 py_leaked
36 .try_borrow(py)
37 .map_err(|e| from_cpython_pyerr(py, e))?
38 };
39
40 let mut as_set: HashSet<Revision> = rev_pyiter_collect(revs, index)?;
41 dagops::retain_heads(index, &mut as_set).map_err(GraphError::from_hg)?;
42 Ok(as_set.into_iter().map(Into::into).collect())
43 }
44
45 /// Computes the rank, i.e. the number of ancestors including itself,
46 /// of a node represented by its parents.
47 ///
48 /// Currently, the pure Rust index supports only the REVLOGV1 format, hence
49 /// the only possible return value is that the rank is unknown.
50 ///
51 /// References:
52 /// - C implementation, function `index_fast_rank()`.
53 /// - `impl vcsgraph::graph::RankedGraph for Index` in `crate::cindex`.
54 #[pyfunction]
55 pub fn rank(
56 _index: &Bound<'_, PyAny>,
57 _p1r: PyRevision,
58 _p2r: PyRevision,
59 ) -> PyResult<()> {
60 Err(GraphError::from_vcsgraph(
61 vcsgraph::graph::GraphReadError::InconsistentGraphData,
62 ))
63 }
64
16 pub fn init_module<'py>(
65 pub fn init_module<'py>(
17 py: Python<'py>,
66 py: Python<'py>,
18 package: &str,
67 package: &str,
19 ) -> PyResult<Bound<'py, PyModule>> {
68 ) -> PyResult<Bound<'py, PyModule>> {
20 let m = new_submodule(py, package, "dagop")?;
69 let m = new_submodule(py, package, "dagop")?;
70 m.add_function(wrap_pyfunction!(headrevs, &m)?)?;
71 m.add_function(wrap_pyfunction!(rank, &m)?)?;
21 Ok(m)
72 Ok(m)
22 }
73 }
@@ -1,155 +1,167
1 import sys
1 import sys
2
2
3 from mercurial.node import wdirrev
3 from mercurial.node import wdirrev
4
4
5 from mercurial.testing import revlog as revlogtesting
5 from mercurial.testing import revlog as revlogtesting
6
6
7 try:
7 try:
8 from mercurial import rustext
8 from mercurial import pyo3_rustext, rustext
9
9
10 rustext.__name__ # trigger immediate actual import
10 rustext.__name__ # trigger immediate actual import
11 pyo3_rustext.__name__
11 except ImportError:
12 except ImportError:
12 rustext = None
13 rustext = pyo3_rustext = None
13 else:
14 else:
14 # this would fail already without appropriate ancestor.__package__
15 # this would fail already without appropriate ancestor.__package__
15 from mercurial.rustext.ancestor import (
16 from mercurial.rustext.ancestor import (
16 AncestorsIterator,
17 AncestorsIterator,
17 LazyAncestors,
18 LazyAncestors,
18 MissingAncestors,
19 MissingAncestors,
19 )
20 )
20 from mercurial.rustext import dagop
21 from mercurial.rustext import dagop
21
22
22 try:
23 try:
23 from mercurial.cext import parsers as cparsers
24 from mercurial.cext import parsers as cparsers
24 except ImportError:
25 except ImportError:
25 cparsers = None
26 cparsers = None
26
27
27
28
28 class rustancestorstest(revlogtesting.RustRevlogBasedTestBase):
29 class rustancestorstest(revlogtesting.RustRevlogBasedTestBase):
29 """Test the correctness of binding to Rust code.
30 """Test the correctness of binding to Rust code.
30
31
31 This test is merely for the binding to Rust itself: extraction of
32 This test is merely for the binding to Rust itself: extraction of
32 Python variable, giving back the results etc.
33 Python variable, giving back the results etc.
33
34
34 It is not meant to test the algorithmic correctness of the operations
35 It is not meant to test the algorithmic correctness of the operations
35 on ancestors it provides. Hence the very simple embedded index data is
36 on ancestors it provides. Hence the very simple embedded index data is
36 good enough.
37 good enough.
37
38
38 Algorithmic correctness is asserted by the Rust unit tests.
39 Algorithmic correctness is asserted by the Rust unit tests.
39 """
40 """
40
41
41 def testiteratorrevlist(self):
42 def testiteratorrevlist(self):
42 idx = self.parserustindex()
43 idx = self.parserustindex()
43 # checking test assumption about the index binary data:
44 # checking test assumption about the index binary data:
44 self.assertEqual(
45 self.assertEqual(
45 {i: (r[5], r[6]) for i, r in enumerate(idx)},
46 {i: (r[5], r[6]) for i, r in enumerate(idx)},
46 {0: (-1, -1), 1: (0, -1), 2: (1, -1), 3: (2, -1)},
47 {0: (-1, -1), 1: (0, -1), 2: (1, -1), 3: (2, -1)},
47 )
48 )
48 ait = AncestorsIterator(idx, [3], 0, True)
49 ait = AncestorsIterator(idx, [3], 0, True)
49 self.assertEqual([r for r in ait], [3, 2, 1, 0])
50 self.assertEqual([r for r in ait], [3, 2, 1, 0])
50
51
51 ait = AncestorsIterator(idx, [3], 0, False)
52 ait = AncestorsIterator(idx, [3], 0, False)
52 self.assertEqual([r for r in ait], [2, 1, 0])
53 self.assertEqual([r for r in ait], [2, 1, 0])
53
54
54 def testlazyancestors(self):
55 def testlazyancestors(self):
55 idx = self.parserustindex()
56 idx = self.parserustindex()
56 start_count = sys.getrefcount(idx.inner) # should be 2 (see Python doc)
57 start_count = sys.getrefcount(idx.inner) # should be 2 (see Python doc)
57 self.assertEqual(
58 self.assertEqual(
58 {i: (r[5], r[6]) for i, r in enumerate(idx)},
59 {i: (r[5], r[6]) for i, r in enumerate(idx)},
59 {0: (-1, -1), 1: (0, -1), 2: (1, -1), 3: (2, -1)},
60 {0: (-1, -1), 1: (0, -1), 2: (1, -1), 3: (2, -1)},
60 )
61 )
61 lazy = LazyAncestors(idx, [3], 0, True)
62 lazy = LazyAncestors(idx, [3], 0, True)
62 # the LazyAncestors instance holds just one reference to the
63 # the LazyAncestors instance holds just one reference to the
63 # inner revlog.
64 # inner revlog.
64 self.assertEqual(sys.getrefcount(idx.inner), start_count + 1)
65 self.assertEqual(sys.getrefcount(idx.inner), start_count + 1)
65
66
66 self.assertTrue(2 in lazy)
67 self.assertTrue(2 in lazy)
67 self.assertTrue(bool(lazy))
68 self.assertTrue(bool(lazy))
68 self.assertEqual(list(lazy), [3, 2, 1, 0])
69 self.assertEqual(list(lazy), [3, 2, 1, 0])
69 # a second time to validate that we spawn new iterators
70 # a second time to validate that we spawn new iterators
70 self.assertEqual(list(lazy), [3, 2, 1, 0])
71 self.assertEqual(list(lazy), [3, 2, 1, 0])
71
72
72 # now let's watch the refcounts closer
73 # now let's watch the refcounts closer
73 ait = iter(lazy)
74 ait = iter(lazy)
74 self.assertEqual(sys.getrefcount(idx.inner), start_count + 2)
75 self.assertEqual(sys.getrefcount(idx.inner), start_count + 2)
75 del ait
76 del ait
76 self.assertEqual(sys.getrefcount(idx.inner), start_count + 1)
77 self.assertEqual(sys.getrefcount(idx.inner), start_count + 1)
77 del lazy
78 del lazy
78 self.assertEqual(sys.getrefcount(idx.inner), start_count)
79 self.assertEqual(sys.getrefcount(idx.inner), start_count)
79
80
80 # let's check bool for an empty one
81 # let's check bool for an empty one
81 self.assertFalse(LazyAncestors(idx, [0], 0, False))
82 self.assertFalse(LazyAncestors(idx, [0], 0, False))
82
83
83 def testmissingancestors(self):
84 def testmissingancestors(self):
84 idx = self.parserustindex()
85 idx = self.parserustindex()
85 missanc = MissingAncestors(idx, [1])
86 missanc = MissingAncestors(idx, [1])
86 self.assertTrue(missanc.hasbases())
87 self.assertTrue(missanc.hasbases())
87 self.assertEqual(missanc.missingancestors([3]), [2, 3])
88 self.assertEqual(missanc.missingancestors([3]), [2, 3])
88 missanc.addbases({2})
89 missanc.addbases({2})
89 self.assertEqual(missanc.bases(), {1, 2})
90 self.assertEqual(missanc.bases(), {1, 2})
90 self.assertEqual(missanc.missingancestors([3]), [3])
91 self.assertEqual(missanc.missingancestors([3]), [3])
91 self.assertEqual(missanc.basesheads(), {2})
92 self.assertEqual(missanc.basesheads(), {2})
92
93
93 def testmissingancestorsremove(self):
94 def testmissingancestorsremove(self):
94 idx = self.parserustindex()
95 idx = self.parserustindex()
95 missanc = MissingAncestors(idx, [1])
96 missanc = MissingAncestors(idx, [1])
96 revs = {0, 1, 2, 3}
97 revs = {0, 1, 2, 3}
97 missanc.removeancestorsfrom(revs)
98 missanc.removeancestorsfrom(revs)
98 self.assertEqual(revs, {2, 3})
99 self.assertEqual(revs, {2, 3})
99
100
100 def testrefcount(self):
101 def testrefcount(self):
101 idx = self.parserustindex()
102 idx = self.parserustindex()
102 start_count = sys.getrefcount(idx.inner)
103 start_count = sys.getrefcount(idx.inner)
103
104
104 # refcount increases upon iterator init...
105 # refcount increases upon iterator init...
105 ait = AncestorsIterator(idx, [3], 0, True)
106 ait = AncestorsIterator(idx, [3], 0, True)
106 self.assertEqual(sys.getrefcount(idx.inner), start_count + 1)
107 self.assertEqual(sys.getrefcount(idx.inner), start_count + 1)
107 self.assertEqual(next(ait), 3)
108 self.assertEqual(next(ait), 3)
108
109
109 # and decreases once the iterator is removed
110 # and decreases once the iterator is removed
110 del ait
111 del ait
111 self.assertEqual(sys.getrefcount(idx.inner), start_count)
112 self.assertEqual(sys.getrefcount(idx.inner), start_count)
112
113
113 # and removing ref to the index after iterator init is no issue
114 # and removing ref to the index after iterator init is no issue
114 ait = AncestorsIterator(idx, [3], 0, True)
115 ait = AncestorsIterator(idx, [3], 0, True)
115 del idx
116 del idx
116 self.assertEqual(list(ait), [3, 2, 1, 0])
117 self.assertEqual(list(ait), [3, 2, 1, 0])
117
118
118 # the index is not tracked by the GC, hence there is nothing more
119 # the index is not tracked by the GC, hence there is nothing more
119 # we can assert to check that it is properly deleted once its refcount
120 # we can assert to check that it is properly deleted once its refcount
120 # drops to 0
121 # drops to 0
121
122
122 def testgrapherror(self):
123 def testgrapherror(self):
123 data = (
124 data = (
124 revlogtesting.data_non_inlined[: 64 + 27]
125 revlogtesting.data_non_inlined[: 64 + 27]
125 + b'\xf2'
126 + b'\xf2'
126 + revlogtesting.data_non_inlined[64 + 28 :]
127 + revlogtesting.data_non_inlined[64 + 28 :]
127 )
128 )
128 idx = self.parserustindex(data=data)
129 idx = self.parserustindex(data=data)
129 with self.assertRaises(rustext.GraphError) as arc:
130 with self.assertRaises(rustext.GraphError) as arc:
130 AncestorsIterator(idx, [1], -1, False)
131 AncestorsIterator(idx, [1], -1, False)
131 exc = arc.exception
132 exc = arc.exception
132 self.assertIsInstance(exc, ValueError)
133 self.assertIsInstance(exc, ValueError)
133 # rust-cpython issues appropriate str instances for Python 2 and 3
134 # rust-cpython issues appropriate str instances for Python 2 and 3
134 self.assertEqual(exc.args, ('ParentOutOfRange', 1))
135 self.assertEqual(exc.args, ('ParentOutOfRange', 1))
135
136
136 def testwdirunsupported(self):
137 def testwdirunsupported(self):
137 # trying to access ancestors of the working directory raises
138 # trying to access ancestors of the working directory raises
138 idx = self.parserustindex()
139 idx = self.parserustindex()
139 with self.assertRaises(rustext.GraphError) as arc:
140 with self.assertRaises(rustext.GraphError) as arc:
140 list(AncestorsIterator(idx, [wdirrev], -1, False))
141 list(AncestorsIterator(idx, [wdirrev], -1, False))
141
142
142 exc = arc.exception
143 exc = arc.exception
143 self.assertIsInstance(exc, ValueError)
144 self.assertIsInstance(exc, ValueError)
144 # rust-cpython issues appropriate str instances for Python 2 and 3
145 # rust-cpython issues appropriate str instances for Python 2 and 3
145 self.assertEqual(exc.args, ('InvalidRevision', wdirrev))
146 self.assertEqual(exc.args, ('InvalidRevision', wdirrev))
146
147
147 def testheadrevs(self):
148 def testheadrevs(self):
148 idx = self.parserustindex()
149 idx = self.parserustindex()
149 self.assertEqual(dagop.headrevs(idx, [1, 2, 3]), {3})
150 self.assertEqual(dagop.headrevs(idx, [1, 2, 3]), {3})
150
151
152 def testpyo3_headrevs(self):
153 idx = self.parserustindex()
154 self.assertEqual(pyo3_rustext.dagop.headrevs(idx, [1, 2, 3]), {3})
155
156 def testpyo3_rank(self):
157 idx = self.parserustindex()
158 try:
159 pyo3_rustext.dagop.rank(idx, 1, 2)
160 except pyo3_rustext.GraphError as exc:
161 self.assertEqual(exc.args, ("InconsistentGraphData",))
162
151
163
152 if __name__ == '__main__':
164 if __name__ == '__main__':
153 import silenttestrunner
165 import silenttestrunner
154
166
155 silenttestrunner.main(__name__)
167 silenttestrunner.main(__name__)
General Comments 0
You need to be logged in to leave comments. Login now