Mercurial > public > mercurial-scm > hg-stable
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) |