Commit | Line | Data |
---|---|---|
dbd6ff68 PG |
1 | """ |
2 | Intra2net 2017 | |
3 | ||
4 | =============================================================================== | |
5 | test_recover.py – behavior facing file corruption | |
6 | =============================================================================== | |
7 | ||
8 | Corruptors have the signature ``(unittest × string × bool × bool) → void``, | |
9 | where the *string* argument is the name of the file to modify, the *booleans* | |
10 | specialize the operation for compressed and encrypted data. Issues are | |
11 | communicated upward by throwing. | |
12 | ||
13 | - corrupt_header (): | |
14 | Modify the first object header where it hurts. With encryption, the tag | |
15 | is corrupted to cause authentication of the decrypted data to fail. For | |
16 | compressed data, the two byte magic is altered, for uncompressed | |
17 | archives, the tar header checksum field. | |
18 | ||
37ccf5bc PG |
19 | - corrupt_truncate (): |
20 | Drop the file’s content after two thirds, causing extraction of later | |
21 | objects to fail. Since the operation preserves the offsets of objects | |
22 | before the cutoff, this yields the same results regardless of whether | |
23 | restore or rescue mode is used. | |
24 | ||
dbd6ff68 PG |
25 | - corrupt_ctsize (): |
26 | Modify the *ctsize* field of a PDTCRYPT header. The goal is to have | |
27 | decryption continue past the end of the object, causing data | |
28 | authentication to fail and file reads to be at odds with the offsets in | |
29 | the index. Only applicable to encrypted archives; will raise | |
30 | *UndefinedTest* otherwise. | |
31 | ||
32 | - corrupt_entire_header (): | |
33 | Invert all bits of the first object header (PDTCRYPT, gzip, tar) without | |
0b5c1c5e PG |
34 | affecting the payload. This renders the object unreadable; the result will |
35 | resemble a file with arbitrary leading data but all the remaining object | |
dbd6ff68 PG |
36 | offsets intact, so the contents can still be extracted with index based |
37 | recovery. | |
38 | ||
39 | - corrupt_payload_start (): | |
40 | For all header variants, skip to the first byte past the header and | |
41 | corrupt it. Encrypted objects will fail to authenticate. Compressed | |
42 | objects will yield a bad CRC32. The Tar layer will take no notice but | |
43 | the extracted object will fail an independent checksum comparison with | |
44 | that of the original file. | |
45 | ||
46 | - corrupt_leading_garbage (): | |
47 | Prepend random data to an otherwise valid file. Creates a situation that | |
48 | index based recovery cannot handle by shifting the offsets of all objects | |
49 | in the file. In rescue mode, these objects must be located and extracted | |
50 | regardless. | |
51 | ||
52 | - corrupt_trailing_data (): | |
53 | Append data to an otherwise valid file. Both the recovery and rescue | |
54 | modes must be able to retrieve all objects from that file. | |
55 | ||
56 | - corrupt_volume (): | |
57 | Zero out an entire backup file. This is interesting for multivolume | |
58 | tests: all files from the affected volume must be missing but objects | |
59 | that span volume bounds will still be partially recoverable. | |
60 | ||
61 | - corrupt_hole (): | |
62 | Remove a region from a file. Following the damaged part, no object can be | |
63 | recovered in index mode, but rescue mode will still find those. The | |
64 | object containing the start of the hole will fail checksum tests because | |
65 | of the missing part and the overlap with the subsequent object. | |
66 | ||
67 | """ | |
68 | ||
fbdc9f4a PG |
69 | import logging |
70 | import os | |
71 | import shutil | |
3692fd82 | 72 | import stat |
b9cf4a0f PG |
73 | import sys |
74 | import unittest | |
fbdc9f4a | 75 | |
2fe5f6e7 PG |
76 | from functools import partial |
77 | ||
fbdc9f4a | 78 | import deltatar.deltatar as deltatar |
3267933a | 79 | import deltatar.crypto as crypto |
203cb25e | 80 | import deltatar.tarfile as tarfile |
fbdc9f4a PG |
81 | |
82 | from . import BaseTest | |
83 | ||
e25f31ac | 84 | TEST_PASSWORD = "test1234" |
85e7013f | 85 | TEST_VOLSIZ = 2 # MB |
e25f31ac | 86 | TEST_FILESPERVOL = 3 |
85e7013f PG |
87 | VOLUME_OVERHEAD = 1.4 # account for tar overhead when fitting files into |
88 | # volumes; this is black magic | |
20e1d773 | 89 | TEST_BLOCKSIZE = 4096 |
96fe6399 PG |
90 | |
91 | ############################################################################### | |
92 | ## helpers ## | |
93 | ############################################################################### | |
94 | ||
3267933a PG |
95 | def flip_bits (fname, off, b=0x01, n=1): |
96 | """ | |
97 | Open file *fname* at offset *off*, replacing the next *n* bytes with | |
98 | their values xor’ed with *b*. | |
99 | """ | |
100 | fd = os.open (fname, os.O_RDWR) | |
203cb25e | 101 | |
3267933a PG |
102 | try: |
103 | pos = os.lseek (fd, off, os.SEEK_SET) | |
104 | assert pos == off | |
105 | chunk = os.read (fd, n) | |
106 | chunk = bytes (map (lambda v: v ^ b, chunk)) | |
da8996f0 PG |
107 | pos = os.lseek (fd, off, os.SEEK_SET) |
108 | assert pos == off | |
3267933a PG |
109 | os.write (fd, chunk) |
110 | finally: | |
111 | os.close (fd) | |
112 | ||
203cb25e PG |
113 | |
114 | def gz_header_size (fname, off=0): | |
115 | """ | |
116 | Determine the length of the gzip header starting at *off* in file fname. | |
117 | ||
118 | The header is variable length because it may contain the filename as NUL | |
119 | terminated bytes. | |
120 | """ | |
121 | # length so we need to determine where the actual payload starts | |
122 | off = tarfile.GZ_HEADER_SIZE | |
123 | fd = os.open (fname, os.O_RDONLY) | |
124 | ||
125 | try: | |
126 | pos = os.lseek (fd, off, os.SEEK_SET) | |
127 | assert pos == off | |
128 | while os.read (fd, 1)[0] != 0: | |
129 | off += 1 | |
130 | pos = os.lseek (fd, off, os.SEEK_SET) | |
131 | assert pos == off | |
132 | finally: | |
133 | os.close (fd) | |
134 | ||
135 | return off | |
136 | ||
da8996f0 | 137 | |
96fe6399 PG |
138 | def is_pdt_encrypted (fname): |
139 | """ | |
140 | Returns true if the file contains at least one PDT header plus enough | |
141 | space for the object. | |
142 | """ | |
143 | try: | |
144 | with open (fname, "rb") as st: | |
145 | hdr = crypto.hdr_read_stream (st) | |
146 | siz = hdr ["ctsize"] | |
147 | assert (len (st.read (siz)) == siz) | |
148 | except Exception as exn: | |
149 | return False | |
150 | return True | |
151 | ||
152 | ||
3692fd82 PG |
153 | ############################################################################### |
154 | ## corruption simulators ## | |
155 | ############################################################################### | |
156 | ||
0c8baf2b PG |
157 | class UndefinedTest (Exception): |
158 | """No test available for the asked combination of parameters.""" | |
159 | ||
00b8c150 PG |
160 | def corrupt_header (_, fname, compress, encrypt): |
161 | """ | |
162 | Modify a significant byte in the object header of the format. | |
163 | """ | |
164 | if encrypt is True: # damage GCM tag | |
165 | flip_bits (fname, crypto.HDR_OFF_TAG + 1) | |
166 | elif compress is True: # invalidate magic | |
167 | flip_bits (fname, 1) | |
168 | else: # Fudge checksum. From tar(5): | |
169 | # | |
170 | # struct header_gnu_tar { | |
171 | # char name[100]; | |
172 | # char mode[8]; | |
173 | # char uid[8]; | |
174 | # char gid[8]; | |
175 | # char size[12]; | |
176 | # char mtime[12]; | |
177 | # char checksum[8]; | |
178 | # … | |
179 | flip_bits (fname, 100 + 8 + 8 + 8 + 12 + 12 + 1) | |
180 | ||
181 | ||
37ccf5bc PG |
182 | def corrupt_truncate (_, fname, _compress, _encrypt): |
183 | """ | |
184 | Shorten file by one third. | |
185 | """ | |
186 | fd = os.open (fname, os.O_WRONLY) | |
187 | size = os.lseek (fd, 0, os.SEEK_END) | |
188 | os.ftruncate (fd, 2 * size // 3) | |
189 | os.fsync (fd) | |
190 | os.close (fd) | |
191 | ||
192 | ||
0c8baf2b PG |
193 | def corrupt_ctsize (_, fname, compress, encrypt): |
194 | """ | |
195 | Blow up the size of an object so as to cause its apparent payload to leak | |
196 | into the next one. | |
197 | """ | |
198 | if encrypt is True: | |
199 | # damage lowest bit of second least significant byte of size field; | |
200 | # this effectively sets the ciphertext size to 422, causing it to | |
201 | # extend over the next object into the third one. | |
202 | return flip_bits (fname, crypto.HDR_OFF_CTSIZE + 1, b=0x01) | |
203 | raise UndefinedTest ("corrupt_ctsize %s %s %s" % (fname, compress, encrypt)) | |
204 | ||
205 | ||
da8996f0 PG |
206 | def corrupt_entire_header (_, fname, compress, encrypt): |
207 | """ | |
208 | Flip all bits in the first object header. | |
209 | """ | |
210 | if encrypt is True: | |
211 | flip_bits (fname, 0, 0xff, crypto.PDTCRYPT_HDR_SIZE) | |
dbd6ff68 | 212 | elif compress is True: |
da8996f0 PG |
213 | flip_bits (fname, 0, 0xff, gz_header_size (fname)) |
214 | else: | |
215 | flip_bits (fname, 0, 0xff, tarfile.BLOCKSIZE) | |
216 | ||
217 | ||
00b8c150 PG |
218 | def corrupt_payload_start (_, fname, compress, encrypt): |
219 | """ | |
220 | Modify the byte following the object header structure of the format. | |
221 | """ | |
222 | if encrypt is True: | |
223 | flip_bits (fname, crypto.PDTCRYPT_HDR_SIZE + 1) | |
224 | elif compress is True: | |
225 | flip_bits (fname, gz_header_size (fname) + 1) | |
226 | else: | |
227 | flip_bits (fname, tarfile.BLOCKSIZE + 1) | |
228 | ||
229 | ||
afb2d647 PG |
230 | def corrupt_leading_garbage (_, fname, compress, encrypt): |
231 | """ | |
232 | Prepend junk to file. | |
233 | """ | |
234 | aname = os.path.abspath (fname) | |
235 | infd = os.open (fname, os.O_RDONLY) | |
236 | size = os.lseek (infd, 0, os.SEEK_END) | |
237 | assert os.lseek (infd, 0, os.SEEK_SET) == 0 | |
238 | outfd = os.open (os.path.dirname (aname), os.O_WRONLY | os.O_TMPFILE, | |
239 | stat.S_IRUSR | stat.S_IWUSR) | |
a793ee30 | 240 | junk = os.urandom (42) |
afb2d647 PG |
241 | |
242 | # write new file with garbage prepended | |
243 | done = 0 | |
244 | os.write (outfd, junk) # junk first | |
245 | done += len (junk) | |
246 | while done < size: | |
247 | data = os.read (infd, TEST_BLOCKSIZE) | |
248 | os.write (outfd, data) | |
249 | done += len (data) | |
250 | ||
251 | assert os.lseek (outfd, 0, os.SEEK_CUR) == done | |
252 | ||
253 | # close and free old file | |
254 | os.close (infd) | |
255 | os.unlink (fname) | |
256 | ||
257 | # install the new file in its place, atomically | |
258 | path = "/proc/self/fd/%d" % outfd | |
259 | os.link (path, aname, src_dir_fd=0, follow_symlinks=True) | |
260 | os.close (outfd) | |
261 | ||
262 | ||
517d35b7 PG |
263 | def corrupt_trailing_data (_, fname, compress, encrypt): |
264 | """ | |
dbd6ff68 | 265 | Append random data to file. |
517d35b7 PG |
266 | """ |
267 | junk = os.urandom (42) | |
268 | fd = os.open (fname, os.O_WRONLY | os.O_APPEND) | |
269 | os.write (fd, junk) | |
270 | os.close (fd) | |
271 | ||
00b8c150 | 272 | |
20e1d773 PG |
273 | def corrupt_volume (_, fname, compress, encrypt): |
274 | """ | |
275 | Zero out an entire volume. | |
276 | """ | |
277 | fd = os.open (fname, os.O_WRONLY) | |
278 | size = os.lseek (fd, 0, os.SEEK_END) | |
279 | assert os.lseek (fd, 0, os.SEEK_SET) == 0 | |
280 | zeros = bytes (b'\x00' * TEST_BLOCKSIZE) | |
281 | while size > 0: | |
282 | todo = min (size, TEST_BLOCKSIZE) | |
283 | os.write (fd, zeros [:todo]) | |
284 | size -= todo | |
285 | os.close (fd) | |
286 | ||
287 | ||
3692fd82 PG |
288 | def corrupt_hole (_, fname, compress, encrypt): |
289 | """ | |
290 | Cut file in three pieces, reassemble without the middle one. | |
291 | """ | |
292 | aname = os.path.abspath (fname) | |
293 | infd = os.open (fname, os.O_RDONLY) | |
294 | size = os.lseek (infd, 0, os.SEEK_END) | |
295 | assert os.lseek (infd, 0, os.SEEK_SET) == 0 | |
296 | assert size > 3 * TEST_BLOCKSIZE | |
297 | hole = (size / 3, size * 2 / 3) | |
298 | outfd = os.open (os.path.dirname (aname), os.O_WRONLY | os.O_TMPFILE, | |
299 | stat.S_IRUSR | stat.S_IWUSR) | |
300 | ||
3692fd82 PG |
301 | done = 0 |
302 | while done < size: | |
303 | data = os.read (infd, TEST_BLOCKSIZE) | |
304 | if done < hole [0] or hole [1] < done: | |
305 | # only copy from outside hole | |
306 | os.write (outfd, data) | |
307 | done += len (data) | |
308 | ||
309 | os.close (infd) | |
310 | os.unlink (fname) | |
311 | ||
312 | path = "/proc/self/fd/%d" % outfd | |
313 | os.link (path, aname, src_dir_fd=0, follow_symlinks=True) | |
314 | os.close (outfd) | |
315 | ||
2fe5f6e7 PG |
316 | def immaculate (_, _fname, _compress, _encrypt): |
317 | """ | |
318 | No-op dummy. | |
319 | """ | |
320 | pass | |
3692fd82 | 321 | |
96fe6399 PG |
322 | ############################################################################### |
323 | ## tests ## | |
324 | ############################################################################### | |
203cb25e | 325 | |
0c6682ce | 326 | class DefectiveTest (BaseTest): |
fbdc9f4a PG |
327 | """ |
328 | Disaster recovery: restore corrupt backups. | |
329 | """ | |
330 | ||
96fe6399 PG |
331 | COMPRESSION = None |
332 | PASSWORD = None | |
9d89c237 PG |
333 | FAILURES = 0 # files that could not be restored |
334 | MISMATCHES = 0 # files that were restored but corrupted | |
00b8c150 | 335 | CORRUPT = corrupt_payload_start |
e25f31ac | 336 | VOLUMES = 1 |
4d4925de | 337 | MISSING = None # normally the number of failures |
96fe6399 | 338 | |
fbdc9f4a PG |
339 | |
340 | def setUp(self): | |
341 | ''' | |
342 | Create base test data | |
343 | ''' | |
96fe6399 PG |
344 | self.pwd = os.getcwd() |
345 | self.dst_path = "source_dir" | |
346 | self.src_path = "%s2" % self.dst_path | |
347 | self.hash = dict() | |
348 | ||
fbdc9f4a | 349 | os.system('rm -rf target_dir source_dir* backup_dir* huge') |
96fe6399 | 350 | os.makedirs (self.src_path) |
fbdc9f4a | 351 | |
96fe6399 | 352 | for i in range (5): |
85e7013f | 353 | f = "dummy_%d" % i |
96fe6399 PG |
354 | self.hash [f] = self.create_file ("%s/%s" |
355 | % (self.src_path, f), 5 + i) | |
fbdc9f4a | 356 | |
96fe6399 PG |
357 | |
358 | def tearDown(self): | |
359 | ''' | |
360 | Remove temporal files created by unit tests and reset globals. | |
361 | ''' | |
362 | os.chdir(self.pwd) | |
363 | os.system("rm -rf source_dir source_dir2 backup_dir*") | |
fbdc9f4a PG |
364 | |
365 | ||
2fe5f6e7 PG |
366 | @staticmethod |
367 | def default_volume_name (backup_file, _x, _y, n, *a, **kwa): | |
368 | return backup_file % n | |
0c6682ce | 369 | |
2fe5f6e7 | 370 | def gen_file_names (self, comp, pw): |
203cb25e | 371 | bak_path = "backup_dir" |
e25f31ac PG |
372 | backup_file = "the_full_backup_%0.2d.tar" |
373 | backup_full = ("%s/%s" % (bak_path, backup_file)) % 0 | |
96fe6399 PG |
374 | index_file = "the_full_index" |
375 | ||
376 | if self.COMPRESSION is not None: | |
377 | backup_file += ".gz" | |
378 | backup_full += ".gz" | |
379 | index_file += ".gz" | |
380 | ||
381 | if self.PASSWORD is not None: | |
e25f31ac PG |
382 | backup_file = "%s.%s" % (backup_file, deltatar.PDTCRYPT_EXTENSION) |
383 | backup_full = "%s.%s" % (backup_full, deltatar.PDTCRYPT_EXTENSION) | |
384 | index_file = "%s.%s" % (index_file , deltatar.PDTCRYPT_EXTENSION) | |
385 | ||
2fe5f6e7 PG |
386 | return bak_path, backup_file, backup_full, index_file |
387 | ||
388 | ||
047239f3 PG |
389 | def gen_multivol (self, nvol): |
390 | # add n files for one nth the volume size each, corrected | |
391 | # for metadata and tar block overhead | |
392 | fsiz = int ( ( TEST_VOLSIZ | |
393 | / (TEST_FILESPERVOL * VOLUME_OVERHEAD)) | |
394 | * 1024 * 1024) | |
395 | fcnt = (self.VOLUMES - 1) * TEST_FILESPERVOL | |
396 | for i in range (fcnt): | |
397 | nvol, invol = divmod(i, TEST_FILESPERVOL) | |
398 | f = "dummy_vol_%d_n_%0.2d" % (nvol, invol) | |
399 | self.hash [f] = self.create_file ("%s/%s" | |
400 | % (self.src_path, f), | |
401 | fsiz, | |
402 | random=True) | |
403 | ||
404 | ||
2fe5f6e7 PG |
405 | class RecoverTest (DefectiveTest): |
406 | """ | |
407 | Recover: restore corrupt backups from index file information. | |
408 | """ | |
409 | ||
410 | def test_recover_corrupt (self): | |
411 | """ | |
412 | Perform various damaging actions that cause unreadable objects. | |
413 | ||
414 | Expects the extraction to fail in normal mode. With disaster recovery, | |
415 | extraction must succeed, and exactly one file must be missing. | |
416 | """ | |
417 | mode = self.COMPRESSION or "#" | |
418 | bak_path, backup_file, backup_full, index_file = \ | |
419 | self.gen_file_names (self.COMPRESSION, self.PASSWORD) | |
420 | ||
e25f31ac | 421 | if self.VOLUMES > 1: |
047239f3 | 422 | self.gen_multivol (self.VOLUMES) |
e25f31ac | 423 | |
2fe5f6e7 | 424 | vname = partial (self.default_volume_name, backup_file) |
96fe6399 PG |
425 | dtar = deltatar.DeltaTar (mode=mode, |
426 | logger=None, | |
427 | password=self.PASSWORD, | |
203cb25e | 428 | index_name_func=lambda _: index_file, |
3267933a | 429 | volume_name_func=vname) |
fbdc9f4a PG |
430 | |
431 | dtar.create_full_backup \ | |
e25f31ac PG |
432 | (source_path=self.src_path, backup_path=bak_path, |
433 | max_volume_size=1) | |
96fe6399 PG |
434 | |
435 | if self.PASSWORD is not None: | |
436 | # ensure all files are at least superficially in PDT format | |
437 | for f in os.listdir (bak_path): | |
438 | assert is_pdt_encrypted ("%s/%s" % (bak_path, f)) | |
203cb25e PG |
439 | |
440 | # first restore must succeed | |
96fe6399 | 441 | dtar.restore_backup(target_path=self.dst_path, |
f090d35a PG |
442 | backup_indexes_paths=[ |
443 | "%s/%s" % (bak_path, index_file) | |
b750b280 PG |
444 | ], |
445 | disaster=tarfile.TOLERANCE_RECOVER, | |
446 | strict_validation=False) | |
203cb25e | 447 | for key, value in self.hash.items (): |
96fe6399 | 448 | f = "%s/%s" % (self.dst_path, key) |
b15e549b PG |
449 | assert os.path.exists (f) |
450 | assert value == self.md5sum (f) | |
96fe6399 PG |
451 | shutil.rmtree (self.dst_path) |
452 | shutil.rmtree (self.src_path) | |
203cb25e | 453 | |
00b8c150 PG |
454 | self.CORRUPT (backup_full, |
455 | self.COMPRESSION is not None, | |
456 | self.PASSWORD is not None) | |
203cb25e PG |
457 | |
458 | # normal restore must fail | |
96fe6399 PG |
459 | try: |
460 | dtar.restore_backup(target_path=self.dst_path, | |
203cb25e | 461 | backup_tar_path=backup_full) |
96fe6399 PG |
462 | except tarfile.CompressionError: |
463 | if self.PASSWORD is not None or self.COMPRESSION is not None: | |
464 | pass | |
00b8c150 PG |
465 | else: |
466 | raise | |
96fe6399 | 467 | except tarfile.ReadError: |
00b8c150 PG |
468 | # can happen with all three modes |
469 | pass | |
470 | except tarfile.DecryptionError: | |
471 | if self.PASSWORD is not None: | |
96fe6399 | 472 | pass |
00b8c150 PG |
473 | else: |
474 | raise | |
96fe6399 PG |
475 | |
476 | os.chdir (self.pwd) # not restored due to the error above | |
203cb25e | 477 | # but recover will succeed |
96fe6399 | 478 | failed = dtar.recover_backup(target_path=self.dst_path, |
b15e549b PG |
479 | backup_indexes_paths=[ |
480 | "%s/%s" % (bak_path, index_file) | |
481 | ]) | |
96fe6399 PG |
482 | |
483 | assert len (failed) == self.FAILURES | |
203cb25e PG |
484 | |
485 | # with one file missing | |
9d89c237 PG |
486 | missing = [] |
487 | mismatch = [] | |
203cb25e | 488 | for key, value in self.hash.items (): |
96fe6399 | 489 | kkey = "%s/%s" % (self.dst_path, key) |
b15e549b | 490 | if os.path.exists (kkey): |
9d89c237 PG |
491 | if value != self.md5sum (kkey): |
492 | mismatch.append (key) | |
203cb25e | 493 | else: |
757319dd | 494 | missing.append (key) |
4d4925de PG |
495 | |
496 | # usually, an object whose extraction fails will not be found on | |
497 | # disk afterwards so the number of failures equals that of missing | |
498 | # files. however, some modes will create partial files for objects | |
499 | # spanning multiple volumes that contain the parts whose checksums | |
500 | # were valid. | |
501 | assert len (missing) == (self.MISSING if self.MISSING is not None | |
502 | else self.FAILURES) | |
9d89c237 | 503 | assert len (mismatch) == self.MISMATCHES |
96fe6399 PG |
504 | |
505 | shutil.rmtree (self.dst_path) | |
506 | ||
507 | ||
0c6682ce PG |
508 | class RescueTest (DefectiveTest): |
509 | """ | |
510 | Rescue: restore corrupt backups from backup set that is damaged to a degree | |
511 | that the index file is worthless. | |
512 | """ | |
513 | ||
514 | def test_rescue_corrupt (self): | |
515 | """ | |
516 | Perform various damaging actions that cause unreadable objects, then | |
517 | attempt to extract objects regardless. | |
518 | """ | |
2fe5f6e7 PG |
519 | mode = self.COMPRESSION or "#" |
520 | bak_path, backup_file, backup_full, index_file = \ | |
521 | self.gen_file_names (self.COMPRESSION, self.PASSWORD) | |
0c6682ce PG |
522 | |
523 | if self.VOLUMES > 1: | |
047239f3 | 524 | self.gen_multivol (self.VOLUMES) |
0c6682ce | 525 | |
2fe5f6e7 | 526 | vname = partial (self.default_volume_name, backup_file) |
0c6682ce PG |
527 | dtar = deltatar.DeltaTar (mode=mode, |
528 | logger=None, | |
529 | password=self.PASSWORD, | |
530 | index_name_func=lambda _: index_file, | |
531 | volume_name_func=vname) | |
532 | ||
533 | dtar.create_full_backup \ | |
534 | (source_path=self.src_path, backup_path=bak_path, | |
535 | max_volume_size=1) | |
536 | ||
537 | if self.PASSWORD is not None: | |
538 | # ensure all files are at least superficially in PDT format | |
539 | for f in os.listdir (bak_path): | |
540 | assert is_pdt_encrypted ("%s/%s" % (bak_path, f)) | |
541 | ||
542 | # first restore must succeed | |
543 | dtar.restore_backup(target_path=self.dst_path, | |
544 | backup_indexes_paths=[ | |
545 | "%s/%s" % (bak_path, index_file) | |
b750b280 PG |
546 | ], |
547 | disaster=tarfile.TOLERANCE_RECOVER, | |
548 | strict_validation=False) | |
0c6682ce PG |
549 | for key, value in self.hash.items (): |
550 | f = "%s/%s" % (self.dst_path, key) | |
551 | assert os.path.exists (f) | |
552 | assert value == self.md5sum (f) | |
553 | shutil.rmtree (self.dst_path) | |
554 | shutil.rmtree (self.src_path) | |
555 | ||
556 | self.CORRUPT (backup_full, | |
557 | self.COMPRESSION is not None, | |
558 | self.PASSWORD is not None) | |
559 | ||
560 | # normal restore must fail | |
561 | try: | |
562 | dtar.restore_backup(target_path=self.dst_path, | |
563 | backup_tar_path=backup_full) | |
564 | except tarfile.CompressionError: | |
565 | if self.PASSWORD is not None or self.COMPRESSION is not None: | |
566 | pass | |
567 | else: | |
568 | raise | |
569 | except tarfile.ReadError: | |
570 | # can happen with all three modes | |
571 | pass | |
572 | except tarfile.DecryptionError: | |
573 | if self.PASSWORD is not None: | |
574 | pass | |
575 | else: | |
576 | raise | |
577 | ||
578 | os.chdir (self.pwd) # not restored due to the error above | |
579 | # but recover will succeed | |
580 | failed = dtar.rescue_backup(target_path=self.dst_path, | |
2fe5f6e7 | 581 | backup_tar_path=backup_full) |
0c6682ce PG |
582 | # with one file missing |
583 | missing = [] | |
584 | mismatch = [] | |
585 | for key, value in self.hash.items (): | |
586 | kkey = "%s/%s" % (self.dst_path, key) | |
587 | if os.path.exists (kkey): | |
588 | if value != self.md5sum (kkey): | |
589 | mismatch.append (key) | |
590 | else: | |
591 | missing.append (key) | |
592 | ||
79bc14cf | 593 | assert len (failed) == self.FAILURES |
2fe5f6e7 PG |
594 | assert len (missing) == (self.MISSING if self.MISSING is not None |
595 | else self.FAILURES) | |
0c6682ce PG |
596 | assert len (mismatch) == self.MISMATCHES |
597 | ||
598 | shutil.rmtree (self.dst_path) | |
599 | ||
600 | ||
2fe5f6e7 PG |
601 | class GenIndexTest (DefectiveTest): |
602 | """ | |
603 | Deducing an index for a backup with tarfile. | |
604 | """ | |
605 | ||
606 | def test_gen_index (self): | |
607 | """ | |
608 | Create backup, leave it unharmed, then generate an index. | |
609 | """ | |
610 | mode = self.COMPRESSION or "#" | |
611 | bak_path, backup_file, backup_full, index_file = \ | |
612 | self.gen_file_names (self.COMPRESSION, self.PASSWORD) | |
613 | ||
047239f3 PG |
614 | if self.VOLUMES > 1: |
615 | self.gen_multivol (self.VOLUMES) | |
616 | ||
2fe5f6e7 PG |
617 | vname = partial (self.default_volume_name, backup_file) |
618 | dtar = deltatar.DeltaTar (mode=mode, | |
619 | logger=None, | |
620 | password=self.PASSWORD, | |
621 | index_name_func=lambda _: index_file, | |
622 | volume_name_func=vname) | |
623 | ||
624 | dtar.create_full_backup \ | |
625 | (source_path=self.src_path, backup_path=bak_path, | |
626 | max_volume_size=1) | |
627 | ||
27ee4dd4 PG |
628 | def gen_volume_name (nvol): |
629 | return os.path.join (bak_path, vname (backup_full, True, nvol)) | |
630 | ||
631 | psidx = tarfile.gen_rescue_index (gen_volume_name, | |
632 | mode, | |
633 | password=self.PASSWORD) | |
2fe5f6e7 | 634 | |
047239f3 PG |
635 | # correct for objects spanning volumes: these are treated as separate |
636 | # in the index! | |
637 | assert len (psidx) - self.VOLUMES + 1 == len (self.hash) | |
2fe5f6e7 PG |
638 | |
639 | ||
640 | ############################################################################### | |
641 | # rescue | |
642 | ############################################################################### | |
643 | ||
e25f31ac | 644 | class RecoverCorruptPayloadTestBase (RecoverTest): |
00b8c150 PG |
645 | COMPRESSION = None |
646 | PASSWORD = None | |
9d89c237 PG |
647 | FAILURES = 0 # tarfile will restore but corrupted, as |
648 | MISMATCHES = 1 # revealed by the hash | |
00b8c150 | 649 | |
e25f31ac PG |
650 | class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase): |
651 | VOLUMES = 1 | |
652 | ||
653 | class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase): | |
654 | VOLUMES = 3 | |
655 | ||
00b8c150 | 656 | |
e25f31ac | 657 | class RecoverCorruptPayloadGZTestBase (RecoverTest): |
00b8c150 PG |
658 | COMPRESSION = "#gz" |
659 | PASSWORD = None | |
660 | FAILURES = 1 | |
9d89c237 | 661 | MISMATCHES = 0 |
00b8c150 | 662 | |
e25f31ac PG |
663 | class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase): |
664 | VOLUMES = 1 | |
00b8c150 | 665 | |
e25f31ac PG |
666 | class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase): |
667 | VOLUMES = 3 | |
668 | ||
669 | ||
670 | class RecoverCorruptPayloadGZAESTestBase (RecoverTest): | |
00b8c150 PG |
671 | COMPRESSION = "#gz" |
672 | PASSWORD = TEST_PASSWORD | |
673 | FAILURES = 1 | |
9d89c237 | 674 | MISMATCHES = 0 |
00b8c150 | 675 | |
e25f31ac PG |
676 | class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase): |
677 | VOLUMES = 1 | |
678 | ||
679 | class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase): | |
680 | VOLUMES = 3 | |
00b8c150 | 681 | |
e25f31ac PG |
682 | |
683 | class RecoverCorruptHeaderTestBase (RecoverTest): | |
0349168a PG |
684 | COMPRESSION = None |
685 | PASSWORD = None | |
686 | FAILURES = 1 | |
687 | CORRUPT = corrupt_header | |
9d89c237 | 688 | MISMATCHES = 0 |
0349168a | 689 | |
e25f31ac PG |
690 | class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase): |
691 | VOLUMES = 1 | |
692 | ||
693 | class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase): | |
694 | VOLUMES = 3 | |
695 | ||
0349168a | 696 | |
e25f31ac | 697 | class RecoverCorruptHeaderGZTestBase (RecoverTest): |
96fe6399 PG |
698 | COMPRESSION = "#gz" |
699 | PASSWORD = None | |
700 | FAILURES = 1 | |
00b8c150 | 701 | CORRUPT = corrupt_header |
9d89c237 | 702 | MISMATCHES = 0 |
96fe6399 | 703 | |
e25f31ac PG |
704 | class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase): |
705 | VOLUMES = 1 | |
3267933a | 706 | |
e25f31ac PG |
707 | class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase): |
708 | VOLUMES = 3 | |
709 | ||
710 | ||
711 | class RecoverCorruptHeaderGZAESTestBase (RecoverTest): | |
96fe6399 PG |
712 | COMPRESSION = "#gz" |
713 | PASSWORD = TEST_PASSWORD | |
714 | FAILURES = 1 | |
00b8c150 | 715 | CORRUPT = corrupt_header |
9d89c237 | 716 | MISMATCHES = 0 |
fbdc9f4a | 717 | |
e25f31ac PG |
718 | class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase): |
719 | VOLUMES = 1 | |
720 | ||
721 | class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase): | |
722 | VOLUMES = 3 | |
da8996f0 | 723 | |
e25f31ac | 724 | |
37ccf5bc PG |
725 | class RecoverCorruptTruncateTestBase (RecoverTest): |
726 | COMPRESSION = None | |
727 | PASSWORD = None | |
728 | FAILURES = 0 | |
729 | CORRUPT = corrupt_truncate | |
730 | MISMATCHES = 0 | |
731 | ||
732 | class RecoverCorruptTruncateTest (RecoverCorruptTruncateTestBase): | |
733 | pass | |
734 | ||
735 | class RecoverCorruptTruncateGZTest (RecoverCorruptTruncateTestBase): | |
736 | """Two files that failed missing.""" | |
737 | COMPRESSION = "#gz" | |
738 | FAILURES = 2 | |
739 | ||
740 | class RecoverCorruptTruncateGZAESTest (RecoverCorruptTruncateTestBase): | |
741 | """Two files that failed missing.""" | |
742 | COMPRESSION = "#gz" | |
743 | PASSWORD = TEST_PASSWORD | |
744 | FAILURES = 2 | |
745 | ||
746 | ||
e25f31ac | 747 | class RecoverCorruptEntireHeaderTestBase (RecoverTest): |
da8996f0 PG |
748 | COMPRESSION = None |
749 | PASSWORD = None | |
750 | FAILURES = 1 | |
751 | CORRUPT = corrupt_entire_header | |
9d89c237 | 752 | MISMATCHES = 0 |
da8996f0 | 753 | |
e25f31ac PG |
754 | class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase): |
755 | VOLUMES = 1 | |
756 | ||
757 | class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase): | |
758 | VOLUMES = 3 | |
759 | ||
da8996f0 | 760 | |
e25f31ac | 761 | class RecoverCorruptEntireHeaderGZTestBase (RecoverTest): |
da8996f0 PG |
762 | COMPRESSION = "#gz" |
763 | PASSWORD = None | |
764 | FAILURES = 1 | |
765 | CORRUPT = corrupt_entire_header | |
9d89c237 | 766 | MISMATCHES = 0 |
da8996f0 | 767 | |
e25f31ac PG |
768 | class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase): |
769 | VOLUMES = 1 | |
da8996f0 | 770 | |
e25f31ac PG |
771 | class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase): |
772 | VOLUMES = 3 | |
773 | ||
774 | ||
775 | class RecoverCorruptEntireHeaderGZAESTestBase (RecoverTest): | |
da8996f0 PG |
776 | COMPRESSION = "#gz" |
777 | PASSWORD = TEST_PASSWORD | |
778 | FAILURES = 1 | |
779 | CORRUPT = corrupt_entire_header | |
9d89c237 | 780 | MISMATCHES = 0 |
da8996f0 | 781 | |
e25f31ac PG |
782 | class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase): |
783 | VOLUMES = 1 | |
784 | ||
785 | class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase): | |
786 | VOLUMES = 3 | |
517d35b7 | 787 | |
e25f31ac PG |
788 | |
789 | class RecoverCorruptTrailingDataTestBase (RecoverTest): | |
517d35b7 PG |
790 | # plain Tar is indifferent against traling data and the results |
791 | # are consistent | |
792 | COMPRESSION = None | |
793 | PASSWORD = None | |
794 | FAILURES = 0 | |
795 | CORRUPT = corrupt_trailing_data | |
796 | MISMATCHES = 0 | |
797 | ||
e25f31ac PG |
798 | class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase): |
799 | VOLUMES = 1 | |
800 | ||
801 | class RecoverCorruptTrailingDataMultiTest (RecoverCorruptTrailingDataTestBase): | |
14895f4b PG |
802 | # the last object in first archive has extra bytes somewhere in the |
803 | # middle because tar itself performs no data checksumming. | |
804 | MISMATCHES = 1 | |
e25f31ac PG |
805 | VOLUMES = 3 |
806 | ||
517d35b7 | 807 | |
e25f31ac | 808 | class RecoverCorruptTrailingDataGZTestBase (RecoverTest): |
517d35b7 PG |
809 | # reading past the final object will cause decompression failure; |
810 | # all objects except for the last survive unharmed though | |
811 | COMPRESSION = "#gz" | |
812 | PASSWORD = None | |
813 | FAILURES = 1 | |
814 | CORRUPT = corrupt_trailing_data | |
815 | MISMATCHES = 0 | |
816 | ||
e25f31ac PG |
817 | class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase): |
818 | VOLUMES = 1 | |
517d35b7 | 819 | |
e25f31ac PG |
820 | class RecoverCorruptTrailingDataGZMultiTest (RecoverCorruptTrailingDataGZTestBase): |
821 | VOLUMES = 3 | |
14895f4b PG |
822 | # the last file of the first volume will only contain the data of the |
823 | # second part which is contained in the second volume. this happens | |
824 | # because the CRC32 is wrong for the first part so it gets discarded, then | |
825 | # the object is recreated from the first header of the second volume, | |
826 | # containing only the remainder of the data. | |
827 | MISMATCHES = 1 | |
4d4925de | 828 | MISSING = 0 |
e25f31ac PG |
829 | |
830 | ||
831 | class RecoverCorruptTrailingDataGZAESTestBase (RecoverTest): | |
517d35b7 PG |
832 | COMPRESSION = "#gz" |
833 | PASSWORD = TEST_PASSWORD | |
834 | FAILURES = 0 | |
835 | CORRUPT = corrupt_trailing_data | |
836 | MISMATCHES = 0 | |
837 | ||
e25f31ac PG |
838 | class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase): |
839 | VOLUMES = 1 | |
840 | ||
841 | class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase): | |
842 | VOLUMES = 3 | |
517d35b7 | 843 | |
20e1d773 PG |
844 | |
845 | class RecoverCorruptVolumeBaseTest (RecoverTest): | |
846 | COMPRESSION = None | |
847 | PASSWORD = None | |
848 | FAILURES = 8 | |
849 | CORRUPT = corrupt_volume | |
850 | VOLUMES = 3 | |
851 | ||
852 | class RecoverCorruptVolumeTest (RecoverCorruptVolumeBaseTest): | |
853 | pass | |
854 | ||
3692fd82 PG |
855 | class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest): |
856 | COMPRESSION = "#gz" | |
857 | ||
858 | class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest): | |
20e1d773 | 859 | COMPRESSION = "#gz" |
3692fd82 PG |
860 | PASSWORD = TEST_PASSWORD |
861 | ||
862 | ||
b9cf4a0f | 863 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
3692fd82 PG |
864 | class RecoverCorruptHoleBaseTest (RecoverTest): |
865 | """ | |
866 | Cut bytes from the middle of a volume. | |
867 | ||
868 | Index-based recovery works only up to the hole. | |
869 | """ | |
870 | COMPRESSION = None | |
20e1d773 | 871 | PASSWORD = None |
3692fd82 PG |
872 | FAILURES = 3 |
873 | CORRUPT = corrupt_hole | |
874 | VOLUMES = 2 # request two vols to swell up the first one | |
875 | MISMATCHES = 1 | |
876 | ||
b9cf4a0f | 877 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
3692fd82 PG |
878 | class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest): |
879 | FAILURES = 2 | |
880 | ||
b9cf4a0f | 881 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
3692fd82 PG |
882 | class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest): |
883 | COMPRESSION = "#gz" | |
884 | MISSING = 2 | |
20e1d773 | 885 | |
b9cf4a0f | 886 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
3692fd82 | 887 | class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest): |
20e1d773 PG |
888 | COMPRESSION = "#gz" |
889 | PASSWORD = TEST_PASSWORD | |
3692fd82 | 890 | MISSING = 2 |
20e1d773 | 891 | |
2fe5f6e7 PG |
892 | ############################################################################### |
893 | # rescue | |
894 | ############################################################################### | |
895 | ||
37ccf5bc PG |
896 | class RescueCorruptTruncateTestBase (RescueTest): |
897 | COMPRESSION = None | |
898 | PASSWORD = None | |
899 | FAILURES = 0 | |
900 | CORRUPT = corrupt_truncate | |
901 | MISMATCHES = 0 | |
902 | ||
903 | class RescueCorruptTruncateTest (RescueCorruptTruncateTestBase): | |
904 | pass | |
905 | ||
906 | class RescueCorruptTruncateGZTest (RescueCorruptTruncateTestBase): | |
907 | """Two files that failed missing.""" | |
908 | COMPRESSION = "#gz" | |
909 | MISSING = 2 | |
910 | ||
911 | class RescueCorruptTruncateGZAESTest (RescueCorruptTruncateTestBase): | |
912 | """Two files missing but didn’t fail on account of their absence.""" | |
913 | COMPRESSION = "#gz" | |
914 | PASSWORD = TEST_PASSWORD | |
915 | MISSING = 2 | |
916 | ||
917 | ||
b9cf4a0f | 918 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
2fe5f6e7 PG |
919 | class RescueCorruptHoleBaseTest (RescueTest): |
920 | """ | |
921 | Cut bytes from the middle of a volume. | |
922 | """ | |
923 | COMPRESSION = None | |
924 | PASSWORD = None | |
79bc14cf | 925 | FAILURES = 0 |
2fe5f6e7 PG |
926 | CORRUPT = corrupt_hole |
927 | VOLUMES = 2 # request two vols to swell up the first one | |
79bc14cf PG |
928 | MISMATCHES = 2 # intersected by hole |
929 | MISSING = 1 # excised by hole | |
2fe5f6e7 | 930 | |
b9cf4a0f | 931 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
2fe5f6e7 | 932 | class RescueCorruptHoleTest (RescueCorruptHoleBaseTest): |
79bc14cf | 933 | pass |
2fe5f6e7 | 934 | |
b9cf4a0f | 935 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
2fe5f6e7 PG |
936 | class RescueCorruptHoleGZTest (RescueCorruptHoleBaseTest): |
937 | COMPRESSION = "#gz" | |
79bc14cf PG |
938 | # the decompressor explodes in our face processing the first dummy, nothing |
939 | # we can do to recover | |
940 | FAILURES = 1 | |
2fe5f6e7 | 941 | |
b9cf4a0f | 942 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
2fe5f6e7 PG |
943 | class RescueCorruptHoleGZAESTest (RescueCorruptHoleBaseTest): |
944 | COMPRESSION = "#gz" | |
945 | PASSWORD = TEST_PASSWORD | |
79bc14cf PG |
946 | # again, ignoring the crypto errors yields a bad zlib stream causing the |
947 | # decompressor to abort where the hole begins; the file is extracted up | |
948 | # to this point though | |
949 | FAILURES = 1 | |
2fe5f6e7 | 950 | |
0c8baf2b | 951 | |
afb2d647 | 952 | class RescueCorruptHeaderCTSizeGZAESTest (RescueTest): |
0c8baf2b PG |
953 | COMPRESSION = "#gz" |
954 | PASSWORD = TEST_PASSWORD | |
955 | FAILURES = 0 | |
956 | CORRUPT = corrupt_ctsize | |
957 | MISMATCHES = 0 | |
958 | ||
959 | ||
b9cf4a0f | 960 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
afb2d647 PG |
961 | class RescueCorruptLeadingGarbageTestBase (RescueTest): |
962 | # plain Tar is indifferent against traling data and the results | |
963 | # are consistent | |
964 | COMPRESSION = None | |
965 | PASSWORD = None | |
966 | FAILURES = 0 | |
967 | CORRUPT = corrupt_leading_garbage | |
968 | MISMATCHES = 0 | |
969 | ||
b9cf4a0f | 970 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
afb2d647 PG |
971 | class RescueCorruptLeadingGarbageSingleTest (RescueCorruptLeadingGarbageTestBase): |
972 | VOLUMES = 1 | |
973 | ||
b9cf4a0f | 974 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
afb2d647 PG |
975 | class RescueCorruptLeadingGarbageMultiTest (RescueCorruptLeadingGarbageTestBase): |
976 | # the last object in first archive has extra bytes somewhere in the | |
977 | # middle because tar itself performs no data checksumming. | |
978 | MISMATCHES = 2 | |
979 | VOLUMES = 3 | |
980 | ||
981 | ||
2fe5f6e7 PG |
982 | ############################################################################### |
983 | # index | |
984 | ############################################################################### | |
985 | ||
986 | class GenIndexIntactBaseTest (GenIndexTest): | |
987 | """ | |
988 | """ | |
989 | COMPRESSION = None | |
990 | PASSWORD = None | |
991 | FAILURES = 0 | |
992 | CORRUPT = immaculate | |
993 | VOLUMES = 1 | |
994 | MISMATCHES = 1 | |
995 | ||
047239f3 PG |
996 | class GenIndexIntactSingleTest (GenIndexIntactBaseTest): |
997 | pass | |
998 | ||
999 | class GenIndexIntactSingleGZTest (GenIndexIntactBaseTest): | |
1000 | COMPRESSION = "#gz" | |
1001 | MISSING = 2 | |
1002 | ||
1003 | class GenIndexIntactSingleGZAESTest (GenIndexIntactBaseTest): | |
1004 | COMPRESSION = "#gz" | |
1005 | PASSWORD = TEST_PASSWORD | |
1006 | MISSING = 2 | |
2fe5f6e7 | 1007 | |
047239f3 PG |
1008 | class GenIndexIntactMultiTest (GenIndexIntactBaseTest): |
1009 | VOLUMES = 3 | |
2fe5f6e7 PG |
1010 | pass |
1011 | ||
047239f3 PG |
1012 | class GenIndexIntactMultiGZTest (GenIndexIntactBaseTest): |
1013 | VOLUMES = 3 | |
2fe5f6e7 PG |
1014 | COMPRESSION = "#gz" |
1015 | MISSING = 2 | |
1016 | ||
047239f3 PG |
1017 | class GenIndexIntactMultiGZAESTest (GenIndexIntactBaseTest): |
1018 | VOLUMES = 3 | |
2fe5f6e7 PG |
1019 | COMPRESSION = "#gz" |
1020 | PASSWORD = TEST_PASSWORD | |
1021 | MISSING = 2 | |
1022 | ||
6e1f5355 | 1023 | |
37ccf5bc PG |
1024 | class GenIndexCorruptTruncateBaseTest (GenIndexTest): |
1025 | """ | |
1026 | Recreate index from file that lacks the latter portion. | |
1027 | """ | |
1028 | COMPRESSION = None | |
1029 | PASSWORD = None | |
1030 | FAILURES = 0 | |
1031 | CORRUPT = corrupt_truncate | |
1032 | MISSING = 2 | |
1033 | ||
1034 | class GenIndexCorruptTruncateTest (GenIndexCorruptTruncateBaseTest): | |
1035 | pass | |
1036 | ||
1037 | class GenIndexCorruptTruncateGZTest (GenIndexCorruptTruncateBaseTest): | |
1038 | COMPRESSION = "#gz" | |
1039 | ||
1040 | class GenIndexCorruptTruncateGZAESTest (GenIndexCorruptTruncateBaseTest): | |
1041 | COMPRESSION = "#gz" | |
1042 | PASSWORD = TEST_PASSWORD | |
1043 | ||
1044 | ||
6e1f5355 PG |
1045 | class GenIndexCorruptHoleBaseTest (GenIndexTest): |
1046 | """ | |
1047 | Recreate index from file with hole. | |
1048 | """ | |
1049 | COMPRESSION = None | |
1050 | PASSWORD = None | |
1051 | FAILURES = 0 | |
1052 | CORRUPT = corrupt_hole | |
1053 | VOLUMES = 1 | |
1054 | MISMATCHES = 1 | |
1055 | ||
1056 | class GenIndexCorruptHoleTest (GenIndexCorruptHoleBaseTest): | |
1057 | pass | |
1058 | ||
1059 | class GenIndexCorruptHoleGZTest (GenIndexCorruptHoleBaseTest): | |
1060 | COMPRESSION = "#gz" | |
1061 | MISSING = 2 | |
1062 | ||
1063 | class GenIndexCorruptHoleGZAESTest (GenIndexCorruptHoleBaseTest): | |
1064 | COMPRESSION = "#gz" | |
1065 | PASSWORD = TEST_PASSWORD | |
1066 | MISSING = 2 | |
1067 | ||
1068 | ||
6e1f5355 PG |
1069 | class GenIndexCorruptEntireHeaderBaseTest (GenIndexTest): |
1070 | """ | |
0b5c1c5e | 1071 | Recreate index from file with defective headers. |
6e1f5355 PG |
1072 | """ |
1073 | COMPRESSION = None | |
1074 | PASSWORD = None | |
1075 | FAILURES = 0 | |
1076 | CORRUPT = corrupt_entire_header | |
1077 | VOLUMES = 1 | |
1078 | MISMATCHES = 1 | |
1079 | ||
1080 | class GenIndexCorruptEntireHeaderTest (GenIndexCorruptEntireHeaderBaseTest): | |
1081 | pass | |
1082 | ||
1083 | class GenIndexCorruptEntireHeaderGZTest (GenIndexCorruptEntireHeaderBaseTest): | |
1084 | COMPRESSION = "#gz" | |
1085 | MISSING = 2 | |
1086 | ||
1087 | class GenIndexCorruptEntireHeaderGZAESTest (GenIndexCorruptEntireHeaderBaseTest): | |
1088 | COMPRESSION = "#gz" | |
1089 | PASSWORD = TEST_PASSWORD | |
1090 | MISSING = 2 | |
1091 |