rust-pyo3: reviving test-rust-revlog.py
This test file was removed in 7346f93be7a4. Adaptation to the
new `InnerRevlog` / `RustIndexProxy` structure was as easy as
redefining the `parserustindex()` method to use `RustIndexProxy`.
As we did before with `test-rust-ancestors.py`, we are preparing
a mixin class that will contain tests for both bindings.
Existing tests will migrate from `RustInnerRevlogTest`
(the one for `hg-cpython`) to the mixin.
--- a/mercurial/testing/revlog.py Wed Dec 25 14:00:34 2024 +0100
+++ b/mercurial/testing/revlog.py Sun Dec 22 20:26:57 2024 +0100
@@ -44,9 +44,22 @@
rust_revlog = None
+try:
+ from ..pyo3_rustext import ( # pytype: disable=import-error
+ revlog as pyo3_revlog,
+ )
+
+ pyo3_revlog.__name__ # force actual import
+except ImportError:
+ pyo3_revlog = None
+
+
@unittest.skipIf(
cparsers is None,
- 'The C version of the "parsers" module is not available. It is needed for this test.',
+ (
+ 'The C version of the "parsers" module is not available. '
+ 'It is needed for this test.'
+ ),
)
class RevlogBasedTestBase(unittest.TestCase):
def parseindex(self, data=None):
@@ -65,13 +78,21 @@
revlog_delta_config = revlog.DeltaConfig()
revlog_feature_config = revlog.FeatureConfig()
+ @classmethod
+ def irl_class(cls):
+ return rust_revlog.InnerRevlog
+
+ @classmethod
+ def nodetree(cls, idx):
+ return rust_revlog.NodeTree(idx)
+
def make_inner_revlog(
self, data=None, vfs_is_readonly=True, kind=KIND_CHANGELOG
):
if data is None:
data = data_non_inlined
- return rust_revlog.InnerRevlog(
+ return self.irl_class()(
vfs_base=b"Just a path",
fncache=None, # might be enough for now
vfs_is_readonly=vfs_is_readonly,
@@ -91,3 +112,17 @@
def parserustindex(self, data=None):
return revlog.RustIndexProxy(self.make_inner_revlog(data=data))
+
+
+@unittest.skipIf(
+ pyo3_revlog is None,
+ 'The Rust PyO3 revlog module is not available. It is needed for this test.',
+)
+class PyO3RevlogBasedTestBase(RustRevlogBasedTestBase):
+ @classmethod
+ def irl_class(cls):
+ return pyo3_revlog.InnerRevlog
+
+ @classmethod
+ def nodetree(cls, idx):
+ return pyo3_revlog.NodeTree(idx)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-rust-revlog.py Sun Dec 22 20:26:57 2024 +0100
@@ -0,0 +1,97 @@
+import struct
+
+from mercurial.node import (
+ bin as node_bin,
+ hex,
+)
+
+try:
+ from mercurial import rustext
+
+ rustext.__name__ # trigger immediate actual import
+except ImportError:
+ rustext = None
+else:
+ # this would fail already without appropriate ancestor.__package__
+ from mercurial.rustext.ancestor import LazyAncestors
+
+from mercurial.testing import revlog as revlogtesting
+
+header = struct.unpack(">I", revlogtesting.data_non_inlined[:4])[0]
+
+
+class RustInnerRevlogTestMixin:
+ """Common tests for both Rust Python bindings."""
+
+ node_hex0 = b'd1f4bbb0befc13bd8cd39d0fcdd93b8c078c4a2f'
+ node0 = node_bin(node_hex0)
+
+
+# Conditional skipping done by the base class
+class RustInnerRevlogTest(
+ revlogtesting.RustRevlogBasedTestBase, RustInnerRevlogTestMixin
+):
+ """For reference"""
+
+ def test_heads(self):
+ idx = self.parserustindex()
+ self.assertEqual(idx.headrevs(), [3])
+
+ def test_len(self):
+ idx = self.parserustindex()
+ self.assertEqual(len(idx), 4)
+
+ def test_ancestors(self):
+ rustidx = self.parserustindex()
+ lazy = LazyAncestors(rustidx, [3], 0, True)
+ # we have two more references to the index:
+ # - in its inner iterator for __contains__ and __bool__
+ # - in the LazyAncestors instance itself (to spawn new iterators)
+ self.assertTrue(2 in lazy)
+ self.assertTrue(bool(lazy))
+ self.assertEqual(list(lazy), [3, 2, 1, 0])
+ # a second time to validate that we spawn new iterators
+ self.assertEqual(list(lazy), [3, 2, 1, 0])
+
+ # let's check bool for an empty one
+ self.assertFalse(LazyAncestors(rustidx, [0], 0, False))
+
+ def test_standalone_nodetree(self):
+ idx = self.parserustindex()
+ nt = self.nodetree(idx)
+ for i in range(4):
+ nt.insert(i)
+
+ bin_nodes = [entry[7] for entry in idx]
+ hex_nodes = [hex(n) for n in bin_nodes]
+
+ for i, node in enumerate(hex_nodes):
+ self.assertEqual(nt.prefix_rev_lookup(node), i)
+ self.assertEqual(nt.prefix_rev_lookup(node[:5]), i)
+
+ # all 4 revisions in idx (standard data set) have different
+ # first nybbles in their Node IDs,
+ # hence `nt.shortest()` should return 1 for them, except when
+ # the leading nybble is 0 (ambiguity with NULL_NODE)
+ for i, (bin_node, hex_node) in enumerate(zip(bin_nodes, hex_nodes)):
+ shortest = nt.shortest(bin_node)
+ expected = 2 if hex_node[0] == ord('0') else 1
+ self.assertEqual(shortest, expected)
+ self.assertEqual(nt.prefix_rev_lookup(hex_node[:shortest]), i)
+
+ # test invalidation (generation poisoning) detection
+ del idx[3]
+ self.assertTrue(nt.is_invalidated())
+
+
+# Conditional skipping done by the base class
+class PyO3InnerRevlogTest(
+ revlogtesting.PyO3RevlogBasedTestBase, RustInnerRevlogTestMixin
+):
+ """Testing new PyO3 bindings, by comparison with rust-cpython bindings."""
+
+
+if __name__ == '__main__':
+ import silenttestrunner
+
+ silenttestrunner.main(__name__)