cd48a00411a88acbfb253125fd9d05abdd0e2ce3
[python-delta-tar] / testing / test_recover.py
1 import logging
2 import os
3 import shutil
4 import stat
5
6 from functools import partial
7
8 import deltatar.deltatar as deltatar
9 import deltatar.crypto   as crypto
10 import deltatar.tarfile  as tarfile
11
12 from . import BaseTest
13
14 TEST_PASSWORD     = "test1234"
15 TEST_VOLSIZ       = 2 # MB
16 TEST_FILESPERVOL  = 3
17 VOLUME_OVERHEAD   = 1.4 # account for tar overhead when fitting files into
18                         # volumes; this is black magic
19 TEST_BLOCKSIZE    = 4096
20
21 ###############################################################################
22 ## helpers                                                                   ##
23 ###############################################################################
24
25 def flip_bits (fname, off, b=0x01, n=1):
26     """
27     Open file *fname* at offset *off*, replacing the next *n* bytes with
28     their values xor’ed with *b*.
29     """
30     fd = os.open (fname, os.O_RDWR)
31
32     try:
33         pos = os.lseek (fd, off, os.SEEK_SET)
34         assert pos == off
35         chunk = os.read (fd, n)
36         chunk = bytes (map (lambda v: v ^ b, chunk))
37         pos = os.lseek (fd, off, os.SEEK_SET)
38         assert pos == off
39         os.write (fd, chunk)
40     finally:
41         os.close (fd)
42
43
44 def gz_header_size (fname, off=0):
45     """
46     Determine the length of the gzip header starting at *off* in file fname.
47
48     The header is variable length because it may contain the filename as NUL
49     terminated bytes.
50     """
51     # length so we need to determine where the actual payload starts
52     off = tarfile.GZ_HEADER_SIZE
53     fd = os.open (fname, os.O_RDONLY)
54
55     try:
56         pos = os.lseek (fd, off, os.SEEK_SET)
57         assert pos == off
58         while os.read (fd, 1)[0] != 0:
59             off += 1
60             pos = os.lseek (fd, off, os.SEEK_SET)
61             assert pos == off
62     finally:
63         os.close (fd)
64
65     return off
66
67
68 def is_pdt_encrypted (fname):
69     """
70     Returns true if the file contains at least one PDT header plus enough
71     space for the object.
72     """
73     try:
74         with open (fname, "rb") as st:
75             hdr = crypto.hdr_read_stream (st)
76             siz = hdr ["ctsize"]
77             assert (len (st.read (siz)) == siz)
78     except Exception as exn:
79         return False
80     return True
81
82
83 ###############################################################################
84 ## corruption simulators                                                     ##
85 ###############################################################################
86
87 def corrupt_header (_, fname, compress, encrypt):
88     """
89     Modify a significant byte in the object header of the format.
90     """
91     if encrypt is True: # damage GCM tag
92         flip_bits (fname, crypto.HDR_OFF_TAG + 1)
93     elif compress is True: # invalidate magic
94         flip_bits (fname, 1)
95     else: # Fudge checksum. From tar(5):
96         #
97         #       struct header_gnu_tar {
98         #               char name[100];
99         #               char mode[8];
100         #               char uid[8];
101         #               char gid[8];
102         #               char size[12];
103         #               char mtime[12];
104         #               char checksum[8];
105         #               …
106         flip_bits (fname, 100 + 8 + 8 + 8 + 12 + 12 + 1)
107
108
109 def corrupt_entire_header (_, fname, compress, encrypt):
110     """
111     Flip all bits in the first object header.
112     """
113     if encrypt is True:
114         flip_bits (fname, 0, 0xff, crypto.PDTCRYPT_HDR_SIZE)
115     elif compress is True: # invalidate magic
116         flip_bits (fname, 0, 0xff, gz_header_size (fname))
117     else:
118         flip_bits (fname, 0, 0xff, tarfile.BLOCKSIZE)
119
120
121 def corrupt_payload_start (_, fname, compress, encrypt):
122     """
123     Modify the byte following the object header structure of the format.
124     """
125     if encrypt is True:
126         flip_bits (fname, crypto.PDTCRYPT_HDR_SIZE + 1)
127     elif compress is True:
128         flip_bits (fname, gz_header_size (fname) + 1)
129     else:
130         flip_bits (fname, tarfile.BLOCKSIZE + 1)
131
132
133 def corrupt_trailing_data (_, fname, compress, encrypt):
134     """
135     Modify the byte following the object header structure of the format.
136     """
137     junk = os.urandom (42)
138     fd = os.open (fname, os.O_WRONLY | os.O_APPEND)
139     os.write (fd, junk)
140     os.close (fd)
141
142
143 def corrupt_volume (_, fname, compress, encrypt):
144     """
145     Zero out an entire volume.
146     """
147     fd = os.open (fname, os.O_WRONLY)
148     size = os.lseek (fd, 0, os.SEEK_END)
149     assert os.lseek (fd, 0, os.SEEK_SET) == 0
150     zeros = bytes (b'\x00' * TEST_BLOCKSIZE)
151     while size > 0:
152         todo = min (size, TEST_BLOCKSIZE)
153         os.write (fd, zeros [:todo])
154         size -= todo
155     os.close (fd)
156
157
158 def corrupt_hole (_, fname, compress, encrypt):
159     """
160     Cut file in three pieces, reassemble without the middle one.
161     """
162     aname = os.path.abspath (fname)
163     infd = os.open (fname, os.O_RDONLY)
164     size = os.lseek (infd, 0, os.SEEK_END)
165     assert os.lseek (infd, 0, os.SEEK_SET) == 0
166     assert size > 3 * TEST_BLOCKSIZE
167     hole = (size / 3, size * 2 / 3)
168     outfd = os.open (os.path.dirname (aname), os.O_WRONLY | os.O_TMPFILE,
169                      stat.S_IRUSR | stat.S_IWUSR)
170     
171     zeros = bytes (b'\x00' * TEST_BLOCKSIZE)
172     done = 0
173     while done < size:
174         data = os.read (infd, TEST_BLOCKSIZE)
175         if done < hole [0] or hole [1] < done:
176             # only copy from outside hole
177             os.write (outfd, data)
178         done += len (data)
179
180     os.close (infd)
181     os.unlink (fname)
182
183     path = "/proc/self/fd/%d" % outfd
184     os.link (path, aname, src_dir_fd=0, follow_symlinks=True)
185     os.close (outfd)
186
187 def immaculate (_, _fname, _compress, _encrypt):
188     """
189     No-op dummy.
190     """
191     pass
192
193 ###############################################################################
194 ## tests                                                                     ##
195 ###############################################################################
196
197 class DefectiveTest (BaseTest):
198     """
199     Disaster recovery: restore corrupt backups.
200     """
201
202     COMPRESSION = None
203     PASSWORD    = None
204     FAILURES    = 0     # files that could not be restored
205     MISMATCHES  = 0     # files that were restored but corrupted
206     CORRUPT     = corrupt_payload_start
207     VOLUMES     = 1
208     MISSING     = None  # normally the number of failures
209
210
211     def setUp(self):
212         '''
213         Create base test data
214         '''
215         self.pwd      = os.getcwd()
216         self.dst_path = "source_dir"
217         self.src_path = "%s2" % self.dst_path
218         self.hash     = dict()
219
220         os.system('rm -rf target_dir source_dir* backup_dir* huge')
221         os.makedirs (self.src_path)
222
223         for i in range (5):
224             f = "dummy_%d" % i
225             self.hash [f] = self.create_file ("%s/%s"
226                                               % (self.src_path, f), 5 + i)
227
228
229     def tearDown(self):
230         '''
231         Remove temporal files created by unit tests and reset globals.
232         '''
233         os.chdir(self.pwd)
234         os.system("rm -rf source_dir source_dir2 backup_dir*")
235
236
237     @staticmethod
238     def default_volume_name (backup_file, _x, _y, n, *a, **kwa):
239         return backup_file % n
240
241     def gen_file_names (self, comp, pw):
242         bak_path       = "backup_dir"
243         backup_file    = "the_full_backup_%0.2d.tar"
244         backup_full    = ("%s/%s" % (bak_path, backup_file)) % 0
245         index_file     = "the_full_index"
246
247         if self.COMPRESSION is not None:
248             backup_file += ".gz"
249             backup_full += ".gz"
250             index_file  += ".gz"
251
252         if self.PASSWORD is not None:
253             backup_file = "%s.%s" % (backup_file, deltatar.PDTCRYPT_EXTENSION)
254             backup_full = "%s.%s" % (backup_full, deltatar.PDTCRYPT_EXTENSION)
255             index_file  = "%s.%s" % (index_file , deltatar.PDTCRYPT_EXTENSION)
256
257         return bak_path, backup_file, backup_full, index_file
258
259
260 class RecoverTest (DefectiveTest):
261     """
262     Recover: restore corrupt backups from index file information.
263     """
264
265     def test_recover_corrupt (self):
266         """
267         Perform various damaging actions that cause unreadable objects.
268
269         Expects the extraction to fail in normal mode. With disaster recovery,
270         extraction must succeed, and exactly one file must be missing.
271         """
272         mode = self.COMPRESSION or "#"
273         bak_path, backup_file, backup_full, index_file = \
274             self.gen_file_names (self.COMPRESSION, self.PASSWORD)
275
276         if self.VOLUMES > 1:
277             # add n files for one nth the volume size each, corrected
278             # for metadata and tar block overhead
279             fsiz = int (  (  TEST_VOLSIZ
280                            / (TEST_FILESPERVOL * VOLUME_OVERHEAD))
281                         * 1024 * 1024)
282             fcnt = (self.VOLUMES - 1) * TEST_FILESPERVOL
283             for i in range (fcnt):
284                 nvol, invol = divmod(i, TEST_FILESPERVOL)
285                 f = "dummy_vol_%d_n_%0.2d" % (nvol, invol)
286                 self.hash [f] = self.create_file ("%s/%s"
287                                                   % (self.src_path, f),
288                                                   fsiz,
289                                                   random=True)
290
291         vname = partial (self.default_volume_name, backup_file)
292         dtar = deltatar.DeltaTar (mode=mode,
293                                   logger=None,
294                                   password=self.PASSWORD,
295                                   index_name_func=lambda _: index_file,
296                                   volume_name_func=vname)
297
298         dtar.create_full_backup \
299             (source_path=self.src_path, backup_path=bak_path,
300              max_volume_size=1)
301
302         if self.PASSWORD is not None:
303             # ensure all files are at least superficially in PDT format
304             for f in os.listdir (bak_path):
305                 assert is_pdt_encrypted ("%s/%s" % (bak_path, f))
306
307         # first restore must succeed
308         dtar.restore_backup(target_path=self.dst_path,
309                             backup_indexes_paths=[
310                                 "%s/%s" % (bak_path, index_file)
311                             ])
312         for key, value in self.hash.items ():
313             f = "%s/%s" % (self.dst_path, key)
314             assert os.path.exists (f)
315             assert value == self.md5sum (f)
316         shutil.rmtree (self.dst_path)
317         shutil.rmtree (self.src_path)
318
319         self.CORRUPT (backup_full,
320                       self.COMPRESSION is not None,
321                       self.PASSWORD    is not None)
322
323         # normal restore must fail
324         try:
325             dtar.restore_backup(target_path=self.dst_path,
326                                 backup_tar_path=backup_full)
327         except tarfile.CompressionError:
328             if self.PASSWORD is not None or self.COMPRESSION is not None:
329                 pass
330             else:
331                 raise
332         except tarfile.ReadError:
333             # can happen with all three modes
334             pass
335         except tarfile.DecryptionError:
336             if self.PASSWORD is not None:
337                 pass
338             else:
339                 raise
340
341         os.chdir (self.pwd) # not restored due to the error above
342         # but recover will succeed
343         failed = dtar.recover_backup(target_path=self.dst_path,
344                                      backup_indexes_paths=[
345                                          "%s/%s" % (bak_path, index_file)
346                                      ])
347
348         assert len (failed) == self.FAILURES
349
350         # with one file missing
351         missing  = []
352         mismatch = []
353         for key, value in self.hash.items ():
354             kkey = "%s/%s" % (self.dst_path, key)
355             if os.path.exists (kkey):
356                 if value != self.md5sum (kkey):
357                     mismatch.append (key)
358             else:
359                 missing.append (key)
360
361         # usually, an object whose extraction fails will not be found on
362         # disk afterwards so the number of failures equals that of missing
363         # files. however, some modes will create partial files for objects
364         # spanning multiple volumes that contain the parts whose checksums
365         # were valid.
366         assert len (missing)  == (self.MISSING if self.MISSING is not None
367                                                else self.FAILURES)
368         assert len (mismatch) == self.MISMATCHES
369
370         shutil.rmtree (self.dst_path)
371
372
373 class RescueTest (DefectiveTest):
374     """
375     Rescue: restore corrupt backups from backup set that is damaged to a degree
376     that the index file is worthless.
377     """
378
379     def test_rescue_corrupt (self):
380         """
381         Perform various damaging actions that cause unreadable objects, then
382         attempt to extract objects regardless.
383         """
384         mode = self.COMPRESSION or "#"
385         bak_path, backup_file, backup_full, index_file = \
386             self.gen_file_names (self.COMPRESSION, self.PASSWORD)
387
388         if self.VOLUMES > 1:
389             # add n files for one nth the volume size each, corrected
390             # for metadata and tar block overhead
391             fsiz = int (  (  TEST_VOLSIZ
392                            / (TEST_FILESPERVOL * VOLUME_OVERHEAD))
393                         * 1024 * 1024)
394             fcnt = (self.VOLUMES - 1) * TEST_FILESPERVOL
395             for i in range (fcnt):
396                 nvol, invol = divmod(i, TEST_FILESPERVOL)
397                 f = "dummy_vol_%d_n_%0.2d" % (nvol, invol)
398                 self.hash [f] = self.create_file ("%s/%s"
399                                                   % (self.src_path, f),
400                                                   fsiz,
401                                                   random=True)
402
403         vname = partial (self.default_volume_name, backup_file)
404         dtar = deltatar.DeltaTar (mode=mode,
405                                   logger=None,
406                                   password=self.PASSWORD,
407                                   index_name_func=lambda _: index_file,
408                                   volume_name_func=vname)
409
410         dtar.create_full_backup \
411             (source_path=self.src_path, backup_path=bak_path,
412              max_volume_size=1)
413
414         if self.PASSWORD is not None:
415             # ensure all files are at least superficially in PDT format
416             for f in os.listdir (bak_path):
417                 assert is_pdt_encrypted ("%s/%s" % (bak_path, f))
418
419         # first restore must succeed
420         dtar.restore_backup(target_path=self.dst_path,
421                             backup_indexes_paths=[
422                                 "%s/%s" % (bak_path, index_file)
423                             ])
424         for key, value in self.hash.items ():
425             f = "%s/%s" % (self.dst_path, key)
426             assert os.path.exists (f)
427             assert value == self.md5sum (f)
428         shutil.rmtree (self.dst_path)
429         shutil.rmtree (self.src_path)
430
431         self.CORRUPT (backup_full,
432                       self.COMPRESSION is not None,
433                       self.PASSWORD    is not None)
434
435         # normal restore must fail
436         try:
437             dtar.restore_backup(target_path=self.dst_path,
438                                 backup_tar_path=backup_full)
439         except tarfile.CompressionError:
440             if self.PASSWORD is not None or self.COMPRESSION is not None:
441                 pass
442             else:
443                 raise
444         except tarfile.ReadError:
445             # can happen with all three modes
446             pass
447         except tarfile.DecryptionError:
448             if self.PASSWORD is not None:
449                 pass
450             else:
451                 raise
452
453         os.chdir (self.pwd) # not restored due to the error above
454         # but recover will succeed
455         failed = dtar.rescue_backup(target_path=self.dst_path,
456                                     backup_tar_path=backup_full)
457         # with one file missing
458         missing  = []
459         mismatch = []
460         for key, value in self.hash.items ():
461             kkey = "%s/%s" % (self.dst_path, key)
462             if os.path.exists (kkey):
463                 if value != self.md5sum (kkey):
464                     mismatch.append (key)
465             else:
466                 missing.append (key)
467
468         assert len (failed)   == self.FAILURES
469         assert len (missing)  == (self.MISSING if self.MISSING is not None
470                                                else self.FAILURES)
471         assert len (mismatch) == self.MISMATCHES
472
473         shutil.rmtree (self.dst_path)
474
475
476 class GenIndexTest (DefectiveTest):
477     """
478     Deducing an index for a backup with tarfile.
479     """
480
481     def test_gen_index (self):
482         """
483         Create backup, leave it unharmed, then generate an index.
484         """
485         mode = self.COMPRESSION or "#"
486         bak_path, backup_file, backup_full, index_file = \
487             self.gen_file_names (self.COMPRESSION, self.PASSWORD)
488
489         vname = partial (self.default_volume_name, backup_file)
490         dtar = deltatar.DeltaTar (mode=mode,
491                                   logger=None,
492                                   password=self.PASSWORD,
493                                   index_name_func=lambda _: index_file,
494                                   volume_name_func=vname)
495
496         dtar.create_full_backup \
497             (source_path=self.src_path, backup_path=bak_path,
498              max_volume_size=1)
499
500         psidx = tarfile.gen_rescue_index (backup_full, mode, password=self.PASSWORD)
501
502         assert len (psidx) == len (self.hash)
503
504
505 ###############################################################################
506 # rescue
507 ###############################################################################
508
509 class RecoverCorruptPayloadTestBase (RecoverTest):
510     COMPRESSION = None
511     PASSWORD    = None
512     FAILURES    = 0 # tarfile will restore but corrupted, as
513     MISMATCHES  = 1 # revealed by the hash
514
515 class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase):
516     VOLUMES     = 1
517
518 class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase):
519     VOLUMES     = 3
520
521
522 class RecoverCorruptPayloadGZTestBase (RecoverTest):
523     COMPRESSION = "#gz"
524     PASSWORD    = None
525     FAILURES    = 1
526     MISMATCHES  = 0
527
528 class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase):
529     VOLUMES     = 1
530
531 class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase):
532     VOLUMES     = 3
533
534
535 class RecoverCorruptPayloadGZAESTestBase (RecoverTest):
536     COMPRESSION = "#gz"
537     PASSWORD    = TEST_PASSWORD
538     FAILURES    = 1
539     MISMATCHES  = 0
540
541 class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase):
542     VOLUMES     = 1
543
544 class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase):
545     VOLUMES     = 3
546
547
548 class RecoverCorruptHeaderTestBase (RecoverTest):
549     COMPRESSION = None
550     PASSWORD    = None
551     FAILURES    = 1
552     CORRUPT     = corrupt_header
553     MISMATCHES  = 0
554
555 class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase):
556     VOLUMES     = 1
557
558 class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase):
559     VOLUMES     = 3
560
561
562 class RecoverCorruptHeaderGZTestBase (RecoverTest):
563     COMPRESSION = "#gz"
564     PASSWORD    = None
565     FAILURES    = 1
566     CORRUPT     = corrupt_header
567     MISMATCHES  = 0
568
569 class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase):
570     VOLUMES     = 1
571
572 class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase):
573     VOLUMES     = 3
574
575
576 class RecoverCorruptHeaderGZAESTestBase (RecoverTest):
577     COMPRESSION = "#gz"
578     PASSWORD    = TEST_PASSWORD
579     FAILURES    = 1
580     CORRUPT     = corrupt_header
581     MISMATCHES  = 0
582
583 class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase):
584     VOLUMES     = 1
585
586 class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase):
587     VOLUMES     = 3
588
589
590 class RecoverCorruptEntireHeaderTestBase (RecoverTest):
591     COMPRESSION = None
592     PASSWORD    = None
593     FAILURES    = 1
594     CORRUPT     = corrupt_entire_header
595     MISMATCHES  = 0
596
597 class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase):
598     VOLUMES     = 1
599
600 class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase):
601     VOLUMES     = 3
602
603
604 class RecoverCorruptEntireHeaderGZTestBase (RecoverTest):
605     COMPRESSION = "#gz"
606     PASSWORD    = None
607     FAILURES    = 1
608     CORRUPT     = corrupt_entire_header
609     MISMATCHES  = 0
610
611 class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase):
612     VOLUMES     = 1
613
614 class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase):
615     VOLUMES     = 3
616
617
618 class RecoverCorruptEntireHeaderGZAESTestBase (RecoverTest):
619     COMPRESSION = "#gz"
620     PASSWORD    = TEST_PASSWORD
621     FAILURES    = 1
622     CORRUPT     = corrupt_entire_header
623     MISMATCHES  = 0
624
625 class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase):
626     VOLUMES     = 1
627
628 class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase):
629     VOLUMES     = 3
630
631
632 class RecoverCorruptTrailingDataTestBase (RecoverTest):
633     # plain Tar is indifferent against traling data and the results
634     # are consistent
635     COMPRESSION = None
636     PASSWORD    = None
637     FAILURES    = 0
638     CORRUPT     = corrupt_trailing_data
639     MISMATCHES  = 0
640
641 class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase):
642     VOLUMES     = 1
643
644 class RecoverCorruptTrailingDataMultiTest (RecoverCorruptTrailingDataTestBase):
645     # the last object in first archive has extra bytes somewhere in the
646     # middle because tar itself performs no data checksumming.
647     MISMATCHES  = 1
648     VOLUMES     = 3
649
650
651 class RecoverCorruptTrailingDataGZTestBase (RecoverTest):
652     # reading past the final object will cause decompression failure;
653     # all objects except for the last survive unharmed though
654     COMPRESSION = "#gz"
655     PASSWORD    = None
656     FAILURES    = 1
657     CORRUPT     = corrupt_trailing_data
658     MISMATCHES  = 0
659
660 class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase):
661     VOLUMES     = 1
662
663 class RecoverCorruptTrailingDataGZMultiTest (RecoverCorruptTrailingDataGZTestBase):
664     VOLUMES     = 3
665     # the last file of the first volume will only contain the data of the
666     # second part which is contained in the second volume. this happens
667     # because the CRC32 is wrong for the first part so it gets discarded, then
668     # the object is recreated from the first header of the second volume,
669     # containing only the remainder of the data.
670     MISMATCHES  = 1
671     MISSING     = 0
672
673
674 class RecoverCorruptTrailingDataGZAESTestBase (RecoverTest):
675     COMPRESSION = "#gz"
676     PASSWORD    = TEST_PASSWORD
677     FAILURES    = 0
678     CORRUPT     = corrupt_trailing_data
679     MISMATCHES  = 0
680
681 class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase):
682     VOLUMES     = 1
683
684 class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase):
685     VOLUMES     = 3
686
687
688 class RecoverCorruptVolumeBaseTest (RecoverTest):
689     COMPRESSION = None
690     PASSWORD    = None
691     FAILURES    = 8
692     CORRUPT     = corrupt_volume
693     VOLUMES     = 3
694
695 class RecoverCorruptVolumeTest (RecoverCorruptVolumeBaseTest):
696     pass
697
698 class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest):
699     COMPRESSION = "#gz"
700
701 class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest):
702     COMPRESSION = "#gz"
703     PASSWORD    = TEST_PASSWORD
704
705
706 class RecoverCorruptHoleBaseTest (RecoverTest):
707     """
708     Cut bytes from the middle of a volume.
709
710     Index-based recovery works only up to the hole.
711     """
712     COMPRESSION = None
713     PASSWORD    = None
714     FAILURES    = 3
715     CORRUPT     = corrupt_hole
716     VOLUMES     = 2 # request two vols to swell up the first one
717     MISMATCHES  = 1
718
719 class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest):
720     FAILURES    = 2
721
722 class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest):
723     COMPRESSION = "#gz"
724     MISSING     = 2
725
726 class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest):
727     COMPRESSION = "#gz"
728     PASSWORD    = TEST_PASSWORD
729     MISSING     = 2
730
731 ###############################################################################
732 # rescue
733 ###############################################################################
734
735 class RescueCorruptHoleBaseTest (RescueTest):
736     """
737     Cut bytes from the middle of a volume.
738     """
739     COMPRESSION = None
740     PASSWORD    = None
741     FAILURES    = 0
742     CORRUPT     = corrupt_hole
743     VOLUMES     = 2 # request two vols to swell up the first one
744     MISMATCHES  = 2 # intersected by hole
745     MISSING     = 1 # excised by hole
746
747 class RescueCorruptHoleTest (RescueCorruptHoleBaseTest):
748     pass
749
750 class RescueCorruptHoleGZTest (RescueCorruptHoleBaseTest):
751     COMPRESSION = "#gz"
752     # the decompressor explodes in our face processing the first dummy, nothing
753     # we can do to recover
754     FAILURES    = 1
755
756 class RescueCorruptHoleGZAESTest (RescueCorruptHoleBaseTest):
757     COMPRESSION = "#gz"
758     PASSWORD    = TEST_PASSWORD
759     # again, ignoring the crypto errors yields a bad zlib stream causing the
760     # decompressor to abort where the hole begins; the file is extracted up
761     # to this point though
762     FAILURES    = 1
763
764 ###############################################################################
765 # index
766 ###############################################################################
767
768 class GenIndexIntactBaseTest (GenIndexTest):
769     """
770     """
771     COMPRESSION = None
772     PASSWORD    = None
773     FAILURES    = 0
774     CORRUPT     = immaculate
775     VOLUMES     = 1
776     MISMATCHES  = 1
777
778
779 class GenIndexIntactTest (GenIndexIntactBaseTest):
780     pass
781
782 class GenIndexIntactGZTest (GenIndexIntactBaseTest):
783     COMPRESSION = "#gz"
784     MISSING     = 2
785
786 class GenIndexIntactGZAESTest (GenIndexIntactBaseTest):
787     COMPRESSION = "#gz"
788     PASSWORD    = TEST_PASSWORD
789     MISSING     = 2
790