comparison mercurial/testing/storage.py @ 40052:cdf61ab1f54c

testing: add file storage integration for bad hashes and censoring In order to implement these tests, we need a backdoor to write data into storage backends while bypassing normal checks. We invent a callable to do that. As part of writing the tests, I found a bug with censorrevision() pretty quickly! After calling censorrevision(), attempting to access revision data for an affected node raises a cryptic error related to malformed compression. This appears to be due to the revlog not adjusting delta chains as part of censoring. I also found a bug with regards to hash verification and revision fulltext caching. Essentially, we cache the fulltext before hash verification. If we look up the fulltext after a failed hash verification, we don't get a hash verification exception. Furthermore, the behavior of revision(raw=True) can be inconsistent depending on the order of operations. I'll be fixing both these bugs in subsequent commits. Differential Revision: https://phab.mercurial-scm.org/D4865
author Gregory Szorc <gregory.szorc@gmail.com>
date Wed, 03 Oct 2018 10:56:48 -0700
parents 8e136940c0e6
children 801ccd8e67c0
comparison
equal deleted inserted replaced
40051:8e136940c0e6 40052:cdf61ab1f54c
859 self.assertTrue(f.cmp(node0, stored0)) 859 self.assertTrue(f.cmp(node0, stored0))
860 860
861 self.assertFalse(f.cmp(node1, fulltext1)) 861 self.assertFalse(f.cmp(node1, fulltext1))
862 self.assertTrue(f.cmp(node1, stored0)) 862 self.assertTrue(f.cmp(node1, stored0))
863 863
864 def testbadnoderead(self):
865 f = self._makefilefn()
866
867 fulltext0 = b'foo\n' * 30
868 fulltext1 = fulltext0 + b'bar\n'
869
870 with self._maketransactionfn() as tr:
871 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
872 node1 = b'\xaa' * 20
873
874 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1,
875 rawtext=fulltext1)
876
877 self.assertEqual(len(f), 2)
878 self.assertEqual(f.parents(node1), (node0, nullid))
879
880 # revision() raises since it performs hash verification.
881 with self.assertRaises(error.StorageError):
882 f.revision(node1)
883
884 # revision(raw=True) still verifies hashes.
885 # TODO this is buggy because of cache interaction.
886 self.assertEqual(f.revision(node1, raw=True), fulltext1)
887
888 # read() behaves like revision().
889 # TODO this is buggy because of cache interaction.
890 f.read(node1)
891
892 # We can't test renamed() here because some backends may not require
893 # reading/validating the fulltext to return rename metadata.
894
895 def testbadnoderevisionraw(self):
896 # Like above except we test revision(raw=True) first to isolate
897 # revision caching behavior.
898 f = self._makefilefn()
899
900 fulltext0 = b'foo\n' * 30
901 fulltext1 = fulltext0 + b'bar\n'
902
903 with self._maketransactionfn() as tr:
904 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
905 node1 = b'\xaa' * 20
906
907 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1,
908 rawtext=fulltext1)
909
910 with self.assertRaises(error.StorageError):
911 f.revision(node1, raw=True)
912
913 with self.assertRaises(error.StorageError):
914 f.revision(node1, raw=True)
915
916 def testbadnoderevisionraw(self):
917 # Like above except we test read() first to isolate revision caching
918 # behavior.
919 f = self._makefilefn()
920
921 fulltext0 = b'foo\n' * 30
922 fulltext1 = fulltext0 + b'bar\n'
923
924 with self._maketransactionfn() as tr:
925 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
926 node1 = b'\xaa' * 20
927
928 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1,
929 rawtext=fulltext1)
930
931 with self.assertRaises(error.StorageError):
932 f.read(node1)
933
934 # TODO this should raise error.StorageError.
935 f.read(node1)
936
937 def testbadnodedelta(self):
938 f = self._makefilefn()
939
940 fulltext0 = b'foo\n' * 31
941 fulltext1 = fulltext0 + b'bar\n'
942 fulltext2 = fulltext1 + b'baz\n'
943
944 with self._maketransactionfn() as tr:
945 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
946 node1 = b'\xaa' * 20
947
948 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1,
949 rawtext=fulltext1)
950
951 with self.assertRaises(error.StorageError):
952 f.read(node1)
953
954 diff = mdiff.textdiff(fulltext1, fulltext2)
955 node2 = storageutil.hashrevisionsha1(fulltext2, node1, nullid)
956 deltas = [(node2, node1, nullid, b'\x01' * 20, node1, diff, 0)]
957
958 # This /might/ fail on some backends.
959 with self._maketransactionfn() as tr:
960 f.addgroup(deltas, lambda x: 0, tr)
961
962 self.assertEqual(len(f), 3)
963
964 # Assuming a delta is stored, we shouldn't need to validate node1 in
965 # order to retrieve node2.
966 self.assertEqual(f.read(node2), fulltext2)
967
864 def testcensored(self): 968 def testcensored(self):
865 f = self._makefilefn() 969 f = self._makefilefn()
866 970
867 stored1 = storageutil.packmeta({ 971 stored1 = storageutil.packmeta({
868 b'censored': b'tombstone', 972 b'censored': b'tombstone',
869 }, b'') 973 }, b'')
870 974
871 # TODO tests are incomplete because we need the node to be
872 # different due to presence of censor metadata. But we can't
873 # do this with addrevision().
874 with self._maketransactionfn() as tr: 975 with self._maketransactionfn() as tr:
875 node0 = f.add(b'foo', None, tr, 0, nullid, nullid) 976 node0 = f.add(b'foo', None, tr, 0, nullid, nullid)
876 f.addrevision(stored1, tr, 1, node0, nullid, 977
877 flags=repository.REVISION_FLAG_CENSORED) 978 # The node value doesn't matter since we can't verify it.
979 node1 = b'\xbb' * 20
980
981 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1, stored1,
982 censored=True)
878 983
879 self.assertTrue(f.iscensored(1)) 984 self.assertTrue(f.iscensored(1))
880 985
881 self.assertEqual(f.revision(1), stored1) 986 with self.assertRaises(error.CensoredNodeError):
987 f.revision(1)
988
882 self.assertEqual(f.revision(1, raw=True), stored1) 989 self.assertEqual(f.revision(1, raw=True), stored1)
883 990
884 self.assertEqual(f.read(1), b'') 991 with self.assertRaises(error.CensoredNodeError):
992 f.read(1)
993
994 def testcensoredrawrevision(self):
995 # Like above, except we do the revision(raw=True) request first to
996 # isolate revision caching behavior.
997
998 f = self._makefilefn()
999
1000 stored1 = storageutil.packmeta({
1001 b'censored': b'tombstone',
1002 }, b'')
1003
1004 with self._maketransactionfn() as tr:
1005 node0 = f.add(b'foo', None, tr, 0, nullid, nullid)
1006
1007 # The node value doesn't matter since we can't verify it.
1008 node1 = b'\xbb' * 20
1009
1010 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1, stored1,
1011 censored=True)
1012
1013 with self.assertRaises(error.CensoredNodeError):
1014 f.revision(1, raw=True)
885 1015
886 class ifilemutationtests(basetestcase): 1016 class ifilemutationtests(basetestcase):
887 """Generic tests for the ifilemutation interface. 1017 """Generic tests for the ifilemutation interface.
888 1018
889 All file storage backends that support writing should conform to this 1019 All file storage backends that support writing should conform to this
1001 self.assertEqual(f.rev(nodes[1]), 1) 1131 self.assertEqual(f.rev(nodes[1]), 1)
1002 self.assertEqual(f.rev(nodes[2]), 2) 1132 self.assertEqual(f.rev(nodes[2]), 2)
1003 self.assertEqual(f.node(0), nodes[0]) 1133 self.assertEqual(f.node(0), nodes[0])
1004 self.assertEqual(f.node(1), nodes[1]) 1134 self.assertEqual(f.node(1), nodes[1])
1005 self.assertEqual(f.node(2), nodes[2]) 1135 self.assertEqual(f.node(2), nodes[2])
1136
1137 def testdeltaagainstcensored(self):
1138 # Attempt to apply a delta made against a censored revision.
1139 f = self._makefilefn()
1140
1141 stored1 = storageutil.packmeta({
1142 b'censored': b'tombstone',
1143 }, b'')
1144
1145 with self._maketransactionfn() as tr:
1146 node0 = f.add(b'foo\n' * 30, None, tr, 0, nullid, nullid)
1147
1148 # The node value doesn't matter since we can't verify it.
1149 node1 = b'\xbb' * 20
1150
1151 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1, stored1,
1152 censored=True)
1153
1154 delta = mdiff.textdiff(b'bar\n' * 30, (b'bar\n' * 30) + b'baz\n')
1155 deltas = [(b'\xcc' * 20, node1, nullid, b'\x01' * 20, node1, delta, 0)]
1156
1157 with self._maketransactionfn() as tr:
1158 with self.assertRaises(error.CensoredBaseError):
1159 f.addgroup(deltas, lambda x: 0, tr)
1160
1161 def testcensorrevisionbasic(self):
1162 f = self._makefilefn()
1163
1164 with self._maketransactionfn() as tr:
1165 node0 = f.add(b'foo\n' * 30, None, tr, 0, nullid, nullid)
1166 node1 = f.add(b'foo\n' * 31, None, tr, 1, node0, nullid)
1167 node2 = f.add(b'foo\n' * 32, None, tr, 2, node1, nullid)
1168
1169 with self._maketransactionfn() as tr:
1170 f.censorrevision(tr, node1)
1171
1172 self.assertEqual(len(f), 3)
1173 self.assertEqual(list(f.revs()), [0, 1, 2])
1174
1175 self.assertEqual(f.read(node0), b'foo\n' * 30)
1176
1177 # TODO revlog can't resolve revision after censor. Probably due to a
1178 # cache on the revlog instance.
1179 with self.assertRaises(error.StorageError):
1180 self.assertEqual(f.read(node2), b'foo\n' * 32)
1181
1182 # TODO should raise CensoredNodeError, but fallout from above prevents.
1183 with self.assertRaises(error.StorageError):
1184 f.read(node1)
1006 1185
1007 def testgetstrippointnoparents(self): 1186 def testgetstrippointnoparents(self):
1008 # N revisions where none have parents. 1187 # N revisions where none have parents.
1009 f = self._makefilefn() 1188 f = self._makefilefn()
1010 1189
1119 self.assertEqual(len(f), 1) 1298 self.assertEqual(len(f), 1)
1120 1299
1121 with self.assertRaises(error.LookupError): 1300 with self.assertRaises(error.LookupError):
1122 f.rev(node1) 1301 f.rev(node1)
1123 1302
1124 def makeifileindextests(makefilefn, maketransactionfn): 1303 def makeifileindextests(makefilefn, maketransactionfn, addrawrevisionfn):
1125 """Create a unittest.TestCase class suitable for testing file storage. 1304 """Create a unittest.TestCase class suitable for testing file storage.
1126 1305
1127 ``makefilefn`` is a callable which receives the test case as an 1306 ``makefilefn`` is a callable which receives the test case as an
1128 argument and returns an object implementing the ``ifilestorage`` interface. 1307 argument and returns an object implementing the ``ifilestorage`` interface.
1129 1308
1130 ``maketransactionfn`` is a callable which receives the test case as an 1309 ``maketransactionfn`` is a callable which receives the test case as an
1131 argument and returns a transaction object. 1310 argument and returns a transaction object.
1311
1312 ``addrawrevisionfn`` is a callable which receives arguments describing a
1313 low-level revision to add. This callable allows the insertion of
1314 potentially bad data into the store in order to facilitate testing.
1132 1315
1133 Returns a type that is a ``unittest.TestCase`` that can be used for 1316 Returns a type that is a ``unittest.TestCase`` that can be used for
1134 testing the object implementing the file storage interface. Simply 1317 testing the object implementing the file storage interface. Simply
1135 assign the returned value to a module-level attribute and a test loader 1318 assign the returned value to a module-level attribute and a test loader
1136 should find and run it automatically. 1319 should find and run it automatically.
1137 """ 1320 """
1138 d = { 1321 d = {
1139 r'_makefilefn': makefilefn, 1322 r'_makefilefn': makefilefn,
1140 r'_maketransactionfn': maketransactionfn, 1323 r'_maketransactionfn': maketransactionfn,
1324 r'_addrawrevisionfn': addrawrevisionfn,
1141 } 1325 }
1142 return type(r'ifileindextests', (ifileindextests,), d) 1326 return type(r'ifileindextests', (ifileindextests,), d)
1143 1327
1144 def makeifiledatatests(makefilefn, maketransactionfn): 1328 def makeifiledatatests(makefilefn, maketransactionfn, addrawrevisionfn):
1145 d = { 1329 d = {
1146 r'_makefilefn': makefilefn, 1330 r'_makefilefn': makefilefn,
1147 r'_maketransactionfn': maketransactionfn, 1331 r'_maketransactionfn': maketransactionfn,
1332 r'_addrawrevisionfn': addrawrevisionfn,
1148 } 1333 }
1149 return type(r'ifiledatatests', (ifiledatatests,), d) 1334 return type(r'ifiledatatests', (ifiledatatests,), d)
1150 1335
1151 def makeifilemutationtests(makefilefn, maketransactionfn): 1336 def makeifilemutationtests(makefilefn, maketransactionfn, addrawrevisionfn):
1152 d = { 1337 d = {
1153 r'_makefilefn': makefilefn, 1338 r'_makefilefn': makefilefn,
1154 r'_maketransactionfn': maketransactionfn, 1339 r'_maketransactionfn': maketransactionfn,
1340 r'_addrawrevisionfn': addrawrevisionfn,
1155 } 1341 }
1156 return type(r'ifilemutationtests', (ifilemutationtests,), d) 1342 return type(r'ifilemutationtests', (ifilemutationtests,), d)