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