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 | |
34 | affecting the payload. This renders the object unreadable; the file will | |
35 | be resemble one with arbitrary leading data but all the remaining object | |
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) | |
444 | ]) | |
203cb25e | 445 | for key, value in self.hash.items (): |
96fe6399 | 446 | f = "%s/%s" % (self.dst_path, key) |
b15e549b PG |
447 | assert os.path.exists (f) |
448 | assert value == self.md5sum (f) | |
96fe6399 PG |
449 | shutil.rmtree (self.dst_path) |
450 | shutil.rmtree (self.src_path) | |
203cb25e | 451 | |
00b8c150 PG |
452 | self.CORRUPT (backup_full, |
453 | self.COMPRESSION is not None, | |
454 | self.PASSWORD is not None) | |
203cb25e PG |
455 | |
456 | # normal restore must fail | |
96fe6399 PG |
457 | try: |
458 | dtar.restore_backup(target_path=self.dst_path, | |
203cb25e | 459 | backup_tar_path=backup_full) |
96fe6399 PG |
460 | except tarfile.CompressionError: |
461 | if self.PASSWORD is not None or self.COMPRESSION is not None: | |
462 | pass | |
00b8c150 PG |
463 | else: |
464 | raise | |
96fe6399 | 465 | except tarfile.ReadError: |
00b8c150 PG |
466 | # can happen with all three modes |
467 | pass | |
468 | except tarfile.DecryptionError: | |
469 | if self.PASSWORD is not None: | |
96fe6399 | 470 | pass |
00b8c150 PG |
471 | else: |
472 | raise | |
96fe6399 PG |
473 | |
474 | os.chdir (self.pwd) # not restored due to the error above | |
203cb25e | 475 | # but recover will succeed |
96fe6399 | 476 | failed = dtar.recover_backup(target_path=self.dst_path, |
b15e549b PG |
477 | backup_indexes_paths=[ |
478 | "%s/%s" % (bak_path, index_file) | |
479 | ]) | |
96fe6399 PG |
480 | |
481 | assert len (failed) == self.FAILURES | |
203cb25e PG |
482 | |
483 | # with one file missing | |
9d89c237 PG |
484 | missing = [] |
485 | mismatch = [] | |
203cb25e | 486 | for key, value in self.hash.items (): |
96fe6399 | 487 | kkey = "%s/%s" % (self.dst_path, key) |
b15e549b | 488 | if os.path.exists (kkey): |
9d89c237 PG |
489 | if value != self.md5sum (kkey): |
490 | mismatch.append (key) | |
203cb25e | 491 | else: |
757319dd | 492 | missing.append (key) |
4d4925de PG |
493 | |
494 | # usually, an object whose extraction fails will not be found on | |
495 | # disk afterwards so the number of failures equals that of missing | |
496 | # files. however, some modes will create partial files for objects | |
497 | # spanning multiple volumes that contain the parts whose checksums | |
498 | # were valid. | |
499 | assert len (missing) == (self.MISSING if self.MISSING is not None | |
500 | else self.FAILURES) | |
9d89c237 | 501 | assert len (mismatch) == self.MISMATCHES |
96fe6399 PG |
502 | |
503 | shutil.rmtree (self.dst_path) | |
504 | ||
505 | ||
0c6682ce PG |
506 | class RescueTest (DefectiveTest): |
507 | """ | |
508 | Rescue: restore corrupt backups from backup set that is damaged to a degree | |
509 | that the index file is worthless. | |
510 | """ | |
511 | ||
512 | def test_rescue_corrupt (self): | |
513 | """ | |
514 | Perform various damaging actions that cause unreadable objects, then | |
515 | attempt to extract objects regardless. | |
516 | """ | |
2fe5f6e7 PG |
517 | mode = self.COMPRESSION or "#" |
518 | bak_path, backup_file, backup_full, index_file = \ | |
519 | self.gen_file_names (self.COMPRESSION, self.PASSWORD) | |
0c6682ce PG |
520 | |
521 | if self.VOLUMES > 1: | |
047239f3 | 522 | self.gen_multivol (self.VOLUMES) |
0c6682ce | 523 | |
2fe5f6e7 | 524 | vname = partial (self.default_volume_name, backup_file) |
0c6682ce PG |
525 | dtar = deltatar.DeltaTar (mode=mode, |
526 | logger=None, | |
527 | password=self.PASSWORD, | |
528 | index_name_func=lambda _: index_file, | |
529 | volume_name_func=vname) | |
530 | ||
531 | dtar.create_full_backup \ | |
532 | (source_path=self.src_path, backup_path=bak_path, | |
533 | max_volume_size=1) | |
534 | ||
535 | if self.PASSWORD is not None: | |
536 | # ensure all files are at least superficially in PDT format | |
537 | for f in os.listdir (bak_path): | |
538 | assert is_pdt_encrypted ("%s/%s" % (bak_path, f)) | |
539 | ||
540 | # first restore must succeed | |
541 | dtar.restore_backup(target_path=self.dst_path, | |
542 | backup_indexes_paths=[ | |
543 | "%s/%s" % (bak_path, index_file) | |
544 | ]) | |
545 | for key, value in self.hash.items (): | |
546 | f = "%s/%s" % (self.dst_path, key) | |
547 | assert os.path.exists (f) | |
548 | assert value == self.md5sum (f) | |
549 | shutil.rmtree (self.dst_path) | |
550 | shutil.rmtree (self.src_path) | |
551 | ||
552 | self.CORRUPT (backup_full, | |
553 | self.COMPRESSION is not None, | |
554 | self.PASSWORD is not None) | |
555 | ||
556 | # normal restore must fail | |
557 | try: | |
558 | dtar.restore_backup(target_path=self.dst_path, | |
559 | backup_tar_path=backup_full) | |
560 | except tarfile.CompressionError: | |
561 | if self.PASSWORD is not None or self.COMPRESSION is not None: | |
562 | pass | |
563 | else: | |
564 | raise | |
565 | except tarfile.ReadError: | |
566 | # can happen with all three modes | |
567 | pass | |
568 | except tarfile.DecryptionError: | |
569 | if self.PASSWORD is not None: | |
570 | pass | |
571 | else: | |
572 | raise | |
573 | ||
574 | os.chdir (self.pwd) # not restored due to the error above | |
575 | # but recover will succeed | |
576 | failed = dtar.rescue_backup(target_path=self.dst_path, | |
2fe5f6e7 | 577 | backup_tar_path=backup_full) |
0c6682ce PG |
578 | # with one file missing |
579 | missing = [] | |
580 | mismatch = [] | |
581 | for key, value in self.hash.items (): | |
582 | kkey = "%s/%s" % (self.dst_path, key) | |
583 | if os.path.exists (kkey): | |
584 | if value != self.md5sum (kkey): | |
585 | mismatch.append (key) | |
586 | else: | |
587 | missing.append (key) | |
588 | ||
79bc14cf | 589 | assert len (failed) == self.FAILURES |
2fe5f6e7 PG |
590 | assert len (missing) == (self.MISSING if self.MISSING is not None |
591 | else self.FAILURES) | |
0c6682ce PG |
592 | assert len (mismatch) == self.MISMATCHES |
593 | ||
594 | shutil.rmtree (self.dst_path) | |
595 | ||
596 | ||
2fe5f6e7 PG |
597 | class GenIndexTest (DefectiveTest): |
598 | """ | |
599 | Deducing an index for a backup with tarfile. | |
600 | """ | |
601 | ||
602 | def test_gen_index (self): | |
603 | """ | |
604 | Create backup, leave it unharmed, then generate an index. | |
605 | """ | |
606 | mode = self.COMPRESSION or "#" | |
607 | bak_path, backup_file, backup_full, index_file = \ | |
608 | self.gen_file_names (self.COMPRESSION, self.PASSWORD) | |
609 | ||
047239f3 PG |
610 | if self.VOLUMES > 1: |
611 | self.gen_multivol (self.VOLUMES) | |
612 | ||
2fe5f6e7 PG |
613 | vname = partial (self.default_volume_name, backup_file) |
614 | dtar = deltatar.DeltaTar (mode=mode, | |
615 | logger=None, | |
616 | password=self.PASSWORD, | |
617 | index_name_func=lambda _: index_file, | |
618 | volume_name_func=vname) | |
619 | ||
620 | dtar.create_full_backup \ | |
621 | (source_path=self.src_path, backup_path=bak_path, | |
622 | max_volume_size=1) | |
623 | ||
27ee4dd4 PG |
624 | def gen_volume_name (nvol): |
625 | return os.path.join (bak_path, vname (backup_full, True, nvol)) | |
626 | ||
627 | psidx = tarfile.gen_rescue_index (gen_volume_name, | |
628 | mode, | |
629 | password=self.PASSWORD) | |
2fe5f6e7 | 630 | |
047239f3 PG |
631 | # correct for objects spanning volumes: these are treated as separate |
632 | # in the index! | |
633 | assert len (psidx) - self.VOLUMES + 1 == len (self.hash) | |
2fe5f6e7 PG |
634 | |
635 | ||
636 | ############################################################################### | |
637 | # rescue | |
638 | ############################################################################### | |
639 | ||
e25f31ac | 640 | class RecoverCorruptPayloadTestBase (RecoverTest): |
00b8c150 PG |
641 | COMPRESSION = None |
642 | PASSWORD = None | |
9d89c237 PG |
643 | FAILURES = 0 # tarfile will restore but corrupted, as |
644 | MISMATCHES = 1 # revealed by the hash | |
00b8c150 | 645 | |
e25f31ac PG |
646 | class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase): |
647 | VOLUMES = 1 | |
648 | ||
649 | class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase): | |
650 | VOLUMES = 3 | |
651 | ||
00b8c150 | 652 | |
e25f31ac | 653 | class RecoverCorruptPayloadGZTestBase (RecoverTest): |
00b8c150 PG |
654 | COMPRESSION = "#gz" |
655 | PASSWORD = None | |
656 | FAILURES = 1 | |
9d89c237 | 657 | MISMATCHES = 0 |
00b8c150 | 658 | |
e25f31ac PG |
659 | class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase): |
660 | VOLUMES = 1 | |
00b8c150 | 661 | |
e25f31ac PG |
662 | class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase): |
663 | VOLUMES = 3 | |
664 | ||
665 | ||
666 | class RecoverCorruptPayloadGZAESTestBase (RecoverTest): | |
00b8c150 PG |
667 | COMPRESSION = "#gz" |
668 | PASSWORD = TEST_PASSWORD | |
669 | FAILURES = 1 | |
9d89c237 | 670 | MISMATCHES = 0 |
00b8c150 | 671 | |
e25f31ac PG |
672 | class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase): |
673 | VOLUMES = 1 | |
674 | ||
675 | class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase): | |
676 | VOLUMES = 3 | |
00b8c150 | 677 | |
e25f31ac PG |
678 | |
679 | class RecoverCorruptHeaderTestBase (RecoverTest): | |
0349168a PG |
680 | COMPRESSION = None |
681 | PASSWORD = None | |
682 | FAILURES = 1 | |
683 | CORRUPT = corrupt_header | |
9d89c237 | 684 | MISMATCHES = 0 |
0349168a | 685 | |
e25f31ac PG |
686 | class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase): |
687 | VOLUMES = 1 | |
688 | ||
689 | class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase): | |
690 | VOLUMES = 3 | |
691 | ||
0349168a | 692 | |
e25f31ac | 693 | class RecoverCorruptHeaderGZTestBase (RecoverTest): |
96fe6399 PG |
694 | COMPRESSION = "#gz" |
695 | PASSWORD = None | |
696 | FAILURES = 1 | |
00b8c150 | 697 | CORRUPT = corrupt_header |
9d89c237 | 698 | MISMATCHES = 0 |
96fe6399 | 699 | |
e25f31ac PG |
700 | class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase): |
701 | VOLUMES = 1 | |
3267933a | 702 | |
e25f31ac PG |
703 | class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase): |
704 | VOLUMES = 3 | |
705 | ||
706 | ||
707 | class RecoverCorruptHeaderGZAESTestBase (RecoverTest): | |
96fe6399 PG |
708 | COMPRESSION = "#gz" |
709 | PASSWORD = TEST_PASSWORD | |
710 | FAILURES = 1 | |
00b8c150 | 711 | CORRUPT = corrupt_header |
9d89c237 | 712 | MISMATCHES = 0 |
fbdc9f4a | 713 | |
e25f31ac PG |
714 | class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase): |
715 | VOLUMES = 1 | |
716 | ||
717 | class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase): | |
718 | VOLUMES = 3 | |
da8996f0 | 719 | |
e25f31ac | 720 | |
37ccf5bc PG |
721 | class RecoverCorruptTruncateTestBase (RecoverTest): |
722 | COMPRESSION = None | |
723 | PASSWORD = None | |
724 | FAILURES = 0 | |
725 | CORRUPT = corrupt_truncate | |
726 | MISMATCHES = 0 | |
727 | ||
728 | class RecoverCorruptTruncateTest (RecoverCorruptTruncateTestBase): | |
729 | pass | |
730 | ||
731 | class RecoverCorruptTruncateGZTest (RecoverCorruptTruncateTestBase): | |
732 | """Two files that failed missing.""" | |
733 | COMPRESSION = "#gz" | |
734 | FAILURES = 2 | |
735 | ||
736 | class RecoverCorruptTruncateGZAESTest (RecoverCorruptTruncateTestBase): | |
737 | """Two files that failed missing.""" | |
738 | COMPRESSION = "#gz" | |
739 | PASSWORD = TEST_PASSWORD | |
740 | FAILURES = 2 | |
741 | ||
742 | ||
e25f31ac | 743 | class RecoverCorruptEntireHeaderTestBase (RecoverTest): |
da8996f0 PG |
744 | COMPRESSION = None |
745 | PASSWORD = None | |
746 | FAILURES = 1 | |
747 | CORRUPT = corrupt_entire_header | |
9d89c237 | 748 | MISMATCHES = 0 |
da8996f0 | 749 | |
e25f31ac PG |
750 | class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase): |
751 | VOLUMES = 1 | |
752 | ||
753 | class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase): | |
754 | VOLUMES = 3 | |
755 | ||
da8996f0 | 756 | |
e25f31ac | 757 | class RecoverCorruptEntireHeaderGZTestBase (RecoverTest): |
da8996f0 PG |
758 | COMPRESSION = "#gz" |
759 | PASSWORD = None | |
760 | FAILURES = 1 | |
761 | CORRUPT = corrupt_entire_header | |
9d89c237 | 762 | MISMATCHES = 0 |
da8996f0 | 763 | |
e25f31ac PG |
764 | class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase): |
765 | VOLUMES = 1 | |
da8996f0 | 766 | |
e25f31ac PG |
767 | class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase): |
768 | VOLUMES = 3 | |
769 | ||
770 | ||
771 | class RecoverCorruptEntireHeaderGZAESTestBase (RecoverTest): | |
da8996f0 PG |
772 | COMPRESSION = "#gz" |
773 | PASSWORD = TEST_PASSWORD | |
774 | FAILURES = 1 | |
775 | CORRUPT = corrupt_entire_header | |
9d89c237 | 776 | MISMATCHES = 0 |
da8996f0 | 777 | |
e25f31ac PG |
778 | class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase): |
779 | VOLUMES = 1 | |
780 | ||
781 | class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase): | |
782 | VOLUMES = 3 | |
517d35b7 | 783 | |
e25f31ac PG |
784 | |
785 | class RecoverCorruptTrailingDataTestBase (RecoverTest): | |
517d35b7 PG |
786 | # plain Tar is indifferent against traling data and the results |
787 | # are consistent | |
788 | COMPRESSION = None | |
789 | PASSWORD = None | |
790 | FAILURES = 0 | |
791 | CORRUPT = corrupt_trailing_data | |
792 | MISMATCHES = 0 | |
793 | ||
e25f31ac PG |
794 | class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase): |
795 | VOLUMES = 1 | |
796 | ||
797 | class RecoverCorruptTrailingDataMultiTest (RecoverCorruptTrailingDataTestBase): | |
14895f4b PG |
798 | # the last object in first archive has extra bytes somewhere in the |
799 | # middle because tar itself performs no data checksumming. | |
800 | MISMATCHES = 1 | |
e25f31ac PG |
801 | VOLUMES = 3 |
802 | ||
517d35b7 | 803 | |
e25f31ac | 804 | class RecoverCorruptTrailingDataGZTestBase (RecoverTest): |
517d35b7 PG |
805 | # reading past the final object will cause decompression failure; |
806 | # all objects except for the last survive unharmed though | |
807 | COMPRESSION = "#gz" | |
808 | PASSWORD = None | |
809 | FAILURES = 1 | |
810 | CORRUPT = corrupt_trailing_data | |
811 | MISMATCHES = 0 | |
812 | ||
e25f31ac PG |
813 | class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase): |
814 | VOLUMES = 1 | |
517d35b7 | 815 | |
e25f31ac PG |
816 | class RecoverCorruptTrailingDataGZMultiTest (RecoverCorruptTrailingDataGZTestBase): |
817 | VOLUMES = 3 | |
14895f4b PG |
818 | # the last file of the first volume will only contain the data of the |
819 | # second part which is contained in the second volume. this happens | |
820 | # because the CRC32 is wrong for the first part so it gets discarded, then | |
821 | # the object is recreated from the first header of the second volume, | |
822 | # containing only the remainder of the data. | |
823 | MISMATCHES = 1 | |
4d4925de | 824 | MISSING = 0 |
e25f31ac PG |
825 | |
826 | ||
827 | class RecoverCorruptTrailingDataGZAESTestBase (RecoverTest): | |
517d35b7 PG |
828 | COMPRESSION = "#gz" |
829 | PASSWORD = TEST_PASSWORD | |
830 | FAILURES = 0 | |
831 | CORRUPT = corrupt_trailing_data | |
832 | MISMATCHES = 0 | |
833 | ||
e25f31ac PG |
834 | class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase): |
835 | VOLUMES = 1 | |
836 | ||
837 | class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase): | |
838 | VOLUMES = 3 | |
517d35b7 | 839 | |
20e1d773 PG |
840 | |
841 | class RecoverCorruptVolumeBaseTest (RecoverTest): | |
842 | COMPRESSION = None | |
843 | PASSWORD = None | |
844 | FAILURES = 8 | |
845 | CORRUPT = corrupt_volume | |
846 | VOLUMES = 3 | |
847 | ||
848 | class RecoverCorruptVolumeTest (RecoverCorruptVolumeBaseTest): | |
849 | pass | |
850 | ||
3692fd82 PG |
851 | class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest): |
852 | COMPRESSION = "#gz" | |
853 | ||
854 | class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest): | |
20e1d773 | 855 | COMPRESSION = "#gz" |
3692fd82 PG |
856 | PASSWORD = TEST_PASSWORD |
857 | ||
858 | ||
b9cf4a0f | 859 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
3692fd82 PG |
860 | class RecoverCorruptHoleBaseTest (RecoverTest): |
861 | """ | |
862 | Cut bytes from the middle of a volume. | |
863 | ||
864 | Index-based recovery works only up to the hole. | |
865 | """ | |
866 | COMPRESSION = None | |
20e1d773 | 867 | PASSWORD = None |
3692fd82 PG |
868 | FAILURES = 3 |
869 | CORRUPT = corrupt_hole | |
870 | VOLUMES = 2 # request two vols to swell up the first one | |
871 | MISMATCHES = 1 | |
872 | ||
b9cf4a0f | 873 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
3692fd82 PG |
874 | class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest): |
875 | FAILURES = 2 | |
876 | ||
b9cf4a0f | 877 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
3692fd82 PG |
878 | class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest): |
879 | COMPRESSION = "#gz" | |
880 | MISSING = 2 | |
20e1d773 | 881 | |
b9cf4a0f | 882 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
3692fd82 | 883 | class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest): |
20e1d773 PG |
884 | COMPRESSION = "#gz" |
885 | PASSWORD = TEST_PASSWORD | |
3692fd82 | 886 | MISSING = 2 |
20e1d773 | 887 | |
2fe5f6e7 PG |
888 | ############################################################################### |
889 | # rescue | |
890 | ############################################################################### | |
891 | ||
37ccf5bc PG |
892 | class RescueCorruptTruncateTestBase (RescueTest): |
893 | COMPRESSION = None | |
894 | PASSWORD = None | |
895 | FAILURES = 0 | |
896 | CORRUPT = corrupt_truncate | |
897 | MISMATCHES = 0 | |
898 | ||
899 | class RescueCorruptTruncateTest (RescueCorruptTruncateTestBase): | |
900 | pass | |
901 | ||
902 | class RescueCorruptTruncateGZTest (RescueCorruptTruncateTestBase): | |
903 | """Two files that failed missing.""" | |
904 | COMPRESSION = "#gz" | |
905 | MISSING = 2 | |
906 | ||
907 | class RescueCorruptTruncateGZAESTest (RescueCorruptTruncateTestBase): | |
908 | """Two files missing but didn’t fail on account of their absence.""" | |
909 | COMPRESSION = "#gz" | |
910 | PASSWORD = TEST_PASSWORD | |
911 | MISSING = 2 | |
912 | ||
913 | ||
b9cf4a0f | 914 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
2fe5f6e7 PG |
915 | class RescueCorruptHoleBaseTest (RescueTest): |
916 | """ | |
917 | Cut bytes from the middle of a volume. | |
918 | """ | |
919 | COMPRESSION = None | |
920 | PASSWORD = None | |
79bc14cf | 921 | FAILURES = 0 |
2fe5f6e7 PG |
922 | CORRUPT = corrupt_hole |
923 | VOLUMES = 2 # request two vols to swell up the first one | |
79bc14cf PG |
924 | MISMATCHES = 2 # intersected by hole |
925 | MISSING = 1 # excised by hole | |
2fe5f6e7 | 926 | |
b9cf4a0f | 927 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
2fe5f6e7 | 928 | class RescueCorruptHoleTest (RescueCorruptHoleBaseTest): |
79bc14cf | 929 | pass |
2fe5f6e7 | 930 | |
b9cf4a0f | 931 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
2fe5f6e7 PG |
932 | class RescueCorruptHoleGZTest (RescueCorruptHoleBaseTest): |
933 | COMPRESSION = "#gz" | |
79bc14cf PG |
934 | # the decompressor explodes in our face processing the first dummy, nothing |
935 | # we can do to recover | |
936 | FAILURES = 1 | |
2fe5f6e7 | 937 | |
b9cf4a0f | 938 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
2fe5f6e7 PG |
939 | class RescueCorruptHoleGZAESTest (RescueCorruptHoleBaseTest): |
940 | COMPRESSION = "#gz" | |
941 | PASSWORD = TEST_PASSWORD | |
79bc14cf PG |
942 | # again, ignoring the crypto errors yields a bad zlib stream causing the |
943 | # decompressor to abort where the hole begins; the file is extracted up | |
944 | # to this point though | |
945 | FAILURES = 1 | |
2fe5f6e7 | 946 | |
0c8baf2b | 947 | |
afb2d647 | 948 | class RescueCorruptHeaderCTSizeGZAESTest (RescueTest): |
0c8baf2b PG |
949 | COMPRESSION = "#gz" |
950 | PASSWORD = TEST_PASSWORD | |
951 | FAILURES = 0 | |
952 | CORRUPT = corrupt_ctsize | |
953 | MISMATCHES = 0 | |
954 | ||
955 | ||
b9cf4a0f | 956 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
afb2d647 PG |
957 | class RescueCorruptLeadingGarbageTestBase (RescueTest): |
958 | # plain Tar is indifferent against traling data and the results | |
959 | # are consistent | |
960 | COMPRESSION = None | |
961 | PASSWORD = None | |
962 | FAILURES = 0 | |
963 | CORRUPT = corrupt_leading_garbage | |
964 | MISMATCHES = 0 | |
965 | ||
b9cf4a0f | 966 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
afb2d647 PG |
967 | class RescueCorruptLeadingGarbageSingleTest (RescueCorruptLeadingGarbageTestBase): |
968 | VOLUMES = 1 | |
969 | ||
b9cf4a0f | 970 | @unittest.skipIf(sys.version_info < (3, 4), "requires recent os library") |
afb2d647 PG |
971 | class RescueCorruptLeadingGarbageMultiTest (RescueCorruptLeadingGarbageTestBase): |
972 | # the last object in first archive has extra bytes somewhere in the | |
973 | # middle because tar itself performs no data checksumming. | |
974 | MISMATCHES = 2 | |
975 | VOLUMES = 3 | |
976 | ||
977 | ||
2fe5f6e7 PG |
978 | ############################################################################### |
979 | # index | |
980 | ############################################################################### | |
981 | ||
982 | class GenIndexIntactBaseTest (GenIndexTest): | |
983 | """ | |
984 | """ | |
985 | COMPRESSION = None | |
986 | PASSWORD = None | |
987 | FAILURES = 0 | |
988 | CORRUPT = immaculate | |
989 | VOLUMES = 1 | |
990 | MISMATCHES = 1 | |
991 | ||
047239f3 PG |
992 | class GenIndexIntactSingleTest (GenIndexIntactBaseTest): |
993 | pass | |
994 | ||
995 | class GenIndexIntactSingleGZTest (GenIndexIntactBaseTest): | |
996 | COMPRESSION = "#gz" | |
997 | MISSING = 2 | |
998 | ||
999 | class GenIndexIntactSingleGZAESTest (GenIndexIntactBaseTest): | |
1000 | COMPRESSION = "#gz" | |
1001 | PASSWORD = TEST_PASSWORD | |
1002 | MISSING = 2 | |
2fe5f6e7 | 1003 | |
047239f3 PG |
1004 | class GenIndexIntactMultiTest (GenIndexIntactBaseTest): |
1005 | VOLUMES = 3 | |
2fe5f6e7 PG |
1006 | pass |
1007 | ||
047239f3 PG |
1008 | class GenIndexIntactMultiGZTest (GenIndexIntactBaseTest): |
1009 | VOLUMES = 3 | |
2fe5f6e7 PG |
1010 | COMPRESSION = "#gz" |
1011 | MISSING = 2 | |
1012 | ||
047239f3 PG |
1013 | class GenIndexIntactMultiGZAESTest (GenIndexIntactBaseTest): |
1014 | VOLUMES = 3 | |
2fe5f6e7 PG |
1015 | COMPRESSION = "#gz" |
1016 | PASSWORD = TEST_PASSWORD | |
1017 | MISSING = 2 | |
1018 | ||
6e1f5355 | 1019 | |
37ccf5bc PG |
1020 | class GenIndexCorruptTruncateBaseTest (GenIndexTest): |
1021 | """ | |
1022 | Recreate index from file that lacks the latter portion. | |
1023 | """ | |
1024 | COMPRESSION = None | |
1025 | PASSWORD = None | |
1026 | FAILURES = 0 | |
1027 | CORRUPT = corrupt_truncate | |
1028 | MISSING = 2 | |
1029 | ||
1030 | class GenIndexCorruptTruncateTest (GenIndexCorruptTruncateBaseTest): | |
1031 | pass | |
1032 | ||
1033 | class GenIndexCorruptTruncateGZTest (GenIndexCorruptTruncateBaseTest): | |
1034 | COMPRESSION = "#gz" | |
1035 | ||
1036 | class GenIndexCorruptTruncateGZAESTest (GenIndexCorruptTruncateBaseTest): | |
1037 | COMPRESSION = "#gz" | |
1038 | PASSWORD = TEST_PASSWORD | |
1039 | ||
1040 | ||
6e1f5355 PG |
1041 | class GenIndexCorruptHoleBaseTest (GenIndexTest): |
1042 | """ | |
1043 | Recreate index from file with hole. | |
1044 | """ | |
1045 | COMPRESSION = None | |
1046 | PASSWORD = None | |
1047 | FAILURES = 0 | |
1048 | CORRUPT = corrupt_hole | |
1049 | VOLUMES = 1 | |
1050 | MISMATCHES = 1 | |
1051 | ||
1052 | class GenIndexCorruptHoleTest (GenIndexCorruptHoleBaseTest): | |
1053 | pass | |
1054 | ||
1055 | class GenIndexCorruptHoleGZTest (GenIndexCorruptHoleBaseTest): | |
1056 | COMPRESSION = "#gz" | |
1057 | MISSING = 2 | |
1058 | ||
1059 | class GenIndexCorruptHoleGZAESTest (GenIndexCorruptHoleBaseTest): | |
1060 | COMPRESSION = "#gz" | |
1061 | PASSWORD = TEST_PASSWORD | |
1062 | MISSING = 2 | |
1063 | ||
1064 | ||
6e1f5355 PG |
1065 | class GenIndexCorruptEntireHeaderBaseTest (GenIndexTest): |
1066 | """ | |
1067 | Recreate index from file with hole. | |
1068 | """ | |
1069 | COMPRESSION = None | |
1070 | PASSWORD = None | |
1071 | FAILURES = 0 | |
1072 | CORRUPT = corrupt_entire_header | |
1073 | VOLUMES = 1 | |
1074 | MISMATCHES = 1 | |
1075 | ||
1076 | class GenIndexCorruptEntireHeaderTest (GenIndexCorruptEntireHeaderBaseTest): | |
1077 | pass | |
1078 | ||
1079 | class GenIndexCorruptEntireHeaderGZTest (GenIndexCorruptEntireHeaderBaseTest): | |
1080 | COMPRESSION = "#gz" | |
1081 | MISSING = 2 | |
1082 | ||
1083 | class GenIndexCorruptEntireHeaderGZAESTest (GenIndexCorruptEntireHeaderBaseTest): | |
1084 | COMPRESSION = "#gz" | |
1085 | PASSWORD = TEST_PASSWORD | |
1086 | MISSING = 2 | |
1087 |