use real new volume handler during rescue
[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
458         assert len (failed) == self.FAILURES
459
460         # with one file missing
461         missing  = []
462         mismatch = []
463         for key, value in self.hash.items ():
464             kkey = "%s/%s" % (self.dst_path, key)
465             if os.path.exists (kkey):
466                 if value != self.md5sum (kkey):
467                     mismatch.append (key)
468             else:
469                 missing.append (key)
470
471         assert len (missing)  == (self.MISSING if self.MISSING is not None
472                                                else self.FAILURES)
473         assert len (mismatch) == self.MISMATCHES
474
475         shutil.rmtree (self.dst_path)
476
477
478 class GenIndexTest (DefectiveTest):
479     """
480     Deducing an index for a backup with tarfile.
481     """
482
483     def test_gen_index (self):
484         """
485         Create backup, leave it unharmed, then generate an index.
486         """
487         mode = self.COMPRESSION or "#"
488         bak_path, backup_file, backup_full, index_file = \
489             self.gen_file_names (self.COMPRESSION, self.PASSWORD)
490
491         vname = partial (self.default_volume_name, backup_file)
492         dtar = deltatar.DeltaTar (mode=mode,
493                                   logger=None,
494                                   password=self.PASSWORD,
495                                   index_name_func=lambda _: index_file,
496                                   volume_name_func=vname)
497
498         dtar.create_full_backup \
499             (source_path=self.src_path, backup_path=bak_path,
500              max_volume_size=1)
501
502         psidx = tarfile.gen_rescue_index (backup_full, mode, password=self.PASSWORD)
503
504         assert len (psidx) == len (self.hash)
505
506
507 ###############################################################################
508 # rescue
509 ###############################################################################
510
511 class RecoverCorruptPayloadTestBase (RecoverTest):
512     COMPRESSION = None
513     PASSWORD    = None
514     FAILURES    = 0 # tarfile will restore but corrupted, as
515     MISMATCHES  = 1 # revealed by the hash
516
517 class RecoverCorruptPayloadSingleTest (RecoverCorruptPayloadTestBase):
518     VOLUMES     = 1
519
520 class RecoverCorruptPayloadMultiTest (RecoverCorruptPayloadTestBase):
521     VOLUMES     = 3
522
523
524 class RecoverCorruptPayloadGZTestBase (RecoverTest):
525     COMPRESSION = "#gz"
526     PASSWORD    = None
527     FAILURES    = 1
528     MISMATCHES  = 0
529
530 class RecoverCorruptPayloadGZSingleTest (RecoverCorruptPayloadGZTestBase):
531     VOLUMES     = 1
532
533 class RecoverCorruptPayloadGZMultiTest (RecoverCorruptPayloadGZTestBase):
534     VOLUMES     = 3
535
536
537 class RecoverCorruptPayloadGZAESTestBase (RecoverTest):
538     COMPRESSION = "#gz"
539     PASSWORD    = TEST_PASSWORD
540     FAILURES    = 1
541     MISMATCHES  = 0
542
543 class RecoverCorruptPayloadGZAESSingleTest (RecoverCorruptPayloadGZAESTestBase):
544     VOLUMES     = 1
545
546 class RecoverCorruptPayloadGZAESMultiTest (RecoverCorruptPayloadGZAESTestBase):
547     VOLUMES     = 3
548
549
550 class RecoverCorruptHeaderTestBase (RecoverTest):
551     COMPRESSION = None
552     PASSWORD    = None
553     FAILURES    = 1
554     CORRUPT     = corrupt_header
555     MISMATCHES  = 0
556
557 class RecoverCorruptHeaderSingleTest (RecoverCorruptHeaderTestBase):
558     VOLUMES     = 1
559
560 class RecoverCorruptHeaderMultiTest (RecoverCorruptHeaderTestBase):
561     VOLUMES     = 3
562
563
564 class RecoverCorruptHeaderGZTestBase (RecoverTest):
565     COMPRESSION = "#gz"
566     PASSWORD    = None
567     FAILURES    = 1
568     CORRUPT     = corrupt_header
569     MISMATCHES  = 0
570
571 class RecoverCorruptHeaderGZSingleTest (RecoverCorruptHeaderGZTestBase):
572     VOLUMES     = 1
573
574 class RecoverCorruptHeaderGZMultiTest (RecoverCorruptHeaderGZTestBase):
575     VOLUMES     = 3
576
577
578 class RecoverCorruptHeaderGZAESTestBase (RecoverTest):
579     COMPRESSION = "#gz"
580     PASSWORD    = TEST_PASSWORD
581     FAILURES    = 1
582     CORRUPT     = corrupt_header
583     MISMATCHES  = 0
584
585 class RecoverCorruptHeaderGZAESSingleTest (RecoverCorruptHeaderGZAESTestBase):
586     VOLUMES     = 1
587
588 class RecoverCorruptHeaderGZAESMultiTest (RecoverCorruptHeaderGZAESTestBase):
589     VOLUMES     = 3
590
591
592 class RecoverCorruptEntireHeaderTestBase (RecoverTest):
593     COMPRESSION = None
594     PASSWORD    = None
595     FAILURES    = 1
596     CORRUPT     = corrupt_entire_header
597     MISMATCHES  = 0
598
599 class RecoverCorruptEntireHeaderSingleTest (RecoverCorruptEntireHeaderTestBase):
600     VOLUMES     = 1
601
602 class RecoverCorruptEntireHeaderMultiTest (RecoverCorruptEntireHeaderTestBase):
603     VOLUMES     = 3
604
605
606 class RecoverCorruptEntireHeaderGZTestBase (RecoverTest):
607     COMPRESSION = "#gz"
608     PASSWORD    = None
609     FAILURES    = 1
610     CORRUPT     = corrupt_entire_header
611     MISMATCHES  = 0
612
613 class RecoverCorruptEntireHeaderGZSingleTest (RecoverCorruptEntireHeaderGZTestBase):
614     VOLUMES     = 1
615
616 class RecoverCorruptEntireHeaderGZMultiTest (RecoverCorruptEntireHeaderGZTestBase):
617     VOLUMES     = 3
618
619
620 class RecoverCorruptEntireHeaderGZAESTestBase (RecoverTest):
621     COMPRESSION = "#gz"
622     PASSWORD    = TEST_PASSWORD
623     FAILURES    = 1
624     CORRUPT     = corrupt_entire_header
625     MISMATCHES  = 0
626
627 class RecoverCorruptEntireHeaderGZAESSingleTest (RecoverCorruptEntireHeaderGZAESTestBase):
628     VOLUMES     = 1
629
630 class RecoverCorruptEntireHeaderGZAESMultiTest (RecoverCorruptEntireHeaderGZAESTestBase):
631     VOLUMES     = 3
632
633
634 class RecoverCorruptTrailingDataTestBase (RecoverTest):
635     # plain Tar is indifferent against traling data and the results
636     # are consistent
637     COMPRESSION = None
638     PASSWORD    = None
639     FAILURES    = 0
640     CORRUPT     = corrupt_trailing_data
641     MISMATCHES  = 0
642
643 class RecoverCorruptTrailingDataSingleTest (RecoverCorruptTrailingDataTestBase):
644     VOLUMES     = 1
645
646 class RecoverCorruptTrailingDataMultiTest (RecoverCorruptTrailingDataTestBase):
647     # the last object in first archive has extra bytes somewhere in the
648     # middle because tar itself performs no data checksumming.
649     MISMATCHES  = 1
650     VOLUMES     = 3
651
652
653 class RecoverCorruptTrailingDataGZTestBase (RecoverTest):
654     # reading past the final object will cause decompression failure;
655     # all objects except for the last survive unharmed though
656     COMPRESSION = "#gz"
657     PASSWORD    = None
658     FAILURES    = 1
659     CORRUPT     = corrupt_trailing_data
660     MISMATCHES  = 0
661
662 class RecoverCorruptTrailingDataGZSingleTest (RecoverCorruptTrailingDataGZTestBase):
663     VOLUMES     = 1
664
665 class RecoverCorruptTrailingDataGZMultiTest (RecoverCorruptTrailingDataGZTestBase):
666     VOLUMES     = 3
667     # the last file of the first volume will only contain the data of the
668     # second part which is contained in the second volume. this happens
669     # because the CRC32 is wrong for the first part so it gets discarded, then
670     # the object is recreated from the first header of the second volume,
671     # containing only the remainder of the data.
672     MISMATCHES  = 1
673     MISSING     = 0
674
675
676 class RecoverCorruptTrailingDataGZAESTestBase (RecoverTest):
677     COMPRESSION = "#gz"
678     PASSWORD    = TEST_PASSWORD
679     FAILURES    = 0
680     CORRUPT     = corrupt_trailing_data
681     MISMATCHES  = 0
682
683 class RecoverCorruptTrailingDataGZAESSingleTest (RecoverCorruptTrailingDataGZAESTestBase):
684     VOLUMES     = 1
685
686 class RecoverCorruptTrailingDataGZAESMultiTest (RecoverCorruptTrailingDataGZAESTestBase):
687     VOLUMES     = 3
688
689
690 class RecoverCorruptVolumeBaseTest (RecoverTest):
691     COMPRESSION = None
692     PASSWORD    = None
693     FAILURES    = 8
694     CORRUPT     = corrupt_volume
695     VOLUMES     = 3
696
697 class RecoverCorruptVolumeTest (RecoverCorruptVolumeBaseTest):
698     pass
699
700 class RecoverCorruptVolumeGZTest (RecoverCorruptVolumeBaseTest):
701     COMPRESSION = "#gz"
702
703 class RecoverCorruptVolumeGZAESTest (RecoverCorruptVolumeBaseTest):
704     COMPRESSION = "#gz"
705     PASSWORD    = TEST_PASSWORD
706
707
708 class RecoverCorruptHoleBaseTest (RecoverTest):
709     """
710     Cut bytes from the middle of a volume.
711
712     Index-based recovery works only up to the hole.
713     """
714     COMPRESSION = None
715     PASSWORD    = None
716     FAILURES    = 3
717     CORRUPT     = corrupt_hole
718     VOLUMES     = 2 # request two vols to swell up the first one
719     MISMATCHES  = 1
720
721 class RecoverCorruptHoleTest (RecoverCorruptHoleBaseTest):
722     FAILURES    = 2
723
724 class RecoverCorruptHoleGZTest (RecoverCorruptHoleBaseTest):
725     COMPRESSION = "#gz"
726     MISSING     = 2
727
728 class RecoverCorruptHoleGZAESTest (RecoverCorruptHoleBaseTest):
729     COMPRESSION = "#gz"
730     PASSWORD    = TEST_PASSWORD
731     MISSING     = 2
732
733 ###############################################################################
734 # rescue
735 ###############################################################################
736
737 class RescueCorruptHoleBaseTest (RescueTest):
738     """
739     Cut bytes from the middle of a volume.
740     """
741     COMPRESSION = None
742     PASSWORD    = None
743     FAILURES    = 3
744     CORRUPT     = corrupt_hole
745     VOLUMES     = 2 # request two vols to swell up the first one
746     MISMATCHES  = 1
747
748 class RescueCorruptHoleTest (RescueCorruptHoleBaseTest):
749     FAILURES    = 0
750     MISMATCHES  = 1 # intersected by hole
751     MISSING     = 1 # excised by hole
752
753 class RescueCorruptHoleGZTest (RescueCorruptHoleBaseTest):
754     COMPRESSION = "#gz"
755     MISSING     = 2
756
757 class RescueCorruptHoleGZAESTest (RescueCorruptHoleBaseTest):
758     COMPRESSION = "#gz"
759     PASSWORD    = TEST_PASSWORD
760     MISSING     = 2
761
762 ###############################################################################
763 # index
764 ###############################################################################
765
766 class GenIndexIntactBaseTest (GenIndexTest):
767     """
768     """
769     COMPRESSION = None
770     PASSWORD    = None
771     FAILURES    = 0
772     CORRUPT     = immaculate
773     VOLUMES     = 1
774     MISMATCHES  = 1
775
776
777 class GenIndexIntactTest (GenIndexIntactBaseTest):
778     pass
779
780 class GenIndexIntactGZTest (GenIndexIntactBaseTest):
781     COMPRESSION = "#gz"
782     MISSING     = 2
783
784 class GenIndexIntactGZAESTest (GenIndexIntactBaseTest):
785     COMPRESSION = "#gz"
786     PASSWORD    = TEST_PASSWORD
787     MISSING     = 2
788