diff --git a/mercurial/testing/__init__.py b/mercurial/testing/__init__.py new file mode 100644 diff --git a/mercurial/testing/storage.py b/mercurial/testing/storage.py new file mode 100644 --- /dev/null +++ b/mercurial/testing/storage.py @@ -0,0 +1,984 @@ +# storage.py - Testing of storage primitives. +# +# Copyright 2018 Gregory Szorc +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from __future__ import absolute_import + +import unittest + +from ..node import ( + hex, + nullid, + nullrev, +) +from .. import ( + error, + mdiff, + revlog, +) + +class basetestcase(unittest.TestCase): + if not getattr(unittest.TestCase, r'assertRaisesRegex', False): + assertRaisesRegex = (# camelcase-required + unittest.TestCase.assertRaisesRegexp) + +class revisiondeltarequest(object): + def __init__(self, node, p1, p2, linknode, basenode, ellipsis): + self.node = node + self.p1node = p1 + self.p2node = p2 + self.linknode = linknode + self.basenode = basenode + self.ellipsis = ellipsis + +class ifileindextests(basetestcase): + """Generic tests for the ifileindex interface. + + All file storage backends for index data should conform to the tests in this + class. + + Use ``makeifileindextests()`` to create an instance of this type. + """ + def testempty(self): + f = self._makefilefn() + self.assertEqual(len(f), 0, 'new file store has 0 length by default') + self.assertEqual(list(f), [], 'iter yields nothing by default') + + gen = iter(f) + with self.assertRaises(StopIteration): + next(gen) + + # revs() should evaluate to an empty list. + self.assertEqual(list(f.revs()), []) + + revs = iter(f.revs()) + with self.assertRaises(StopIteration): + next(revs) + + self.assertEqual(list(f.revs(start=20)), []) + + # parents() and parentrevs() work with nullid/nullrev. + self.assertEqual(f.parents(nullid), (nullid, nullid)) + self.assertEqual(f.parentrevs(nullrev), (nullrev, nullrev)) + + with self.assertRaises(error.LookupError): + f.parents(b'\x01' * 20) + + for i in range(-5, 5): + if i == nullrev: + continue + + with self.assertRaises(IndexError): + f.parentrevs(i) + + # nullid/nullrev lookup always works. + self.assertEqual(f.rev(nullid), nullrev) + self.assertEqual(f.node(nullrev), nullid) + + with self.assertRaises(error.LookupError): + f.rev(b'\x01' * 20) + + for i in range(-5, 5): + if i == nullrev: + continue + + with self.assertRaises(IndexError): + f.node(i) + + self.assertEqual(f.lookup(nullid), nullid) + self.assertEqual(f.lookup(nullrev), nullid) + self.assertEqual(f.lookup(hex(nullid)), nullid) + + # String converted to integer doesn't work for nullrev. + with self.assertRaises(error.LookupError): + f.lookup(b'%d' % nullrev) + + self.assertEqual(f.linkrev(nullrev), nullrev) + + for i in range(-5, 5): + if i == nullrev: + continue + + with self.assertRaises(IndexError): + f.linkrev(i) + + self.assertEqual(f.flags(nullrev), 0) + + for i in range(-5, 5): + if i == nullrev: + continue + + with self.assertRaises(IndexError): + f.flags(i) + + self.assertFalse(f.iscensored(nullrev)) + + for i in range(-5, 5): + if i == nullrev: + continue + + with self.assertRaises(IndexError): + f.iscensored(i) + + self.assertEqual(list(f.commonancestorsheads(nullid, nullid)), []) + + with self.assertRaises(ValueError): + self.assertEqual(list(f.descendants([])), []) + + self.assertEqual(list(f.descendants([nullrev])), []) + + self.assertEqual(f.headrevs(), [nullrev]) + self.assertEqual(f.heads(), [nullid]) + self.assertEqual(f.heads(nullid), [nullid]) + self.assertEqual(f.heads(None, [nullid]), [nullid]) + self.assertEqual(f.heads(nullid, [nullid]), [nullid]) + + self.assertEqual(f.children(nullid), []) + + with self.assertRaises(error.LookupError): + f.children(b'\x01' * 20) + + self.assertEqual(f.deltaparent(nullrev), nullrev) + + for i in range(-5, 5): + if i == nullrev: + continue + + with self.assertRaises(IndexError): + f.deltaparent(i) + + def testsinglerevision(self): + f = self._makefilefn() + with self._maketransactionfn() as tr: + node = f.add(b'initial', None, tr, 0, nullid, nullid) + + self.assertEqual(len(f), 1) + self.assertEqual(list(f), [0]) + + gen = iter(f) + self.assertEqual(next(gen), 0) + + with self.assertRaises(StopIteration): + next(gen) + + self.assertEqual(list(f.revs()), [0]) + self.assertEqual(list(f.revs(start=1)), []) + self.assertEqual(list(f.revs(start=0)), [0]) + self.assertEqual(list(f.revs(stop=0)), [0]) + self.assertEqual(list(f.revs(stop=1)), [0]) + self.assertEqual(list(f.revs(1, 1)), []) + # TODO buggy + self.assertEqual(list(f.revs(1, 0)), [1, 0]) + self.assertEqual(list(f.revs(2, 0)), [2, 1, 0]) + + self.assertEqual(f.parents(node), (nullid, nullid)) + self.assertEqual(f.parentrevs(0), (nullrev, nullrev)) + + with self.assertRaises(error.LookupError): + f.parents(b'\x01' * 20) + + with self.assertRaises(IndexError): + f.parentrevs(1) + + self.assertEqual(f.rev(node), 0) + + with self.assertRaises(error.LookupError): + f.rev(b'\x01' * 20) + + self.assertEqual(f.node(0), node) + + with self.assertRaises(IndexError): + f.node(1) + + self.assertEqual(f.lookup(node), node) + self.assertEqual(f.lookup(0), node) + self.assertEqual(f.lookup(b'0'), node) + self.assertEqual(f.lookup(hex(node)), node) + + self.assertEqual(f.linkrev(0), 0) + + with self.assertRaises(IndexError): + f.linkrev(1) + + self.assertEqual(f.flags(0), 0) + + with self.assertRaises(IndexError): + f.flags(1) + + self.assertFalse(f.iscensored(0)) + + with self.assertRaises(IndexError): + f.iscensored(1) + + self.assertEqual(list(f.descendants([0])), []) + + self.assertEqual(f.headrevs(), [0]) + + self.assertEqual(f.heads(), [node]) + self.assertEqual(f.heads(node), [node]) + self.assertEqual(f.heads(stop=[node]), [node]) + + with self.assertRaises(error.LookupError): + f.heads(stop=[b'\x01' * 20]) + + self.assertEqual(f.children(node), []) + + self.assertEqual(f.deltaparent(0), nullrev) + + def testmultiplerevisions(self): + fulltext0 = b'x' * 1024 + fulltext1 = fulltext0 + b'y' + fulltext2 = b'y' + fulltext0 + b'z' + + f = self._makefilefn() + with self._maketransactionfn() as tr: + node0 = f.add(fulltext0, None, tr, 0, nullid, nullid) + node1 = f.add(fulltext1, None, tr, 1, node0, nullid) + node2 = f.add(fulltext2, None, tr, 3, node1, nullid) + + self.assertEqual(len(f), 3) + self.assertEqual(list(f), [0, 1, 2]) + + gen = iter(f) + self.assertEqual(next(gen), 0) + self.assertEqual(next(gen), 1) + self.assertEqual(next(gen), 2) + + with self.assertRaises(StopIteration): + next(gen) + + self.assertEqual(list(f.revs()), [0, 1, 2]) + self.assertEqual(list(f.revs(0)), [0, 1, 2]) + self.assertEqual(list(f.revs(1)), [1, 2]) + self.assertEqual(list(f.revs(2)), [2]) + self.assertEqual(list(f.revs(3)), []) + self.assertEqual(list(f.revs(stop=1)), [0, 1]) + self.assertEqual(list(f.revs(stop=2)), [0, 1, 2]) + self.assertEqual(list(f.revs(stop=3)), [0, 1, 2]) + self.assertEqual(list(f.revs(2, 0)), [2, 1, 0]) + self.assertEqual(list(f.revs(2, 1)), [2, 1]) + # TODO this is wrong + self.assertEqual(list(f.revs(3, 2)), [3, 2]) + + self.assertEqual(f.parents(node0), (nullid, nullid)) + self.assertEqual(f.parents(node1), (node0, nullid)) + self.assertEqual(f.parents(node2), (node1, nullid)) + + self.assertEqual(f.parentrevs(0), (nullrev, nullrev)) + self.assertEqual(f.parentrevs(1), (0, nullrev)) + self.assertEqual(f.parentrevs(2), (1, nullrev)) + + self.assertEqual(f.rev(node0), 0) + self.assertEqual(f.rev(node1), 1) + self.assertEqual(f.rev(node2), 2) + + with self.assertRaises(error.LookupError): + f.rev(b'\x01' * 20) + + self.assertEqual(f.node(0), node0) + self.assertEqual(f.node(1), node1) + self.assertEqual(f.node(2), node2) + + with self.assertRaises(IndexError): + f.node(3) + + self.assertEqual(f.lookup(node0), node0) + self.assertEqual(f.lookup(0), node0) + self.assertEqual(f.lookup(b'0'), node0) + self.assertEqual(f.lookup(hex(node0)), node0) + + self.assertEqual(f.lookup(node1), node1) + self.assertEqual(f.lookup(1), node1) + self.assertEqual(f.lookup(b'1'), node1) + self.assertEqual(f.lookup(hex(node1)), node1) + + self.assertEqual(f.linkrev(0), 0) + self.assertEqual(f.linkrev(1), 1) + self.assertEqual(f.linkrev(2), 3) + + with self.assertRaises(IndexError): + f.linkrev(3) + + self.assertEqual(f.flags(0), 0) + self.assertEqual(f.flags(1), 0) + self.assertEqual(f.flags(2), 0) + + with self.assertRaises(IndexError): + f.flags(3) + + self.assertFalse(f.iscensored(0)) + self.assertFalse(f.iscensored(1)) + self.assertFalse(f.iscensored(2)) + + with self.assertRaises(IndexError): + f.iscensored(3) + + self.assertEqual(f.commonancestorsheads(node1, nullid), []) + self.assertEqual(f.commonancestorsheads(node1, node0), [node0]) + self.assertEqual(f.commonancestorsheads(node1, node1), [node1]) + self.assertEqual(f.commonancestorsheads(node0, node1), [node0]) + self.assertEqual(f.commonancestorsheads(node1, node2), [node1]) + self.assertEqual(f.commonancestorsheads(node2, node1), [node1]) + + self.assertEqual(list(f.descendants([0])), [1, 2]) + self.assertEqual(list(f.descendants([1])), [2]) + self.assertEqual(list(f.descendants([0, 1])), [1, 2]) + + self.assertEqual(f.headrevs(), [2]) + + self.assertEqual(f.heads(), [node2]) + self.assertEqual(f.heads(node0), [node2]) + self.assertEqual(f.heads(node1), [node2]) + self.assertEqual(f.heads(node2), [node2]) + + # TODO this behavior seems wonky. Is it correct? If so, the + # docstring for heads() should be updated to reflect desired + # behavior. + self.assertEqual(f.heads(stop=[node1]), [node1, node2]) + self.assertEqual(f.heads(stop=[node0]), [node0, node2]) + self.assertEqual(f.heads(stop=[node1, node2]), [node1, node2]) + + with self.assertRaises(error.LookupError): + f.heads(stop=[b'\x01' * 20]) + + self.assertEqual(f.children(node0), [node1]) + self.assertEqual(f.children(node1), [node2]) + self.assertEqual(f.children(node2), []) + + self.assertEqual(f.deltaparent(0), nullrev) + self.assertEqual(f.deltaparent(1), 0) + self.assertEqual(f.deltaparent(2), 1) + + def testmultipleheads(self): + f = self._makefilefn() + + with self._maketransactionfn() as tr: + node0 = f.add(b'0', None, tr, 0, nullid, nullid) + node1 = f.add(b'1', None, tr, 1, node0, nullid) + node2 = f.add(b'2', None, tr, 2, node1, nullid) + node3 = f.add(b'3', None, tr, 3, node0, nullid) + node4 = f.add(b'4', None, tr, 4, node3, nullid) + node5 = f.add(b'5', None, tr, 5, node0, nullid) + + self.assertEqual(len(f), 6) + + self.assertEqual(list(f.descendants([0])), [1, 2, 3, 4, 5]) + self.assertEqual(list(f.descendants([1])), [2]) + self.assertEqual(list(f.descendants([2])), []) + self.assertEqual(list(f.descendants([3])), [4]) + self.assertEqual(list(f.descendants([0, 1])), [1, 2, 3, 4, 5]) + self.assertEqual(list(f.descendants([1, 3])), [2, 4]) + + self.assertEqual(f.headrevs(), [2, 4, 5]) + + self.assertEqual(f.heads(), [node2, node4, node5]) + self.assertEqual(f.heads(node0), [node2, node4, node5]) + self.assertEqual(f.heads(node1), [node2]) + self.assertEqual(f.heads(node2), [node2]) + self.assertEqual(f.heads(node3), [node4]) + self.assertEqual(f.heads(node4), [node4]) + self.assertEqual(f.heads(node5), [node5]) + + # TODO this seems wrong. + self.assertEqual(f.heads(stop=[node0]), [node0, node2, node4, node5]) + self.assertEqual(f.heads(stop=[node1]), [node1, node2, node4, node5]) + + self.assertEqual(f.children(node0), [node1, node3, node5]) + self.assertEqual(f.children(node1), [node2]) + self.assertEqual(f.children(node2), []) + self.assertEqual(f.children(node3), [node4]) + self.assertEqual(f.children(node4), []) + self.assertEqual(f.children(node5), []) + +class ifiledatatests(basetestcase): + """Generic tests for the ifiledata interface. + + All file storage backends for data should conform to the tests in this + class. + + Use ``makeifiledatatests()`` to create an instance of this type. + """ + def testempty(self): + f = self._makefilefn() + + self.assertEqual(f.rawsize(nullrev), 0) + + for i in range(-5, 5): + if i == nullrev: + continue + + with self.assertRaises(IndexError): + f.rawsize(i) + + self.assertEqual(f.size(nullrev), 0) + + for i in range(-5, 5): + if i == nullrev: + continue + + with self.assertRaises(IndexError): + f.size(i) + + with self.assertRaises(error.RevlogError): + f.checkhash(b'', nullid) + + with self.assertRaises(error.LookupError): + f.checkhash(b'', b'\x01' * 20) + + self.assertEqual(f.revision(nullid), b'') + self.assertEqual(f.revision(nullid, raw=True), b'') + + with self.assertRaises(error.LookupError): + f.revision(b'\x01' * 20) + + self.assertEqual(f.read(nullid), b'') + + with self.assertRaises(error.LookupError): + f.read(b'\x01' * 20) + + self.assertFalse(f.renamed(nullid)) + + with self.assertRaises(error.LookupError): + f.read(b'\x01' * 20) + + self.assertTrue(f.cmp(nullid, b'')) + self.assertTrue(f.cmp(nullid, b'foo')) + + with self.assertRaises(error.LookupError): + f.cmp(b'\x01' * 20, b'irrelevant') + + self.assertEqual(f.revdiff(nullrev, nullrev), b'') + + with self.assertRaises(IndexError): + f.revdiff(0, nullrev) + + with self.assertRaises(IndexError): + f.revdiff(nullrev, 0) + + with self.assertRaises(IndexError): + f.revdiff(0, 0) + + gen = f.emitrevisiondeltas([]) + with self.assertRaises(StopIteration): + next(gen) + + requests = [ + revisiondeltarequest(nullid, nullid, nullid, nullid, nullid, False), + ] + gen = f.emitrevisiondeltas(requests) + + delta = next(gen) + + self.assertEqual(delta.node, nullid) + self.assertEqual(delta.p1node, nullid) + self.assertEqual(delta.p2node, nullid) + self.assertEqual(delta.linknode, nullid) + self.assertEqual(delta.basenode, nullid) + self.assertIsNone(delta.baserevisionsize) + self.assertEqual(delta.revision, b'') + self.assertIsNone(delta.delta) + + with self.assertRaises(StopIteration): + next(gen) + + requests = [ + revisiondeltarequest(nullid, nullid, nullid, nullid, nullid, False), + revisiondeltarequest(nullid, b'\x01' * 20, b'\x02' * 20, + b'\x03' * 20, nullid, False) + ] + + gen = f.emitrevisiondeltas(requests) + + next(gen) + delta = next(gen) + + self.assertEqual(delta.node, nullid) + self.assertEqual(delta.p1node, b'\x01' * 20) + self.assertEqual(delta.p2node, b'\x02' * 20) + self.assertEqual(delta.linknode, b'\x03' * 20) + self.assertEqual(delta.basenode, nullid) + self.assertIsNone(delta.baserevisionsize) + self.assertEqual(delta.revision, b'') + self.assertIsNone(delta.delta) + + with self.assertRaises(StopIteration): + next(gen) + + def testsinglerevision(self): + fulltext = b'initial' + + f = self._makefilefn() + with self._maketransactionfn() as tr: + node = f.add(fulltext, None, tr, 0, nullid, nullid) + + self.assertEqual(f.rawsize(0), len(fulltext)) + + with self.assertRaises(IndexError): + f.rawsize(1) + + self.assertEqual(f.size(0), len(fulltext)) + + with self.assertRaises(IndexError): + f.size(1) + + f.checkhash(fulltext, node) + f.checkhash(fulltext, node, nullid, nullid) + + with self.assertRaises(error.RevlogError): + f.checkhash(fulltext + b'extra', node) + + with self.assertRaises(error.RevlogError): + f.checkhash(fulltext, node, b'\x01' * 20, nullid) + + with self.assertRaises(error.RevlogError): + f.checkhash(fulltext, node, nullid, b'\x01' * 20) + + self.assertEqual(f.revision(node), fulltext) + self.assertEqual(f.revision(node, raw=True), fulltext) + + self.assertEqual(f.read(node), fulltext) + + self.assertFalse(f.renamed(node)) + + self.assertFalse(f.cmp(node, fulltext)) + self.assertTrue(f.cmp(node, fulltext + b'extra')) + + self.assertEqual(f.revdiff(0, 0), b'') + self.assertEqual(f.revdiff(nullrev, 0), + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07%s' % + fulltext) + + self.assertEqual(f.revdiff(0, nullrev), + b'\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00') + + requests = [ + revisiondeltarequest(node, nullid, nullid, nullid, nullid, False), + ] + gen = f.emitrevisiondeltas(requests) + + delta = next(gen) + + self.assertEqual(delta.node, node) + self.assertEqual(delta.p1node, nullid) + self.assertEqual(delta.p2node, nullid) + self.assertEqual(delta.linknode, nullid) + self.assertEqual(delta.basenode, nullid) + self.assertIsNone(delta.baserevisionsize) + self.assertEqual(delta.revision, fulltext) + self.assertIsNone(delta.delta) + + with self.assertRaises(StopIteration): + next(gen) + + def testmultiplerevisions(self): + fulltext0 = b'x' * 1024 + fulltext1 = fulltext0 + b'y' + fulltext2 = b'y' + fulltext0 + b'z' + + f = self._makefilefn() + with self._maketransactionfn() as tr: + node0 = f.add(fulltext0, None, tr, 0, nullid, nullid) + node1 = f.add(fulltext1, None, tr, 1, node0, nullid) + node2 = f.add(fulltext2, None, tr, 3, node1, nullid) + + self.assertEqual(f.rawsize(0), len(fulltext0)) + self.assertEqual(f.rawsize(1), len(fulltext1)) + self.assertEqual(f.rawsize(2), len(fulltext2)) + + with self.assertRaises(IndexError): + f.rawsize(3) + + self.assertEqual(f.size(0), len(fulltext0)) + self.assertEqual(f.size(1), len(fulltext1)) + self.assertEqual(f.size(2), len(fulltext2)) + + with self.assertRaises(IndexError): + f.size(3) + + f.checkhash(fulltext0, node0) + f.checkhash(fulltext1, node1) + f.checkhash(fulltext1, node1, node0, nullid) + f.checkhash(fulltext2, node2, node1, nullid) + + with self.assertRaises(error.RevlogError): + f.checkhash(fulltext1, b'\x01' * 20) + + with self.assertRaises(error.RevlogError): + f.checkhash(fulltext1 + b'extra', node1, node0, nullid) + + with self.assertRaises(error.RevlogError): + f.checkhash(fulltext1, node1, node0, node0) + + self.assertEqual(f.revision(node0), fulltext0) + self.assertEqual(f.revision(node0, raw=True), fulltext0) + self.assertEqual(f.revision(node1), fulltext1) + self.assertEqual(f.revision(node1, raw=True), fulltext1) + self.assertEqual(f.revision(node2), fulltext2) + self.assertEqual(f.revision(node2, raw=True), fulltext2) + + with self.assertRaises(error.LookupError): + f.revision(b'\x01' * 20) + + self.assertEqual(f.read(node0), fulltext0) + self.assertEqual(f.read(node1), fulltext1) + self.assertEqual(f.read(node2), fulltext2) + + with self.assertRaises(error.LookupError): + f.read(b'\x01' * 20) + + self.assertFalse(f.renamed(node0)) + self.assertFalse(f.renamed(node1)) + self.assertFalse(f.renamed(node2)) + + with self.assertRaises(error.LookupError): + f.renamed(b'\x01' * 20) + + self.assertFalse(f.cmp(node0, fulltext0)) + self.assertFalse(f.cmp(node1, fulltext1)) + self.assertFalse(f.cmp(node2, fulltext2)) + + self.assertTrue(f.cmp(node1, fulltext0)) + self.assertTrue(f.cmp(node2, fulltext1)) + + with self.assertRaises(error.LookupError): + f.cmp(b'\x01' * 20, b'irrelevant') + + self.assertEqual(f.revdiff(0, 1), + b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x01' + + fulltext1) + + self.assertEqual(f.revdiff(0, 2), + b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x02' + + fulltext2) + + requests = [ + revisiondeltarequest(node0, nullid, nullid, b'\x01' * 20, nullid, + False), + revisiondeltarequest(node1, node0, nullid, b'\x02' * 20, node0, + False), + revisiondeltarequest(node2, node1, nullid, b'\x03' * 20, node1, + False), + ] + gen = f.emitrevisiondeltas(requests) + + delta = next(gen) + + self.assertEqual(delta.node, node0) + self.assertEqual(delta.p1node, nullid) + self.assertEqual(delta.p2node, nullid) + self.assertEqual(delta.linknode, b'\x01' * 20) + self.assertEqual(delta.basenode, nullid) + self.assertIsNone(delta.baserevisionsize) + self.assertEqual(delta.revision, fulltext0) + self.assertIsNone(delta.delta) + + delta = next(gen) + + self.assertEqual(delta.node, node1) + self.assertEqual(delta.p1node, node0) + self.assertEqual(delta.p2node, nullid) + self.assertEqual(delta.linknode, b'\x02' * 20) + self.assertEqual(delta.basenode, node0) + self.assertIsNone(delta.baserevisionsize) + self.assertIsNone(delta.revision) + self.assertEqual(delta.delta, + b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x01' + + fulltext1) + + delta = next(gen) + + self.assertEqual(delta.node, node2) + self.assertEqual(delta.p1node, node1) + self.assertEqual(delta.p2node, nullid) + self.assertEqual(delta.linknode, b'\x03' * 20) + self.assertEqual(delta.basenode, node1) + self.assertIsNone(delta.baserevisionsize) + self.assertIsNone(delta.revision) + self.assertEqual(delta.delta, + b'\x00\x00\x00\x00\x00\x00\x04\x01\x00\x00\x04\x02' + + fulltext2) + + with self.assertRaises(StopIteration): + next(gen) + + def testrenamed(self): + fulltext0 = b'foo' + fulltext1 = b'bar' + fulltext2 = b'baz' + + meta1 = { + b'copy': b'source0', + b'copyrev': b'a' * 40, + } + + meta2 = { + b'copy': b'source1', + b'copyrev': b'b' * 40, + } + + stored1 = b''.join([ + b'\x01\ncopy: source0\n', + b'copyrev: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\x01\n', + fulltext1, + ]) + + stored2 = b''.join([ + b'\x01\ncopy: source1\n', + b'copyrev: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n\x01\n', + fulltext2, + ]) + + f = self._makefilefn() + with self._maketransactionfn() as tr: + node0 = f.add(fulltext0, None, tr, 0, nullid, nullid) + node1 = f.add(fulltext1, meta1, tr, 1, node0, nullid) + node2 = f.add(fulltext2, meta2, tr, 2, nullid, nullid) + + self.assertEqual(f.rawsize(1), len(stored1)) + self.assertEqual(f.rawsize(2), len(stored2)) + + # Metadata header isn't recognized when parent isn't nullid. + self.assertEqual(f.size(1), len(stored1)) + self.assertEqual(f.size(2), len(fulltext2)) + + self.assertEqual(f.revision(node1), stored1) + self.assertEqual(f.revision(node1, raw=True), stored1) + self.assertEqual(f.revision(node2), stored2) + self.assertEqual(f.revision(node2, raw=True), stored2) + + self.assertEqual(f.read(node1), fulltext1) + self.assertEqual(f.read(node2), fulltext2) + + # Returns False when first parent is set. + self.assertFalse(f.renamed(node1)) + self.assertEqual(f.renamed(node2), (b'source1', b'\xbb' * 20)) + + self.assertTrue(f.cmp(node1, fulltext1)) + self.assertTrue(f.cmp(node1, stored1)) + self.assertFalse(f.cmp(node2, fulltext2)) + self.assertTrue(f.cmp(node2, stored2)) + + def testmetadataprefix(self): + # Content with metadata prefix has extra prefix inserted in storage. + fulltext0 = b'\x01\nfoo' + stored0 = b'\x01\n\x01\n\x01\nfoo' + + fulltext1 = b'\x01\nbar' + meta1 = { + b'copy': b'source0', + b'copyrev': b'b' * 40, + } + stored1 = b''.join([ + b'\x01\ncopy: source0\n', + b'copyrev: %s\n' % (b'b' * 40), + b'\x01\n\x01\nbar', + ]) + + f = self._makefilefn() + with self._maketransactionfn() as tr: + node0 = f.add(fulltext0, {}, tr, 0, nullid, nullid) + node1 = f.add(fulltext1, meta1, tr, 1, nullid, nullid) + + self.assertEqual(f.rawsize(0), len(stored0)) + self.assertEqual(f.rawsize(1), len(stored1)) + + # TODO this is buggy. + self.assertEqual(f.size(0), len(fulltext0) + 4) + + self.assertEqual(f.size(1), len(fulltext1)) + + self.assertEqual(f.revision(node0), stored0) + self.assertEqual(f.revision(node0, raw=True), stored0) + + self.assertEqual(f.revision(node1), stored1) + self.assertEqual(f.revision(node1, raw=True), stored1) + + self.assertEqual(f.read(node0), fulltext0) + self.assertEqual(f.read(node1), fulltext1) + + self.assertFalse(f.cmp(node0, fulltext0)) + self.assertTrue(f.cmp(node0, stored0)) + + self.assertFalse(f.cmp(node1, fulltext1)) + self.assertTrue(f.cmp(node1, stored0)) + + def testcensored(self): + f = self._makefilefn() + + stored1 = revlog.packmeta({ + b'censored': b'tombstone', + }, b'') + + # TODO tests are incomplete because we need the node to be + # different due to presence of censor metadata. But we can't + # do this with addrevision(). + with self._maketransactionfn() as tr: + node0 = f.add(b'foo', None, tr, 0, nullid, nullid) + f.addrevision(stored1, tr, 1, node0, nullid, + flags=revlog.REVIDX_ISCENSORED) + + self.assertEqual(f.flags(1), revlog.REVIDX_ISCENSORED) + self.assertTrue(f.iscensored(1)) + + self.assertEqual(f.revision(1), stored1) + self.assertEqual(f.revision(1, raw=True), stored1) + + self.assertEqual(f.read(1), b'') + +class ifilemutationtests(basetestcase): + """Generic tests for the ifilemutation interface. + + All file storage backends that support writing should conform to this + interface. + + Use ``makeifilemutationtests()`` to create an instance of this type. + """ + def testaddnoop(self): + f = self._makefilefn() + with self._maketransactionfn() as tr: + node0 = f.add(b'foo', None, tr, 0, nullid, nullid) + node1 = f.add(b'foo', None, tr, 0, nullid, nullid) + # Varying by linkrev shouldn't impact hash. + node2 = f.add(b'foo', None, tr, 1, nullid, nullid) + + self.assertEqual(node1, node0) + self.assertEqual(node2, node0) + self.assertEqual(len(f), 1) + + def testaddrevisionbadnode(self): + f = self._makefilefn() + with self._maketransactionfn() as tr: + # Adding a revision with bad node value fails. + with self.assertRaises(error.RevlogError): + f.addrevision(b'foo', tr, 0, nullid, nullid, node=b'\x01' * 20) + + def testaddrevisionunknownflag(self): + f = self._makefilefn() + with self._maketransactionfn() as tr: + for i in range(15, 0, -1): + if (1 << i) & ~revlog.REVIDX_KNOWN_FLAGS: + flags = 1 << i + break + + with self.assertRaises(error.RevlogError): + f.addrevision(b'foo', tr, 0, nullid, nullid, flags=flags) + + def testaddgroupsimple(self): + f = self._makefilefn() + + callbackargs = [] + def cb(*args, **kwargs): + callbackargs.append((args, kwargs)) + + def linkmapper(node): + return 0 + + with self._maketransactionfn() as tr: + nodes = f.addgroup([], None, tr, addrevisioncb=cb) + + self.assertEqual(nodes, []) + self.assertEqual(callbackargs, []) + self.assertEqual(len(f), 0) + + fulltext0 = b'foo' + delta0 = mdiff.trivialdiffheader(len(fulltext0)) + fulltext0 + + deltas = [ + (b'\x01' * 20, nullid, nullid, nullid, nullid, delta0, 0), + ] + + with self._maketransactionfn() as tr: + with self.assertRaises(error.RevlogError): + f.addgroup(deltas, linkmapper, tr, addrevisioncb=cb) + + node0 = f.add(fulltext0, None, tr, 0, nullid, nullid) + + f = self._makefilefn() + + deltas = [ + (node0, nullid, nullid, nullid, nullid, delta0, 0), + ] + + with self._maketransactionfn() as tr: + nodes = f.addgroup(deltas, linkmapper, tr, addrevisioncb=cb) + + self.assertEqual(nodes, [ + b'\x49\xd8\xcb\xb1\x5c\xe2\x57\x92\x04\x47' + b'\x00\x6b\x46\x97\x8b\x7a\xf9\x80\xa9\x79']) + + self.assertEqual(len(callbackargs), 1) + self.assertEqual(callbackargs[0][0][1], nodes[0]) + + self.assertEqual(list(f.revs()), [0]) + self.assertEqual(f.rev(nodes[0]), 0) + self.assertEqual(f.node(0), nodes[0]) + + def testaddgroupmultiple(self): + f = self._makefilefn() + + fulltexts = [ + b'foo', + b'bar', + b'x' * 1024, + ] + + nodes = [] + with self._maketransactionfn() as tr: + for fulltext in fulltexts: + nodes.append(f.add(fulltext, None, tr, 0, nullid, nullid)) + + f = self._makefilefn() + deltas = [] + for i, fulltext in enumerate(fulltexts): + delta = mdiff.trivialdiffheader(len(fulltext)) + fulltext + + deltas.append((nodes[i], nullid, nullid, nullid, nullid, delta, 0)) + + with self._maketransactionfn() as tr: + self.assertEqual(f.addgroup(deltas, lambda x: 0, tr), nodes) + + self.assertEqual(len(f), len(deltas)) + self.assertEqual(list(f.revs()), [0, 1, 2]) + self.assertEqual(f.rev(nodes[0]), 0) + self.assertEqual(f.rev(nodes[1]), 1) + self.assertEqual(f.rev(nodes[2]), 2) + self.assertEqual(f.node(0), nodes[0]) + self.assertEqual(f.node(1), nodes[1]) + self.assertEqual(f.node(2), nodes[2]) + +def makeifileindextests(makefilefn, maketransactionfn): + """Create a unittest.TestCase class suitable for testing file storage. + + ``makefilefn`` is a callable which receives the test case as an + argument and returns an object implementing the ``ifilestorage`` interface. + + ``maketransactionfn`` is a callable which receives the test case as an + argument and returns a transaction object. + + Returns a type that is a ``unittest.TestCase`` that can be used for + testing the object implementing the file storage interface. Simply + assign the returned value to a module-level attribute and a test loader + should find and run it automatically. + """ + d = { + r'_makefilefn': makefilefn, + r'_maketransactionfn': maketransactionfn, + } + return type(r'ifileindextests', (ifileindextests,), d) + +def makeifiledatatests(makefilefn, maketransactionfn): + d = { + r'_makefilefn': makefilefn, + r'_maketransactionfn': maketransactionfn, + } + return type(r'ifiledatatests', (ifiledatatests,), d) + +def makeifilemutationtests(makefilefn, maketransactionfn): + d = { + r'_makefilefn': makefilefn, + r'_maketransactionfn': maketransactionfn, + } + return type(r'ifilemutationtests', (ifilemutationtests,), d) diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -822,6 +822,7 @@ packages = ['mercurial', 'mercurial.thirdparty.zope.interface', 'mercurial.utils', 'mercurial.revlogutils', + 'mercurial.testing', 'hgext', 'hgext.convert', 'hgext.fsmonitor', 'hgext.fastannotate', 'hgext.fsmonitor.pywatchman', diff --git a/tests/test-storage.py b/tests/test-storage.py new file mode 100644 --- /dev/null +++ b/tests/test-storage.py @@ -0,0 +1,46 @@ +# This test verifies the conformance of various classes to various +# storage interfaces. +from __future__ import absolute_import + +import silenttestrunner + +from mercurial import ( + filelog, + transaction, + ui as uimod, + vfs as vfsmod, +) + +from mercurial.testing import ( + storage as storagetesting, +) + +STATE = { + 'lastindex': 0, + 'ui': uimod.ui(), + 'vfs': vfsmod.vfs(b'.', realpath=True), +} + +def makefilefn(self): + """Factory for filelog instances.""" + fl = filelog.filelog(STATE['vfs'], 'filelog-%d' % STATE['lastindex']) + STATE['lastindex'] += 1 + return fl + +def maketransaction(self): + vfsmap = {'plain': STATE['vfs']} + + return transaction.transaction(STATE['ui'].warn, STATE['vfs'], vfsmap, + 'journal', 'undo') + +# Assigning module-level attributes that inherit from unittest.TestCase +# is all that is needed to register tests. +filelogindextests = storagetesting.makeifileindextests(makefilefn, + maketransaction) +filelogdatatests = storagetesting.makeifiledatatests(makefilefn, + maketransaction) +filelogmutationtests = storagetesting.makeifilemutationtests(makefilefn, + maketransaction) + +if __name__ == '__main__': + silenttestrunner.main(__name__)