• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / ZODB / 11452156871

21 Oct 2024 09:32AM UTC coverage: 83.913% (+0.2%) from 83.745%
11452156871

Pull #403

github

Sebatyne
repozo: support incremental recover

Which allows to recover a zodb filestorage by only appending the missing
chunks from the latest recovered file, instead of always recovering from
zero.
Pull Request #403: Repozo incremental recover

2443 of 3552 branches covered (68.78%)

210 of 211 new or added lines in 2 files covered. (99.53%)

39 existing lines in 2 files now uncovered.

13463 of 16044 relevant lines covered (83.91%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.26
/src/ZODB/scripts/tests/test_repozo.py
1
##############################################################################
2
#
3
# Copyright (c) 2004-2009 Zope Foundation and Contributors.
4
# All Rights Reserved.
5
#
6
# This software is subject to the provisions of the Zope Public License,
7
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
8
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11
# FOR A PARTICULAR PURPOSE.
12
#
13
##############################################################################
14

15
import os
1✔
16
import sys
1✔
17
import unittest
1✔
18
from hashlib import md5
1✔
19
from io import BytesIO
1✔
20
from io import StringIO
1✔
21

22
import ZODB.tests.util  # layer used at class scope
1✔
23

24

25
_NOISY = os.environ.get('NOISY_REPOZO_TEST_OUTPUT')
1✔
26

27

28
def _write_file(name, bits, mode='wb'):
1✔
29
    with open(name, mode) as f:
1✔
30
        f.write(bits)
1✔
31
        f.flush()
1✔
32

33

34
def _read_file(name, mode='rb'):
1✔
35
    with open(name, mode) as f:
1✔
36
        return f.read()
1✔
37

38

39
class OurDB:
1✔
40

41
    _file_name = None
1✔
42

43
    def __init__(self, dir):
1✔
44
        import transaction
1✔
45
        from BTrees.OOBTree import OOBTree
1✔
46
        self.dir = dir
1✔
47
        self.getdb()
1✔
48
        conn = self.db.open()
1✔
49
        conn.root()['tree'] = OOBTree()
1✔
50
        transaction.commit()
1✔
51
        self.pos = self.db.storage._pos
1✔
52
        self.close()
1✔
53

54
    def getdb(self):
1✔
55
        from ZODB import DB
1✔
56
        from ZODB.FileStorage import FileStorage
1✔
57
        self._file_name = storage_filename = os.path.join(self.dir, 'Data.fs')
1✔
58
        storage = FileStorage(storage_filename)
1✔
59
        self.db = DB(storage)
1✔
60

61
    def gettree(self):
1✔
62
        self.getdb()
1✔
63
        conn = self.db.open()
1✔
64
        return conn.root()['tree']
1✔
65

66
    def pack(self):
1✔
67
        self.getdb()
1✔
68
        self.db.pack()
1✔
69

70
    def close(self):
1✔
71
        if self.db is not None:
1!
72
            self.db.close()
1✔
73
            self.db = None
1✔
74

75
    def mutate(self):
1✔
76
        # Make random mutations to the btree in the database.
77
        import random
1✔
78

79
        import transaction
1✔
80
        tree = self.gettree()
1✔
81
        for dummy in range(100):
1✔
82
            if random.random() < 0.6:
1✔
83
                tree[random.randrange(100000)] = random.randrange(100000)
1✔
84
            else:
85
                keys = tree.keys()
1✔
86
                if keys:
1!
87
                    del tree[keys[0]]
1✔
88
        transaction.commit()
1✔
89
        self.pos = self.db.storage._pos
1✔
90
        self.maxkey = self.db.storage._oid
1✔
91
        self.close()
1✔
92

93

94
class Test_parseargs(unittest.TestCase):
1✔
95

96
    def setUp(self):
1✔
97
        from ZODB.scripts import repozo
1✔
98
        self._old_verbosity = repozo.VERBOSE
1✔
99
        self._old_stderr = sys.stderr
1✔
100
        repozo.VERBOSE = False
1✔
101
        sys.stderr = StringIO()
1✔
102

103
    def tearDown(self):
1✔
104
        from ZODB.scripts import repozo
1✔
105
        sys.stderr = self._old_stderr
1✔
106
        repozo.VERBOSE = self._old_verbosity
1✔
107

108
    def test_short(self):
1✔
109
        from ZODB.scripts import repozo
1✔
110
        options = repozo.parseargs(['-v', '-V', '-r', '/tmp/nosuchdir'])
1✔
111
        self.assertTrue(repozo.VERBOSE)
1✔
112
        self.assertEqual(options.mode, repozo.VERIFY)
1✔
113
        self.assertEqual(options.repository, '/tmp/nosuchdir')
1✔
114

115
    def test_long(self):
1✔
116
        from ZODB.scripts import repozo
1✔
117
        options = repozo.parseargs(['--verbose', '--verify',
1✔
118
                                    '--repository=/tmp/nosuchdir'])
119
        self.assertTrue(repozo.VERBOSE)
1✔
120
        self.assertEqual(options.mode, repozo.VERIFY)
1✔
121
        self.assertEqual(options.repository, '/tmp/nosuchdir')
1✔
122

123
    def test_help(self):
1✔
124
        from ZODB.scripts import repozo
1✔
125

126
        # Note: can't mock sys.stdout in our setUp: if a test fails,
127
        # zope.testrunner will happily print the traceback and failure message
128
        # into our StringIO before running our tearDown.
129
        old_stdout = sys.stdout
1✔
130
        sys.stdout = StringIO()
1✔
131
        try:
1✔
132
            self.assertRaises(SystemExit, repozo.parseargs, ['--help'])
1✔
133
            self.assertIn('Usage:', sys.stdout.getvalue())
1✔
134
        finally:
135
            sys.stdout = old_stdout
1✔
136

137
    def test_bad_option(self):
1✔
138
        from ZODB.scripts import repozo
1✔
139
        self.assertRaises(SystemExit, repozo.parseargs,
1✔
140
                          ['--crash-please'])
141
        self.assertIn('option --crash-please not recognized',
1✔
142
                      sys.stderr.getvalue())
143

144
    def test_bad_argument(self):
1✔
145
        from ZODB.scripts import repozo
1✔
146
        self.assertRaises(SystemExit, repozo.parseargs,
1✔
147
                          ['crash', 'please'])
148
        self.assertIn('Invalid arguments: crash, please',
1✔
149
                      sys.stderr.getvalue())
150

151
    def test_mode_selection(self):
1✔
152
        from ZODB.scripts import repozo
1✔
153
        options = repozo.parseargs([
1✔
154
            '-B', '-f', '/tmp/Data.fs', '-r', '/tmp/nosuchdir'])
155
        self.assertEqual(options.mode, repozo.BACKUP)
1✔
156
        options = repozo.parseargs(['-R', '-r', '/tmp/nosuchdir'])
1✔
157
        self.assertEqual(options.mode, repozo.RECOVER)
1✔
158
        options = repozo.parseargs(['-V', '-r', '/tmp/nosuchdir'])
1✔
159
        self.assertEqual(options.mode, repozo.VERIFY)
1✔
160

161
    def test_mode_selection_is_mutually_exclusive(self):
1✔
162
        from ZODB.scripts import repozo
1✔
163
        self.assertRaises(SystemExit, repozo.parseargs, ['-B', '-R'])
1✔
164
        self.assertIn('-B, -R, and -V are mutually exclusive',
1✔
165
                      sys.stderr.getvalue())
166
        self.assertRaises(SystemExit, repozo.parseargs, ['-R', '-V'])
1✔
167
        self.assertRaises(SystemExit, repozo.parseargs, ['-V', '-B'])
1✔
168

169
    def test_mode_selection_required(self):
1✔
170
        from ZODB.scripts import repozo
1✔
171
        self.assertRaises(SystemExit, repozo.parseargs, [])
1✔
172
        self.assertIn('Either --backup, --recover or --verify is required',
1✔
173
                      sys.stderr.getvalue())
174

175
    def test_misc_flags(self):
1✔
176
        from ZODB.scripts import repozo
1✔
177
        options = repozo.parseargs([
1✔
178
            '-B', '-f', '/tmp/Data.fs', '-r', '/tmp/nosuchdir', '-F'])
179
        self.assertTrue(options.full)
1✔
180
        options = repozo.parseargs([
1✔
181
            '-B', '-f', '/tmp/Data.fs', '-r', '/tmp/nosuchdir', '-k'])
182
        self.assertTrue(options.killold)
1✔
183

184
    def test_repo_is_required(self):
1✔
185
        from ZODB.scripts import repozo
1✔
186
        self.assertRaises(SystemExit, repozo.parseargs, ['-B'])
1✔
187
        self.assertIn('--repository is required', sys.stderr.getvalue())
1✔
188

189
    def test_backup_ignored_args(self):
1✔
190
        from ZODB.scripts import repozo
1✔
191
        options = repozo.parseargs(['-B', '-r', '/tmp/nosuchdir', '-v',
1✔
192
                                    '-f', '/tmp/Data.fs',
193
                                    '-o', '/tmp/ignored.fs',
194
                                    '-D', '2011-12-13'])
195
        self.assertEqual(options.date, None)
1✔
196
        self.assertIn('--date option is ignored in backup mode',
1✔
197
                      sys.stderr.getvalue())
198
        self.assertEqual(options.output, None)
1✔
199
        self.assertIn('--output option is ignored in backup mode',
1✔
200
                      sys.stderr.getvalue())
201

202
    def test_backup_required_args(self):
1✔
203
        from ZODB.scripts import repozo
1✔
204
        self.assertRaises(SystemExit, repozo.parseargs,
1✔
205
                          ['-B', '-r', '/tmp/nosuchdir'])
206
        self.assertIn('--file is required', sys.stderr.getvalue())
1✔
207

208
    def test_recover_ignored_args(self):
1✔
209
        from ZODB.scripts import repozo
1✔
210
        options = repozo.parseargs(['-R', '-r', '/tmp/nosuchdir', '-v',
1✔
211
                                    '-f', '/tmp/ignored.fs',
212
                                    '-k'])
213
        self.assertEqual(options.file, None)
1✔
214
        self.assertIn('--file option is ignored in recover mode',
1✔
215
                      sys.stderr.getvalue())
216
        self.assertEqual(options.killold, False)
1✔
217
        self.assertIn('--kill-old-on-full option is ignored in recover mode',
1✔
218
                      sys.stderr.getvalue())
219

220
    def test_verify_ignored_args(self):
1✔
221
        from ZODB.scripts import repozo
1✔
222
        options = repozo.parseargs(['-V', '-r', '/tmp/nosuchdir', '-v',
1✔
223
                                    '-o', '/tmp/ignored.fs',
224
                                    '-D', '2011-12-13',
225
                                    '-f', '/tmp/ignored.fs',
226
                                    '-z', '-k', '-F'])
227
        self.assertEqual(options.date, None)
1✔
228
        self.assertIn('--date option is ignored in verify mode',
1✔
229
                      sys.stderr.getvalue())
230
        self.assertEqual(options.output, None)
1✔
231
        self.assertIn('--output option is ignored in verify mode',
1✔
232
                      sys.stderr.getvalue())
233
        self.assertEqual(options.full, False)
1✔
234
        self.assertIn('--full option is ignored in verify mode',
1✔
235
                      sys.stderr.getvalue())
236
        self.assertEqual(options.gzip, False)
1✔
237
        self.assertIn('--gzip option is ignored in verify mode',
1✔
238
                      sys.stderr.getvalue())
239
        self.assertEqual(options.file, None)
1✔
240
        self.assertIn('--file option is ignored in verify mode',
1✔
241
                      sys.stderr.getvalue())
242
        self.assertEqual(options.killold, False)
1✔
243
        self.assertIn('--kill-old-on-full option is ignored in verify mode',
1✔
244
                      sys.stderr.getvalue())
245

246

247
class FileopsBase:
1✔
248

249
    def _makeChunks(self):
1✔
250
        from ZODB.scripts.repozo import READCHUNK
1✔
251
        return [b'x' * READCHUNK, b'y' * READCHUNK, b'z']
1✔
252

253
    def _makeFile(self, text=None):
1✔
254
        if text is None:
1✔
255
            text = b''.join(self._makeChunks())
1✔
256
        return BytesIO(text)
1✔
257

258

259
class Test_dofile(unittest.TestCase, FileopsBase):
1✔
260

261
    def _callFUT(self, func, fp, n):
1✔
262
        from ZODB.scripts.repozo import dofile
1✔
263
        return dofile(func, fp, n)
1✔
264

265
    def test_empty_read_all(self):
1✔
266
        chunks = []
1✔
267
        file = self._makeFile(b'')
1✔
268
        bytes = self._callFUT(chunks.append, file, None)
1✔
269
        self.assertEqual(bytes, 0)
1✔
270
        self.assertEqual(chunks, [])
1✔
271

272
    def test_empty_read_count(self):
1✔
273
        chunks = []
1✔
274
        file = self._makeFile(b'')
1✔
275
        bytes = self._callFUT(chunks.append, file, 42)
1✔
276
        self.assertEqual(bytes, 0)
1✔
277
        self.assertEqual(chunks, [])
1✔
278

279
    def test_nonempty_read_all(self):
1✔
280
        chunks = []
1✔
281
        file = self._makeFile()
1✔
282
        bytes = self._callFUT(chunks.append, file, None)
1✔
283
        self.assertEqual(bytes, file.tell())
1✔
284
        self.assertEqual(chunks, self._makeChunks())
1✔
285

286
    def test_nonempty_read_count(self):
1✔
287
        chunks = []
1✔
288
        file = self._makeFile()
1✔
289
        bytes = self._callFUT(chunks.append, file, 42)
1✔
290
        self.assertEqual(bytes, 42)
1✔
291
        self.assertEqual(chunks, [b'x' * 42])
1✔
292

293

294
class Test_checksum(unittest.TestCase, FileopsBase):
1✔
295

296
    def _callFUT(self, fp, n):
1✔
297
        from ZODB.scripts.repozo import checksum
1✔
298
        return checksum(fp, n)
1✔
299

300
    def test_empty_read_all(self):
1✔
301
        file = self._makeFile(b'')
1✔
302
        sum = self._callFUT(file, None)
1✔
303
        self.assertEqual(sum, md5(b'').hexdigest())
1✔
304

305
    def test_empty_read_count(self):
1✔
306
        file = self._makeFile(b'')
1✔
307
        sum = self._callFUT(file, 42)
1✔
308
        self.assertEqual(sum, md5(b'').hexdigest())
1✔
309

310
    def test_nonempty_read_all(self):
1✔
311
        file = self._makeFile()
1✔
312
        sum = self._callFUT(file, None)
1✔
313
        self.assertEqual(sum, md5(b''.join(self._makeChunks())).hexdigest())
1✔
314

315
    def test_nonempty_read_count(self):
1✔
316
        file = self._makeFile()
1✔
317
        sum = self._callFUT(file, 42)
1✔
318
        self.assertEqual(sum, md5(b'x' * 42).hexdigest())
1✔
319

320

321
class OptionsTestBase:
1✔
322

323
    _repository_directory = None
1✔
324
    _data_directory = None
1✔
325

326
    def tearDown(self):
1✔
327
        if self._repository_directory is not None:
1✔
328
            from shutil import rmtree
1✔
329
            rmtree(self._repository_directory)
1✔
330
        if self._data_directory is not None:
1✔
331
            from shutil import rmtree
1✔
332
            rmtree(self._data_directory)
1✔
333

334
    def _makeOptions(self, **kw):
1✔
335
        import tempfile
1✔
336
        self._repository_directory = tempfile.mkdtemp(prefix='test-repozo-')
1✔
337

338
        class Options:
1✔
339
            repository = self._repository_directory
1✔
340
            date = None
1✔
341

342
            def __init__(self, **kw):
1✔
343
                self.__dict__.update(kw)
1✔
344
        return Options(**kw)
1✔
345

346

347
class Test_copyfile(OptionsTestBase, unittest.TestCase):
1✔
348

349
    def _callFUT(self, options, dest, start, n):
1✔
350
        from ZODB.scripts.repozo import copyfile
1✔
351
        return copyfile(options, dest, start, n)
1✔
352

353
    def test_no_gzip(self):
1✔
354
        options = self._makeOptions(gzip=False)
1✔
355
        source = options.file = os.path.join(self._repository_directory,
1✔
356
                                             'source.txt')
357
        _write_file(source, b'x' * 1000)
1✔
358
        target = os.path.join(self._repository_directory, 'target.txt')
1✔
359
        sum = self._callFUT(options, target, 0, 100)
1✔
360
        self.assertEqual(sum, md5(b'x' * 100).hexdigest())
1✔
361
        self.assertEqual(_read_file(target), b'x' * 100)
1✔
362

363
    def test_w_gzip(self):
1✔
364
        from ZODB.scripts.repozo import _GzipCloser
1✔
365
        options = self._makeOptions(gzip=True)
1✔
366
        source = options.file = os.path.join(self._repository_directory,
1✔
367
                                             'source.txt')
368
        _write_file(source, b'x' * 1000)
1✔
369
        target = os.path.join(self._repository_directory, 'target.txt')
1✔
370
        sum = self._callFUT(options, target, 0, 100)
1✔
371
        self.assertEqual(sum, md5(b'x' * 100).hexdigest())
1✔
372
        with _GzipCloser(target, 'rb') as f:
1✔
373
            self.assertEqual(f.read(), b'x' * 100)
1✔
374

375

376
class Test_concat(OptionsTestBase, unittest.TestCase):
1✔
377

378
    def _callFUT(self, files, ofp):
1✔
379
        from ZODB.scripts.repozo import concat
1✔
380
        return concat(files, ofp)
1✔
381

382
    def _makeFile(self, name, text, gzip_file=False):
1✔
383
        import tempfile
1✔
384

385
        from ZODB.scripts.repozo import _GzipCloser
1✔
386
        if self._repository_directory is None:
1✔
387
            self._repository_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
388
        fqn = os.path.join(self._repository_directory, name)
1✔
389
        if gzip_file:
1✔
390
            _opener = _GzipCloser
1✔
391
        else:
392
            _opener = open
1✔
393
        with _opener(fqn, 'wb') as f:
1✔
394
            f.write(text)
1✔
395
            f.flush()
1✔
396
        return fqn
1✔
397

398
    def test_empty_list_no_ofp(self):
1✔
399
        bytes, sum = self._callFUT([], None)
1✔
400
        self.assertEqual(bytes, 0)
1✔
401
        self.assertEqual(sum, md5(b'').hexdigest())
1✔
402

403
    def test_w_plain_files_no_ofp(self):
1✔
404
        files = [self._makeFile(x, x.encode(), False) for x in 'ABC']
1✔
405
        bytes, sum = self._callFUT(files, None)
1✔
406
        self.assertEqual(bytes, 3)
1✔
407
        self.assertEqual(sum, md5(b'ABC').hexdigest())
1✔
408

409
    def test_w_gzipped_files_no_ofp(self):
1✔
410
        files = [self._makeFile('%s.fsz' % x, x.encode(), True) for x in 'ABC']
1✔
411
        bytes, sum = self._callFUT(files, None)
1✔
412
        self.assertEqual(bytes, 3)
1✔
413
        self.assertEqual(sum, md5(b'ABC').hexdigest())
1✔
414

415
    def test_w_ofp(self):
1✔
416

417
        class Faux:
1✔
418
            _closed = False
1✔
419

420
            def __init__(self):
1✔
421
                self._written = []
1✔
422

423
            def write(self, data):
1✔
424
                self._written.append(data)
1✔
425

426
            def close(self):
1✔
UNCOV
427
                self._closed = True
×
428

429
        files = [self._makeFile(x, x.encode(), False) for x in 'ABC']
1✔
430
        ofp = Faux()
1✔
431
        bytes, sum = self._callFUT(files, ofp)
1✔
432
        self.assertEqual(ofp._written, [x.encode() for x in 'ABC'])
1✔
433
        self.assertFalse(ofp._closed)
1✔
434

435

436
_marker = object()
1✔
437

438

439
class Test_gen_filename(OptionsTestBase, unittest.TestCase):
1✔
440

441
    def _callFUT(self, options, ext=_marker):
1✔
442
        from ZODB.scripts.repozo import gen_filename
1✔
443
        if ext is _marker:
1✔
444
            return gen_filename(options)
1✔
445
        return gen_filename(options, ext)
1✔
446

447
    def test_explicit_ext(self):
1✔
448
        options = self._makeOptions(test_now=(2010, 5, 14, 12, 52, 31))
1✔
449
        fn = self._callFUT(options, '.txt')
1✔
450
        self.assertEqual(fn, '2010-05-14-12-52-31.txt')
1✔
451

452
    def test_full_no_gzip(self):
1✔
453
        options = self._makeOptions(test_now=(2010, 5, 14, 12, 52, 31),
1✔
454
                                    full=True,
455
                                    gzip=False,
456
                                    )
457
        fn = self._callFUT(options)
1✔
458
        self.assertEqual(fn, '2010-05-14-12-52-31.fs')
1✔
459

460
    def test_full_w_gzip(self):
1✔
461
        options = self._makeOptions(test_now=(2010, 5, 14, 12, 52, 31),
1✔
462
                                    full=True,
463
                                    gzip=True,
464
                                    )
465
        fn = self._callFUT(options)
1✔
466
        self.assertEqual(fn, '2010-05-14-12-52-31.fsz')
1✔
467

468
    def test_incr_no_gzip(self):
1✔
469
        options = self._makeOptions(test_now=(2010, 5, 14, 12, 52, 31),
1✔
470
                                    full=False,
471
                                    gzip=False,
472
                                    )
473
        fn = self._callFUT(options)
1✔
474
        self.assertEqual(fn, '2010-05-14-12-52-31.deltafs')
1✔
475

476
    def test_incr_w_gzip(self):
1✔
477
        options = self._makeOptions(test_now=(2010, 5, 14, 12, 52, 31),
1✔
478
                                    full=False,
479
                                    gzip=True,
480
                                    )
481
        fn = self._callFUT(options)
1✔
482
        self.assertEqual(fn, '2010-05-14-12-52-31.deltafsz')
1✔
483

484

485
class Test_find_files(OptionsTestBase, unittest.TestCase):
1✔
486

487
    def _callFUT(self, options):
1✔
488
        from ZODB.scripts.repozo import find_files
1✔
489
        return find_files(options)
1✔
490

491
    def _makeFile(self, hour, min, sec, ext):
1✔
492
        # call _makeOptions first!
493
        name = '2010-05-14-%02d-%02d-%02d%s' % (hour, min, sec, ext)
1✔
494
        fqn = os.path.join(self._repository_directory, name)
1✔
495
        _write_file(fqn, name.encode())
1✔
496
        return fqn
1✔
497

498
    def test_no_files(self):
1✔
499
        options = self._makeOptions(date='2010-05-14-13-30-57')
1✔
500
        found = self._callFUT(options)
1✔
501
        self.assertEqual(found, [])
1✔
502

503
    def test_explicit_date(self):
1✔
504
        options = self._makeOptions(date='2010-05-14-13-30-57')
1✔
505
        files = []
1✔
506
        for h, m, s, e in [(2, 13, 14, '.fs'),
1✔
507
                           (2, 13, 14, '.dat'),
508
                           (3, 14, 15, '.deltafs'),
509
                           (4, 14, 15, '.deltafs'),
510
                           (5, 14, 15, '.deltafs'),
511
                           (12, 13, 14, '.fs'),
512
                           (12, 13, 14, '.dat'),
513
                           (13, 14, 15, '.deltafs'),
514
                           (14, 15, 16, '.deltafs'),
515
                           ]:
516
            files.append(self._makeFile(h, m, s, e))
1✔
517
        found = self._callFUT(options)
1✔
518
        # Older files, .dat file not included
519
        self.assertEqual(found, [files[5], files[7]])
1✔
520

521
    def test_using_gen_filename(self):
1✔
522
        options = self._makeOptions(date=None,
1✔
523
                                    test_now=(2010, 5, 14, 13, 30, 57))
524
        files = []
1✔
525
        for h, m, s, e in [(2, 13, 14, '.fs'),
1✔
526
                           (2, 13, 14, '.dat'),
527
                           (3, 14, 15, '.deltafs'),
528
                           (4, 14, 15, '.deltafs'),
529
                           (5, 14, 15, '.deltafs'),
530
                           (12, 13, 14, '.fs'),
531
                           (12, 13, 14, '.dat'),
532
                           (13, 14, 15, '.deltafs'),
533
                           (14, 15, 16, '.deltafs'),
534
                           ]:
535
            files.append(self._makeFile(h, m, s, e))
1✔
536
        found = self._callFUT(options)
1✔
537
        # Older files, .dat file not included
538
        self.assertEqual(found, [files[5], files[7]])
1✔
539

540

541
class Test_scandat(OptionsTestBase, unittest.TestCase):
1✔
542

543
    def _callFUT(self, repofiles):
1✔
544
        from ZODB.scripts.repozo import scandat
1✔
545
        return scandat(repofiles)
1✔
546

547
    def test_no_dat_file(self):
1✔
548
        self._makeOptions()
1✔
549
        fsfile = os.path.join(self._repository_directory, 'foo.fs')
1✔
550
        fn, startpos, endpos, sum = self._callFUT([fsfile])
1✔
551
        self.assertEqual(fn, None)
1✔
552
        self.assertEqual(startpos, None)
1✔
553
        self.assertEqual(endpos, None)
1✔
554
        self.assertEqual(sum, None)
1✔
555

556
    def test_empty_dat_file(self):
1✔
557
        self._makeOptions()
1✔
558
        fsfile = os.path.join(self._repository_directory, 'foo.fs')
1✔
559
        datfile = os.path.join(self._repository_directory, 'foo.dat')
1✔
560
        _write_file(datfile, b'')
1✔
561
        fn, startpos, endpos, sum = self._callFUT([fsfile])
1✔
562
        self.assertEqual(fn, None)
1✔
563
        self.assertEqual(startpos, None)
1✔
564
        self.assertEqual(endpos, None)
1✔
565
        self.assertEqual(sum, None)
1✔
566

567
    def test_single_line(self):
1✔
568
        self._makeOptions()
1✔
569
        fsfile = os.path.join(self._repository_directory, 'foo.fs')
1✔
570
        datfile = os.path.join(self._repository_directory, 'foo.dat')
1✔
571
        _write_file(datfile, b'foo.fs 0 123 ABC\n')
1✔
572
        fn, startpos, endpos, sum = self._callFUT([fsfile])
1✔
573
        self.assertEqual(fn, 'foo.fs')
1✔
574
        self.assertEqual(startpos, 0)
1✔
575
        self.assertEqual(endpos, 123)
1✔
576
        self.assertEqual(sum, 'ABC')
1✔
577

578
    def test_multiple_lines(self):
1✔
579
        self._makeOptions()
1✔
580
        fsfile = os.path.join(self._repository_directory, 'foo.fs')
1✔
581
        datfile = os.path.join(self._repository_directory, 'foo.dat')
1✔
582
        _write_file(datfile, b'foo.fs 0 123 ABC\n'
1✔
583
                             b'bar.deltafs 123 456 DEF\n')
584
        fn, startpos, endpos, sum = self._callFUT([fsfile])
1✔
585
        self.assertEqual(fn, 'bar.deltafs')
1✔
586
        self.assertEqual(startpos, 123)
1✔
587
        self.assertEqual(endpos, 456)
1✔
588
        self.assertEqual(sum, 'DEF')
1✔
589

590

591
class Test_delete_old_backups(OptionsTestBase, unittest.TestCase):
1✔
592

593
    def _makeOptions(self, filenames=()):
1✔
594
        options = super()._makeOptions()
1✔
595
        for filename in filenames:
1✔
596
            fqn = os.path.join(options.repository, filename)
1✔
597
            _write_file(fqn, b'testing delete_old_backups')
1✔
598
        return options
1✔
599

600
    def _callFUT(self, options=None, filenames=()):
1✔
601
        from ZODB.scripts.repozo import delete_old_backups
1✔
602
        if options is None:
1!
603
            options = self._makeOptions(filenames)
1✔
604
        return delete_old_backups(options)
1✔
605

606
    def test_empty_dir_doesnt_raise(self):
1✔
607
        self._callFUT()
1✔
608
        self.assertEqual(len(os.listdir(self._repository_directory)), 0)
1✔
609

610
    def test_no_repozo_files_doesnt_raise(self):
1✔
611
        FILENAMES = ['bogus.txt', 'not_a_repozo_file']
1✔
612
        self._callFUT(filenames=FILENAMES)
1✔
613
        remaining = os.listdir(self._repository_directory)
1✔
614
        self.assertEqual(len(remaining), len(FILENAMES))
1✔
615
        for name in FILENAMES:
1✔
616
            fqn = os.path.join(self._repository_directory, name)
1✔
617
            self.assertTrue(os.path.isfile(fqn))
1✔
618

619
    def test_doesnt_remove_current_repozo_files(self):
1✔
620
        FILENAMES = ['2009-12-20-10-08-03.fs',
1✔
621
                     '2009-12-20-10-08-03.dat',
622
                     '2009-12-20-10-08-03.index',
623
                     ]
624
        self._callFUT(filenames=FILENAMES)
1✔
625
        remaining = os.listdir(self._repository_directory)
1✔
626
        self.assertEqual(len(remaining), len(FILENAMES))
1✔
627
        for name in FILENAMES:
1✔
628
            fqn = os.path.join(self._repository_directory, name)
1✔
629
            self.assertTrue(os.path.isfile(fqn))
1✔
630

631
    def test_removes_older_repozo_files(self):
1✔
632
        OLDER_FULL = ['2009-12-20-00-01-03.fs',
1✔
633
                      '2009-12-20-00-01-03.dat',
634
                      '2009-12-20-00-01-03.index',
635
                      ]
636
        DELTAS = ['2009-12-21-00-00-01.deltafs',
1✔
637
                  '2009-12-21-00-00-01.index',
638
                  '2009-12-22-00-00-01.deltafs',
639
                  '2009-12-22-00-00-01.index',
640
                  ]
641
        CURRENT_FULL = ['2009-12-23-00-00-01.fs',
1✔
642
                        '2009-12-23-00-00-01.dat',
643
                        '2009-12-23-00-00-01.index',
644
                        ]
645
        FILENAMES = OLDER_FULL + DELTAS + CURRENT_FULL
1✔
646
        self._callFUT(filenames=FILENAMES)
1✔
647
        remaining = os.listdir(self._repository_directory)
1✔
648
        self.assertEqual(len(remaining), len(CURRENT_FULL))
1✔
649
        for name in OLDER_FULL:
1✔
650
            fqn = os.path.join(self._repository_directory, name)
1✔
651
            self.assertFalse(os.path.isfile(fqn))
1✔
652
        for name in DELTAS:
1✔
653
            fqn = os.path.join(self._repository_directory, name)
1✔
654
            self.assertFalse(os.path.isfile(fqn))
1✔
655
        for name in CURRENT_FULL:
1✔
656
            fqn = os.path.join(self._repository_directory, name)
1✔
657
            self.assertTrue(os.path.isfile(fqn))
1✔
658

659
    def test_removes_older_repozo_files_zipped(self):
1✔
660
        OLDER_FULL = ['2009-12-20-00-01-03.fsz',
1✔
661
                      '2009-12-20-00-01-03.dat',
662
                      '2009-12-20-00-01-03.index',
663
                      ]
664
        DELTAS = ['2009-12-21-00-00-01.deltafsz',
1✔
665
                  '2009-12-21-00-00-01.index',
666
                  '2009-12-22-00-00-01.deltafsz',
667
                  '2009-12-22-00-00-01.index',
668
                  ]
669
        CURRENT_FULL = ['2009-12-23-00-00-01.fsz',
1✔
670
                        '2009-12-23-00-00-01.dat',
671
                        '2009-12-23-00-00-01.index',
672
                        ]
673
        FILENAMES = OLDER_FULL + DELTAS + CURRENT_FULL
1✔
674
        self._callFUT(filenames=FILENAMES)
1✔
675
        remaining = os.listdir(self._repository_directory)
1✔
676
        self.assertEqual(len(remaining), len(CURRENT_FULL))
1✔
677
        for name in OLDER_FULL:
1✔
678
            fqn = os.path.join(self._repository_directory, name)
1✔
679
            self.assertFalse(os.path.isfile(fqn))
1✔
680
        for name in DELTAS:
1✔
681
            fqn = os.path.join(self._repository_directory, name)
1✔
682
            self.assertFalse(os.path.isfile(fqn))
1✔
683
        for name in CURRENT_FULL:
1✔
684
            fqn = os.path.join(self._repository_directory, name)
1✔
685
            self.assertTrue(os.path.isfile(fqn))
1✔
686

687

688
class Test_do_full_backup(OptionsTestBase, unittest.TestCase):
1✔
689

690
    def _callFUT(self, options):
1✔
691
        from ZODB.scripts.repozo import do_full_backup
1✔
692
        return do_full_backup(options)
1✔
693

694
    def _makeDB(self):
1✔
695
        import tempfile
1✔
696
        self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
697
        return OurDB(self._data_directory)
1✔
698

699
    def test_dont_overwrite_existing_file(self):
1✔
700
        from ZODB.scripts.repozo import WouldOverwriteFiles
1✔
701
        from ZODB.scripts.repozo import gen_filename
1✔
702
        db = self._makeDB()
1✔
703
        options = self._makeOptions(full=True,
1✔
704
                                    file=db._file_name,
705
                                    gzip=False,
706
                                    test_now=(2010, 5, 14, 10, 51, 22),
707
                                    )
708
        fqn = os.path.join(self._repository_directory, gen_filename(options))
1✔
709
        _write_file(fqn, b'TESTING')
1✔
710
        self.assertRaises(WouldOverwriteFiles, self._callFUT, options)
1✔
711

712
    def test_empty(self):
1✔
713
        import struct
1✔
714

715
        from ZODB.fsIndex import fsIndex
1✔
716
        from ZODB.scripts.repozo import gen_filename
1✔
717
        db = self._makeDB()
1✔
718
        options = self._makeOptions(file=db._file_name,
1✔
719
                                    gzip=False,
720
                                    killold=False,
721
                                    test_now=(2010, 5, 14, 10, 51, 22),
722
                                    )
723
        self._callFUT(options)
1✔
724
        target = os.path.join(self._repository_directory,
1✔
725
                              gen_filename(options))
726
        original = _read_file(db._file_name)
1✔
727
        self.assertEqual(_read_file(target), original)
1✔
728
        datfile = os.path.join(self._repository_directory,
1✔
729
                               gen_filename(options, '.dat'))
730
        self.assertEqual(_read_file(datfile, mode='r'),  # XXX 'rb'?
1✔
731
                         '%s 0 %d %s\n' %
732
                         (target, len(original), md5(original).hexdigest()))
733
        ndxfile = os.path.join(self._repository_directory,
1✔
734
                               gen_filename(options, '.index'))
735
        ndx_info = fsIndex.load(ndxfile)
1✔
736
        self.assertEqual(ndx_info['pos'], len(original))
1✔
737
        index = ndx_info['index']
1✔
738
        pZero = struct.pack(">Q", 0)
1✔
739
        pOne = struct.pack(">Q", 1)
1✔
740
        self.assertEqual(index.minKey(), pZero)
1✔
741
        self.assertEqual(index.maxKey(), pOne)
1✔
742

743

744
class Test_do_incremental_backup(OptionsTestBase, unittest.TestCase):
1✔
745

746
    def _callFUT(self, options, reposz, repofiles):
1✔
747
        from ZODB.scripts.repozo import do_incremental_backup
1✔
748
        return do_incremental_backup(options, reposz, repofiles)
1✔
749

750
    def _makeDB(self):
1✔
751
        import tempfile
1✔
752
        self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
753
        return OurDB(self._data_directory)
1✔
754

755
    def test_dont_overwrite_existing_file(self):
1✔
756
        from ZODB.scripts.repozo import WouldOverwriteFiles
1✔
757
        from ZODB.scripts.repozo import find_files
1✔
758
        from ZODB.scripts.repozo import gen_filename
1✔
759
        db = self._makeDB()
1✔
760
        options = self._makeOptions(full=False,
1✔
761
                                    file=db._file_name,
762
                                    gzip=False,
763
                                    test_now=(2010, 5, 14, 10, 51, 22),
764
                                    date=None,
765
                                    )
766
        fqn = os.path.join(self._repository_directory, gen_filename(options))
1✔
767
        _write_file(fqn, b'TESTING')
1✔
768
        repofiles = find_files(options)
1✔
769
        self.assertRaises(WouldOverwriteFiles,
1✔
770
                          self._callFUT, options, 0, repofiles)
771

772
    def test_no_changes(self):
1✔
773
        import struct
1✔
774

775
        from ZODB.fsIndex import fsIndex
1✔
776
        from ZODB.scripts.repozo import gen_filename
1✔
777
        db = self._makeDB()
1✔
778
        oldpos = db.pos
1✔
779
        options = self._makeOptions(file=db._file_name,
1✔
780
                                    gzip=False,
781
                                    killold=False,
782
                                    test_now=(2010, 5, 14, 10, 51, 22),
783
                                    date=None,
784
                                    )
785
        fullfile = os.path.join(self._repository_directory,
1✔
786
                                '2010-05-14-00-00-00.fs')
787
        original = _read_file(db._file_name)
1✔
788
        _write_file(fullfile, original)
1✔
789
        datfile = os.path.join(self._repository_directory,
1✔
790
                               '2010-05-14-00-00-00.dat')
791
        repofiles = [fullfile, datfile]
1✔
792
        self._callFUT(options, oldpos, repofiles)
1✔
793
        target = os.path.join(self._repository_directory,
1✔
794
                              gen_filename(options))
795
        self.assertEqual(_read_file(target), b'')
1✔
796
        self.assertEqual(_read_file(datfile, mode='r'),  # XXX mode='rb'?
1✔
797
                         '%s %d %d %s\n' %
798
                         (target, oldpos, oldpos, md5(b'').hexdigest()))
799
        ndxfile = os.path.join(self._repository_directory,
1✔
800
                               gen_filename(options, '.index'))
801
        ndx_info = fsIndex.load(ndxfile)
1✔
802
        self.assertEqual(ndx_info['pos'], oldpos)
1✔
803
        index = ndx_info['index']
1✔
804
        pZero = struct.pack(">Q", 0)
1✔
805
        pOne = struct.pack(">Q", 1)
1✔
806
        self.assertEqual(index.minKey(), pZero)
1✔
807
        self.assertEqual(index.maxKey(), pOne)
1✔
808

809
    def test_w_changes(self):
1✔
810
        import struct
1✔
811

812
        from ZODB.fsIndex import fsIndex
1✔
813
        from ZODB.scripts.repozo import gen_filename
1✔
814
        db = self._makeDB()
1✔
815
        oldpos = db.pos
1✔
816
        options = self._makeOptions(file=db._file_name,
1✔
817
                                    gzip=False,
818
                                    killold=False,
819
                                    test_now=(2010, 5, 14, 10, 51, 22),
820
                                    date=None,
821
                                    )
822
        fullfile = os.path.join(self._repository_directory,
1✔
823
                                '2010-05-14-00-00-00.fs')
824
        original = _read_file(db._file_name)
1✔
825
        f = _write_file(fullfile, original)
1✔
826
        datfile = os.path.join(self._repository_directory,
1✔
827
                               '2010-05-14-00-00-00.dat')
828
        repofiles = [fullfile, datfile]
1✔
829
        db.mutate()
1✔
830
        newpos = db.pos
1✔
831
        self._callFUT(options, oldpos, repofiles)
1✔
832
        target = os.path.join(self._repository_directory,
1✔
833
                              gen_filename(options))
834
        with open(db._file_name, 'rb') as f:
1✔
835
            f.seek(oldpos)
1✔
836
            increment = f.read()
1✔
837
        self.assertEqual(_read_file(target), increment)
1✔
838
        self.assertEqual(_read_file(datfile, mode='r'),  # XXX mode='rb'?
1✔
839
                         '%s %d %d %s\n' %
840
                         (target, oldpos, newpos,
841
                             md5(increment).hexdigest()))
842
        ndxfile = os.path.join(self._repository_directory,
1✔
843
                               gen_filename(options, '.index'))
844
        ndx_info = fsIndex.load(ndxfile)
1✔
845
        self.assertEqual(ndx_info['pos'], newpos)
1✔
846
        index = ndx_info['index']
1✔
847
        pZero = struct.pack(">Q", 0)
1✔
848
        self.assertEqual(index.minKey(), pZero)
1✔
849
        self.assertEqual(index.maxKey(), db.maxkey)
1✔
850

851

852
class Mixin_do_recover:
1✔
853
    def _callFUT(self, options):
1✔
854
        from ZODB.scripts.repozo import do_recover
1✔
855
        return do_recover(options)
1✔
856

857
    def _makeFile(self, hour, min, sec, ext, text=None):
1✔
858
        # call _makeOptions first!
859
        name = '2010-05-14-%02d-%02d-%02d%s' % (hour, min, sec, ext)
1✔
860
        if text is None:
1✔
861
            text = name
1✔
862
        fqn = os.path.join(self._repository_directory, name)
1✔
863
        _write_file(fqn, text.encode())
1✔
864
        return fqn
1✔
865

866
    def test_no_files(self):
1✔
867
        from ZODB.scripts.repozo import NoFiles
1✔
868
        options = self._makeOptions(date=None,
1✔
869
                                    test_now=(2010, 5, 15, 13, 30, 57))
870
        self.assertRaises(NoFiles, self._callFUT, options)
1✔
871

872
    def test_no_files_before_explicit_date(self):
1✔
873
        from ZODB.scripts.repozo import NoFiles
1✔
874
        options = self._makeOptions(date='2010-05-13-13-30-57')
1✔
875
        files = []
1✔
876
        for h, m, s, e in [(2, 13, 14, '.fs'),
1✔
877
                           (2, 13, 14, '.dat'),
878
                           (3, 14, 15, '.deltafs'),
879
                           (4, 14, 15, '.deltafs'),
880
                           (5, 14, 15, '.deltafs'),
881
                           (12, 13, 14, '.fs'),
882
                           (12, 13, 14, '.dat'),
883
                           (13, 14, 15, '.deltafs'),
884
                           (14, 15, 16, '.deltafs'),
885
                           ]:
886
            files.append(self._makeFile(h, m, s, e))
1✔
887
        self.assertRaises(NoFiles, self._callFUT, options)
1✔
888

889

890
class Test_do_full_recover(
1✔
891
        Mixin_do_recover,
892
        OptionsTestBase,
893
        unittest.TestCase
894
):
895
    def _makeOptions(self, **kw):
1✔
896
        options = super()._makeOptions(**kw)
1✔
897
        options.full = True
1✔
898
        return options
1✔
899

900
    def test_w_full_backup_latest_no_index(self):
1✔
901
        import tempfile
1✔
902
        dd = self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
903
        output = os.path.join(dd, 'Data.fs')
1✔
904
        options = self._makeOptions(date='2010-05-15-13-30-57',
1✔
905
                                    output=output,
906
                                    withverify=False)
907
        self._makeFile(2, 3, 4, '.fs', 'AAA')
1✔
908
        self._makeFile(4, 5, 6, '.fs', 'BBB')
1✔
909
        self._callFUT(options)
1✔
910
        self.assertEqual(_read_file(output), b'BBB')
1✔
911

912
    def test_w_full_backup_latest_index(self):
1✔
913
        import tempfile
1✔
914
        dd = self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
915
        output = os.path.join(dd, 'Data.fs')
1✔
916
        index = os.path.join(dd, 'Data.fs.index')
1✔
917
        options = self._makeOptions(date='2010-05-15-13-30-57',
1✔
918
                                    output=output,
919
                                    withverify=False)
920
        self._makeFile(2, 3, 4, '.fs', 'AAA')
1✔
921
        self._makeFile(4, 5, 6, '.fs', 'BBB')
1✔
922
        self._makeFile(4, 5, 6, '.index', 'CCC')
1✔
923
        self._callFUT(options)
1✔
924
        self.assertEqual(_read_file(output), b'BBB')
1✔
925
        self.assertEqual(_read_file(index), b'CCC')
1✔
926

927
    def test_w_incr_backup_latest_no_index(self):
1✔
928
        import tempfile
1✔
929
        dd = self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
930
        output = os.path.join(dd, 'Data.fs')
1✔
931
        options = self._makeOptions(date='2010-05-15-13-30-57',
1✔
932
                                    output=output,
933
                                    withverify=False)
934
        self._makeFile(2, 3, 4, '.fs', 'AAA')
1✔
935
        self._makeFile(4, 5, 6, '.deltafs', 'BBB')
1✔
936
        self._callFUT(options)
1✔
937
        self.assertEqual(_read_file(output), b'AAABBB')
1✔
938

939
    def test_w_incr_backup_latest_index(self):
1✔
940
        import tempfile
1✔
941
        dd = self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
942
        output = os.path.join(dd, 'Data.fs')
1✔
943
        index = os.path.join(dd, 'Data.fs.index')
1✔
944
        options = self._makeOptions(date='2010-05-15-13-30-57',
1✔
945
                                    output=output,
946
                                    withverify=False)
947
        self._makeFile(2, 3, 4, '.fs', 'AAA')
1✔
948
        self._makeFile(4, 5, 6, '.deltafs', 'BBB')
1✔
949
        self._makeFile(4, 5, 6, '.index', 'CCC')
1✔
950
        self._callFUT(options)
1✔
951
        self.assertEqual(_read_file(output), b'AAABBB')
1✔
952
        self.assertEqual(_read_file(index), b'CCC')
1✔
953

954
    def test_w_incr_backup_with_verify_all_is_fine(self):
1✔
955
        import tempfile
1✔
956
        dd = self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
957
        output = os.path.join(dd, 'Data.fs')
1✔
958
        options = self._makeOptions(date='2010-05-15-13-30-57',
1✔
959
                                    output=output,
960
                                    withverify=True)
961
        self._makeFile(2, 3, 4, '.fs', 'AAA')
1✔
962
        self._makeFile(4, 5, 6, '.deltafs', 'BBBB')
1✔
963
        self._makeFile(
1✔
964
            2, 3, 4, '.dat',
965
            '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
966
            '/backup/2010-05-14-04-05-06.deltafs 3 7 f50881ced34c7d9e6bce100bf33dec60\n')  # noqa: E501 line too long
967
        self._callFUT(options)
1✔
968
        self.assertFalse(os.path.exists(output + '.part'))
1✔
969
        self.assertEqual(_read_file(output), b'AAABBBB')
1✔
970

971
    def test_w_incr_backup_with_verify_sum_inconsistent(self):
1✔
972
        import tempfile
1✔
973

974
        from ZODB.scripts.repozo import VerificationFail
1✔
975
        dd = self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
976
        output = os.path.join(dd, 'Data.fs')
1✔
977
        options = self._makeOptions(date='2010-05-15-13-30-57',
1✔
978
                                    output=output,
979
                                    withverify=True)
980
        self._makeFile(2, 3, 4, '.fs', 'AAA')
1✔
981
        self._makeFile(4, 5, 6, '.deltafs', 'BBBB')
1✔
982
        self._makeFile(
1✔
983
            2, 3, 4, '.dat',
984
            '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
985
            '/backup/2010-05-14-04-05-06.deltafs 3 7 f50881ced34c7d9e6bce100bf33dec61\n')  # noqa: E501 line too long
986
        self.assertRaises(VerificationFail, self._callFUT, options)
1✔
987
        self.assertTrue(os.path.exists(output + '.part'))
1✔
988

989
    def test_w_incr_backup_with_verify_size_inconsistent(self):
1✔
990
        import tempfile
1✔
991

992
        from ZODB.scripts.repozo import VerificationFail
1✔
993
        dd = self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
994
        output = os.path.join(dd, 'Data.fs')
1✔
995
        options = self._makeOptions(date='2010-05-15-13-30-57',
1✔
996
                                    output=output,
997
                                    withverify=True)
998
        self._makeFile(2, 3, 4, '.fs', 'AAA')
1✔
999
        self._makeFile(4, 5, 6, '.deltafs', 'BBBB')
1✔
1000
        self._makeFile(
1✔
1001
            2, 3, 4, '.dat',
1002
            '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1003
            '/backup/2010-05-14-04-05-06.deltafs 3 8 f50881ced34c7d9e6bce100bf33dec60\n')  # noqa: E501 line too long
1004
        self.assertRaises(VerificationFail, self._callFUT, options)
1✔
1005
        self.assertTrue(os.path.exists(output + '.part'))
1✔
1006

1007

1008
class Test_do_incremental_recover(
1✔
1009
        Mixin_do_recover,
1010
        OptionsTestBase,
1011
        unittest.TestCase
1012
):
1013
    def setUp(self):
1✔
1014
        from ZODB.scripts import repozo
1✔
1015
        self._old_verbosity = repozo.VERBOSE
1✔
1016
        self._old_stderr = sys.stderr
1✔
1017
        repozo.VERBOSE = True
1✔
1018
        sys.stderr = StringIO()
1✔
1019

1020
    def tearDown(self):
1✔
1021
        from ZODB.scripts import repozo
1✔
1022
        sys.stderr = self._old_stderr
1✔
1023
        repozo.VERBOSE = self._old_verbosity
1✔
1024

1025
    def _makeOptions(self, **kw):
1✔
1026
        options = super()._makeOptions(**kw)
1✔
1027
        options.full = False
1✔
1028
        return options
1✔
1029

1030
    def _createRecoveredDataFS(self, output, options):
1✔
1031
        self._makeFile(2, 3, 4, '.fs', 'AAA')
1✔
1032
        self._makeFile(4, 5, 6, '.deltafs', 'BBB')
1✔
1033
        self._makeFile(
1✔
1034
            2, 3, 4, '.dat',
1035
            '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1036
            '/backup/2010-05-14-04-05-06.deltafs 3 6 2bb225f0ba9a58930757a868ed57d9a3\n')  # noqa: E501 line too long
1037
        self._callFUT(options)
1✔
1038
        self.assertEqual(_read_file(output), b'AAABBB')
1✔
1039
        self.assertFalse(os.path.exists(output + '.part'))
1✔
1040
        return output
1✔
1041

1042
    def test_do_nothing(self):
1✔
1043
        import tempfile
1✔
1044
        dd = self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
1045
        output = os.path.join(dd, 'Data.fs')
1✔
1046
        options = self._makeOptions(date='2010-05-15-13-30-57',
1✔
1047
                                    output=output,
1048
                                    withverify=False)
1049
        self._createRecoveredDataFS(output, options)
1✔
1050
        self._callFUT(options)
1✔
1051
        self.assertIn(
1✔
1052
            "doing nothing", sys.stderr.getvalue())
1053

1054
    def test_w_incr_recover_from_incr_backup(self):
1✔
1055
        import tempfile
1✔
1056
        dd = self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
1057
        output = os.path.join(dd, 'Data.fs')
1✔
1058
        options = self._makeOptions(date='2010-05-15-13-30-57',
1✔
1059
                                    output=output,
1060
                                    withverify=False)
1061
        self._createRecoveredDataFS(output, options)
1✔
1062
        # Create 2 more .deltafs, to prove the code knows where to pick up
1063
        self._makeFile(6, 7, 8, '.deltafs', 'CCC')
1✔
1064
        self._makeFile(8, 9, 10, '.deltafs', 'DDD')
1✔
1065
        self._makeFile(
1✔
1066
            2, 3, 4, '.dat',
1067
            '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1068
            '/backup/2010-05-14-04-05-06.deltafs 3 6 2bb225f0ba9a58930757a868ed57d9a3\n'  # noqa: E501 line too long
1069
            '/backup/2010-05-14-06-07-08.deltafs 6 9 defb99e69a9f1f6e06f15006b1f166ae\n'  # noqa: E501 line too long
1070
            '/backup/2010-05-14-08-09-10.deltafs 9 12 45054f47ac3305a2a33e9bcceadff712\n')  # noqa: E501 line too long
1071
        self._callFUT(options)
1✔
1072
        self.assertEqual(_read_file(output), b'AAABBBCCCDDD')
1✔
1073
        self.assertFalse(os.path.exists(output + '.part'))
1✔
1074

1075
    def test_w_incr_backup_with_verify_sum_inconsistent(self):
1✔
1076
        import tempfile
1✔
1077
        dd = self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
1078
        output = os.path.join(dd, 'Data.fs')
1✔
1079
        options = self._makeOptions(date='2010-05-15-13-30-57',
1✔
1080
                                    output=output,
1081
                                    withverify=True)
1082
        self._createRecoveredDataFS(output, options)
1✔
1083
        self._makeFile(6, 7, 8, '.deltafs', 'CCC')
1✔
1084
        self._makeFile(8, 9, 10, '.deltafs', 'DDD')
1✔
1085
        self._makeFile(
1✔
1086
            2, 3, 4, '.dat',
1087
            '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1088
            '/backup/2010-05-14-04-05-06.deltafs 3 6 2bb225f0ba9a58930757a868ed57d9a3\n'  # noqa: E501 line too long
1089
            '/backup/2010-05-14-06-07-08.deltafs 6 9 defb99e69a9f1f6e06f15006b1f166af\n'  # noqa: E501 line too long
1090
            '/backup/2010-05-14-08-09-10.deltafs 9 12 45054f47ac3305a2a33e9bcceadff712\n')  # noqa: E501 line too long
1091
        from ZODB.scripts.repozo import VerificationFail
1✔
1092
        self.assertRaises(VerificationFail, self._callFUT, options)
1✔
1093
        self.assertTrue(os.path.exists(output + '.part'))
1✔
1094

1095
    def test_w_incr_backup_with_verify_size_inconsistent_too_small(self):
1✔
1096
        import tempfile
1✔
1097
        dd = self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
1098
        output = os.path.join(dd, 'Data.fs')
1✔
1099
        options = self._makeOptions(date='2010-05-15-13-30-57',
1✔
1100
                                    output=output,
1101
                                    withverify=True)
1102
        self._createRecoveredDataFS(output, options)
1✔
1103
        self._makeFile(6, 7, 8, '.deltafs', 'CCC')
1✔
1104
        self._makeFile(8, 9, 10, '.deltafs', 'DDD')
1✔
1105
        self._makeFile(
1✔
1106
            2, 3, 4, '.dat',
1107
            '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1108
            '/backup/2010-05-14-04-05-06.deltafs 3 6 2bb225f0ba9a58930757a868ed57d9a3\n'  # noqa: E501 line too long
1109
            '/backup/2010-05-14-06-07-08.deltafs 6 8 defb99e69a9f1f6e06f15006b1f166ae\n'  # noqa: E501 line too long
1110
            '/backup/2010-05-14-08-09-10.deltafs 9 12 45054f47ac3305a2a33e9bcceadff712\n')  # noqa: E501 line too long
1111
        from ZODB.scripts.repozo import VerificationFail
1✔
1112
        self.assertRaises(VerificationFail, self._callFUT, options)
1✔
1113
        self.assertTrue(os.path.exists(output + '.part'))
1✔
1114

1115
    def test_w_incr_backup_with_verify_size_inconsistent_too_big(self):
1✔
1116
        import tempfile
1✔
1117
        dd = self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
1118
        output = os.path.join(dd, 'Data.fs')
1✔
1119
        options = self._makeOptions(date='2010-05-15-13-30-57',
1✔
1120
                                    output=output,
1121
                                    withverify=True)
1122
        self._createRecoveredDataFS(output, options)
1✔
1123
        self._makeFile(6, 7, 8, '.deltafs', 'CCC')
1✔
1124
        self._makeFile(8, 9, 10, '.deltafs', 'DDD')
1✔
1125
        self._makeFile(
1✔
1126
            2, 3, 4, '.dat',
1127
            '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1128
            '/backup/2010-05-14-04-05-06.deltafs 3 6 2bb225f0ba9a58930757a868ed57d9a3\n'  # noqa: E501 line too long
1129
            '/backup/2010-05-14-06-07-08.deltafs 6 10 defb99e69a9f1f6e06f15006b1f166ae\n'  # noqa: E501 line too long
1130
            '/backup/2010-05-14-08-09-10.deltafs 9 12 45054f47ac3305a2a33e9bcceadff712\n')  # noqa: E501 line too long
1131
        from ZODB.scripts.repozo import VerificationFail
1✔
1132
        self.assertRaises(VerificationFail, self._callFUT, options)
1✔
1133
        self.assertTrue(os.path.exists(output + '.part'))
1✔
1134

1135
    def test_w_inc_backup_switch_auto_to_full_recover_if_output_larger_than_dat(self):  # noqa: E501 line too long
1✔
1136
        import tempfile
1✔
1137
        dd = self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
1138
        output = os.path.join(dd, 'Data.fs')
1✔
1139
        options = self._makeOptions(date='2010-05-15-13-30-57',
1✔
1140
                                    output=output,
1141
                                    withverify=False)
1142
        self._createRecoveredDataFS(output, options)
1✔
1143
        self._makeFile(6, 7, 8, '.deltafs', 'CCC')
1✔
1144
        self._makeFile(
1✔
1145
            2, 3, 4, '.dat',
1146
            '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1147
            '/backup/2010-05-14-04-05-06.deltafs 3 6 2bb225f0ba9a58930757a868ed57d9a3\n'  # noqa: E501 line too long
1148
            '/backup/2010-05-14-06-07-08.deltafs 6 9 defb99e69a9f1f6e06f15006b1f166ae\n')  # noqa: E501 line too long
1149
        # The ZODB is longer than announced in the .dat file
1150
        with open(output, 'r+b') as f:
1✔
1151
            f.write(b'AAABBBCCCDDD')
1✔
1152
        self._callFUT(options)
1✔
1153
        self.assertEqual(_read_file(output), b'AAABBBCCC')
1✔
1154
        self.assertFalse(os.path.exists(output + '.part'))
1✔
1155
        self.assertIn(
1✔
1156
            "falling back to a full recover", sys.stderr.getvalue())
1157

1158
    def test_w_inc_backup_switch_auto_to_full_recover_if_last_chunk_is_wrong(self):  # noqa: E501 line too long
1✔
1159
        import tempfile
1✔
1160
        dd = self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
1161
        output = os.path.join(dd, 'Data.fs')
1✔
1162
        options = self._makeOptions(date='2010-05-15-13-30-57',
1✔
1163
                                    output=output,
1164
                                    withverify=False)
1165
        self._createRecoveredDataFS(output, options)
1✔
1166
        self._makeFile(6, 7, 8, '.deltafs', 'CCC')
1✔
1167
        self._makeFile(
1✔
1168
            2, 3, 4, '.dat',
1169
            '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1170
            '/backup/2010-05-14-04-05-06.deltafs 3 6 2bb225f0ba9a58930757a868ed57d9a4\n'  # noqa: E501 line too long
1171
            '/backup/2010-05-14-06-07-08.deltafs 6 9 defb99e69a9f1f6e06f15006b1f166ae\n')  # noqa: E501 line too long
1172
        self._callFUT(options)
1✔
1173
        self.assertEqual(_read_file(output), b'AAABBBCCC')
1✔
1174
        self.assertFalse(os.path.exists(output + '.part'))
1✔
1175
        self.assertIn(
1✔
1176
            "Last whole common chunk checksum did not match with backup, falling back to a full recover.",  # noqa: E501 line too long
1177
            sys.stderr.getvalue())
1178

1179
    def test_w_inc_backup_switch_auto_to_full_recover_after_pack(self):
1✔
1180
        import tempfile
1✔
1181
        dd = self._data_directory = tempfile.mkdtemp(prefix='zodb-test-')
1✔
1182
        output = os.path.join(dd, 'Data.fs')
1✔
1183
        options = self._makeOptions(date='2010-05-15-13-30-57',
1✔
1184
                                    output=output,
1185
                                    withverify=False)
1186
        self._makeFile(2, 3, 4, '.fs', 'AAA')
1✔
1187
        self._makeFile(4, 5, 6, '.deltafs', 'BBB')
1✔
1188
        self._makeFile(
1✔
1189
            2, 3, 4, '.dat',
1190
            '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1191
            '/backup/2010-05-14-04-05-06.deltafs 3 6 2bb225f0ba9a58930757a868ed57d9a3\n')  # noqa: E501 line too long
1192
        self._callFUT(options)
1✔
1193
        self.assertEqual(_read_file(output), b'AAABBB')
1✔
1194

1195
        self._makeFile(6, 7, 8, '.fs', 'CCDD')
1✔
1196
        self._makeFile(
1✔
1197
            6, 7, 8, '.dat',
1198
            '/backup/2010-05-14-06-07-08.fs 0 4 dc0ee37408176d839c13f291a4d588de\n')  # noqa: E501 line too long
1199
        self._callFUT(options)
1✔
1200
        self.assertEqual(_read_file(output), b'CCDD')
1✔
1201
        self.assertFalse(os.path.exists(output + '.part'))
1✔
1202
        self.assertIn(
1✔
1203
            'Target file is longer than latest backup, falling back to a full recover.',  # noqa: E501 line too long
1204
            sys.stderr.getvalue())
1205

1206

1207
class Test_do_verify(OptionsTestBase, unittest.TestCase):
1✔
1208

1209
    def _callFUT(self, options):
1✔
1210
        from ZODB.scripts.repozo import do_verify
1✔
1211
        do_verify(options)
1✔
1212

1213
    def _makeFile(self, hour, min, sec, ext, text=None):
1✔
1214
        from ZODB.scripts.repozo import _GzipCloser
1✔
1215
        assert self._repository_directory, 'call _makeOptions first!'
1✔
1216
        name = '2010-05-14-%02d-%02d-%02d%s' % (hour, min, sec, ext)
1✔
1217
        if text is None:
1!
1218
            text = name
×
1219
        fqn = os.path.join(self._repository_directory, name)
1✔
1220
        if ext.endswith('fsz'):
1✔
1221
            _opener = _GzipCloser
1✔
1222
        else:
1223
            _opener = open
1✔
1224
        with _opener(fqn, 'wb') as f:
1✔
1225
            f.write(text.encode())
1✔
1226
            f.flush()
1✔
1227
        return fqn
1✔
1228

1229
    def test_no_files(self):
1✔
1230
        from ZODB.scripts.repozo import NoFiles
1✔
1231
        options = self._makeOptions()
1✔
1232
        self.assertRaises(NoFiles, self._callFUT, options)
1✔
1233

1234
    def test_all_is_fine(self):
1✔
1235
        options = self._makeOptions(quick=False)
1✔
1236
        self._makeFile(2, 3, 4, '.fs', 'AAA')
1✔
1237
        self._makeFile(4, 5, 6, '.deltafs', 'BBBB')
1✔
1238
        self._makeFile(
1✔
1239
            2, 3, 4, '.dat',
1240
            '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1241
            '/backup/2010-05-14-04-05-06.deltafs 3 7 f50881ced34c7d9e6bce100bf33dec60\n')  # noqa: E501 line too long
1242
        self._callFUT(options)
1✔
1243

1244
    def test_all_is_fine_gzip(self):
1✔
1245
        options = self._makeOptions(quick=False)
1✔
1246
        self._makeFile(2, 3, 4, '.fsz', 'AAA')
1✔
1247
        self._makeFile(4, 5, 6, '.deltafsz', 'BBBB')
1✔
1248
        self._makeFile(
1✔
1249
            2, 3, 4, '.dat',
1250
            '/backup/2010-05-14-02-03-04.fsz 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1251
            '/backup/2010-05-14-04-05-06.deltafsz 3 7 f50881ced34c7d9e6bce100bf33dec60\n')  # noqa: E501 line too long
1252
        self._callFUT(options)
1✔
1253

1254
    def test_missing_file(self):
1✔
1255
        from ZODB.scripts.repozo import VerificationFail
1✔
1256
        options = self._makeOptions(quick=True)
1✔
1257
        self._makeFile(2, 3, 4, '.fs', 'AAA')
1✔
1258
        self._makeFile(
1✔
1259
            2, 3, 4, '.dat',
1260
            '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1261
            '/backup/2010-05-14-04-05-06.deltafs 3 7 f50881ced34c7d9e6bce100bf33dec60\n')  # noqa: E501 line too long
1262
        self.assertRaisesRegex(
1✔
1263
            VerificationFail,
1264
            '2010-05-14-04-05-06.deltafs is missing',
1265
            self._callFUT,
1266
            options,
1267
        )
1268

1269
    def test_missing_file_gzip(self):
1✔
1270
        from ZODB.scripts.repozo import VerificationFail
1✔
1271
        options = self._makeOptions(quick=True)
1✔
1272
        self._makeFile(2, 3, 4, '.fsz', 'AAA')
1✔
1273
        self._makeFile(
1✔
1274
            2, 3, 4, '.dat',
1275
            '/backup/2010-05-14-02-03-04.fsz 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1276
            '/backup/2010-05-14-04-05-06.deltafsz 3 7 f50881ced34c7d9e6bce100bf33dec60\n')  # noqa: E501 line too long
1277
        self.assertRaisesRegex(
1✔
1278
            VerificationFail,
1279
            '2010-05-14-04-05-06.deltafsz is missing',
1280
            self._callFUT,
1281
            options,
1282
        )
1283

1284
    def test_bad_size(self):
1✔
1285
        from ZODB.scripts.repozo import VerificationFail
1✔
1286
        options = self._makeOptions(quick=False)
1✔
1287
        self._makeFile(2, 3, 4, '.fs', 'AAA')
1✔
1288
        self._makeFile(4, 5, 6, '.deltafs', 'BBB')
1✔
1289
        self._makeFile(
1✔
1290
            2, 3, 4, '.dat',
1291
            '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1292
            '/backup/2010-05-14-04-05-06.deltafs 3 7 f50881ced34c7d9e6bce100bf33dec60\n')  # noqa: E501 line too long
1293
        self.assertRaisesRegex(
1✔
1294
            VerificationFail,
1295
            '2010-05-14-04-05-06.deltafs is 3 bytes, should be 4 bytes',
1296
            self._callFUT,
1297
            options,
1298
        )
1299

1300
    def test_bad_size_gzip(self):
1✔
1301
        from ZODB.scripts.repozo import VerificationFail
1✔
1302
        options = self._makeOptions(quick=False)
1✔
1303
        self._makeFile(2, 3, 4, '.fsz', 'AAA')
1✔
1304
        self._makeFile(4, 5, 6, '.deltafsz', 'BBB')
1✔
1305
        self._makeFile(
1✔
1306
            2, 3, 4, '.dat',
1307
            '/backup/2010-05-14-02-03-04.fsz 0 3 e1faffb3e614e6c2fba74296962386b7\n'   # noqa: E501 line too long
1308
            '/backup/2010-05-14-04-05-06.deltafsz 3 7 f50881ced34c7d9e6bce100bf33dec60\n')  # noqa: E501 line too long
1309
        self.assertRaisesRegex(
1✔
1310
            VerificationFail,
1311
            (
1312
                '2010-05-14-04-05-06.deltafsz is 3 bytes'
1313
                r' \(when uncompressed\), should be 4 bytes'
1314
            ),
1315
            self._callFUT,
1316
            options,
1317
        )
1318

1319
    def test_bad_checksum(self):
1✔
1320
        from ZODB.scripts.repozo import VerificationFail
1✔
1321
        options = self._makeOptions(quick=False)
1✔
1322
        self._makeFile(2, 3, 4, '.fs', 'AAA')
1✔
1323
        self._makeFile(4, 5, 6, '.deltafs', 'BbBB')
1✔
1324
        self._makeFile(
1✔
1325
            2, 3, 4, '.dat',
1326
            '/backup/2010-05-14-02-03-04.fs 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1327
            '/backup/2010-05-14-04-05-06.deltafs 3 7 f50881ced34c7d9e6bce100bf33dec60\n')  # noqa: E501 line too long
1328
        self.assertRaisesRegex(
1✔
1329
            VerificationFail,
1330
            (
1331
                '2010-05-14-04-05-06.deltafs has checksum'
1332
                + ' 36486440db255f0ee6ab109d5d231406 instead of'
1333
                + ' f50881ced34c7d9e6bce100bf33dec60'
1334
            ),
1335
            self._callFUT,
1336
            options,
1337
        )
1338

1339
    def test_bad_checksum_gzip(self):
1✔
1340
        from ZODB.scripts.repozo import VerificationFail
1✔
1341
        options = self._makeOptions(quick=False)
1✔
1342
        self._makeFile(2, 3, 4, '.fsz', 'AAA')
1✔
1343
        self._makeFile(4, 5, 6, '.deltafsz', 'BbBB')
1✔
1344
        self._makeFile(
1✔
1345
            2, 3, 4, '.dat',
1346
            '/backup/2010-05-14-02-03-04.fsz 0 3 e1faffb3e614e6c2fba74296962386b7\n'  # noqa: E501 line too long
1347
            '/backup/2010-05-14-04-05-06.deltafsz 3 7 f50881ced34c7d9e6bce100bf33dec60\n')  # noqa: E501 line too long
1348
        self.assertRaisesRegex(
1✔
1349
            VerificationFail,
1350
            (
1351
                '2010-05-14-04-05-06.deltafsz has checksum'
1352
                + r' 36486440db255f0ee6ab109d5d231406 \(when uncompressed\)'
1353
                + ' instead of f50881ced34c7d9e6bce100bf33dec60'),
1354
            self._callFUT,
1355
            options,
1356
        )
1357

1358
    def test_quick_ignores_checksums(self):
1✔
1359
        options = self._makeOptions(quick=True)
1✔
1360
        self._makeFile(2, 3, 4, '.fs', 'AAA')
1✔
1361
        self._makeFile(4, 5, 6, '.deltafs', 'BBBB')
1✔
1362
        self._makeFile(
1✔
1363
                2, 3, 4, '.dat',
1364
                '/backup/2010-05-14-02-03-04.fs 0 3 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'  # noqa: E501 line too long
1365
                '/backup/2010-05-14-04-05-06.deltafs 3 7 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n')  # noqa: E501 line too long
1366
        self._callFUT(options)
1✔
1367

1368
    def test_quick_ignores_checksums_gzip(self):
1✔
1369
        options = self._makeOptions(quick=True)
1✔
1370
        self._makeFile(2, 3, 4, '.fsz', 'AAA')
1✔
1371
        self._makeFile(4, 5, 6, '.deltafsz', 'BBBB')
1✔
1372
        self._makeFile(
1✔
1373
            2, 3, 4, '.dat',
1374
            '/backup/2010-05-14-02-03-04.fsz 0 3 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'  # noqa: E501 line too long
1375
            '/backup/2010-05-14-04-05-06.deltafsz 3 7 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n')  # noqa: E501 line too long
1376
        self._callFUT(options)
1✔
1377

1378

1379
class MonteCarloTests(unittest.TestCase):
1✔
1380

1381
    layer = ZODB.tests.util.MininalTestLayer('repozo')
1✔
1382

1383
    def setUp(self):
1✔
1384
        # compute directory names
1385
        import tempfile
1✔
1386
        self.basedir = tempfile.mkdtemp(prefix='zodb-test-')
1✔
1387
        self.backupdir = os.path.join(self.basedir, 'backup')
1✔
1388
        self.datadir = os.path.join(self.basedir, 'data')
1✔
1389
        self.restoredir = os.path.join(self.basedir, 'restore')
1✔
1390
        self.copydir = os.path.join(self.basedir, 'copy')
1✔
1391
        self.currdir = os.getcwd()
1✔
1392
        # create empty directories
1393
        os.mkdir(self.backupdir)
1✔
1394
        os.mkdir(self.datadir)
1✔
1395
        os.mkdir(self.restoredir)
1✔
1396
        os.mkdir(self.copydir)
1✔
1397
        os.chdir(self.datadir)
1✔
1398
        self.db = OurDB(self.datadir)
1✔
1399

1400
    def tearDown(self):
1✔
1401
        os.chdir(self.currdir)
1✔
1402
        import shutil
1✔
1403
        shutil.rmtree(self.basedir)
1✔
1404

1405
    def _callRepozoMain(self, argv):
1✔
1406
        from ZODB.scripts.repozo import main
1✔
1407
        main(argv)
1✔
1408

1409
    @ZODB.tests.util.time_monotonically_increases
1✔
1410
    def test_via_monte_carlo(self):
1✔
1411
        self.saved_snapshots = []  # list of (name, time) pairs for copies.
1✔
1412

1413
        for i in range(100):
1✔
1414
            self.mutate_pack_backup(i)
1✔
1415

1416
        # Verify snapshots can be reproduced exactly.
1417
        for copyname, copytime in self.saved_snapshots:
1✔
1418
            if _NOISY:
1!
UNCOV
1419
                print("Checking that", copyname, end=' ')
×
UNCOV
1420
                print("at", copytime, "is reproducible.")
×
1421
            self.assertRestored(copyname, copytime)
1✔
1422

1423
    def mutate_pack_backup(self, i):
1✔
1424
        import random
1✔
1425
        from shutil import copyfile
1✔
1426
        from time import gmtime
1✔
1427
        self.db.mutate()
1✔
1428

1429
        # Pack about each tenth time.
1430
        if random.random() < 0.1:
1✔
1431
            if _NOISY:
1!
UNCOV
1432
                print("packing")
×
1433
            self.db.pack()
1✔
1434
            self.db.close()
1✔
1435

1436
        # Make an incremental backup, half the time with gzip (-z).
1437
        argv = ['-BQr', self.backupdir, '-f', 'Data.fs']
1✔
1438
        if _NOISY:
1!
UNCOV
1439
            argv.insert(0, '-v')
×
1440
        if random.random() < 0.5:
1✔
1441
            argv.insert(0, '-z')
1✔
1442
        self._callRepozoMain(argv)
1✔
1443

1444
        # Save snapshots to assert that dated restores are possible
1445
        if i % 9 == 0:
1✔
1446
            srcname = os.path.join(self.datadir, 'Data.fs')
1✔
1447
            copytime = '%04d-%02d-%02d-%02d-%02d-%02d' % (gmtime()[:6])
1✔
1448
            copyname = os.path.join(self.copydir, "Data%d.fs" % i)
1✔
1449
            copyfile(srcname, copyname)
1✔
1450
            self.saved_snapshots.append((copyname, copytime))
1✔
1451

1452
        # The clock moves forward automatically on calls to time.time()
1453

1454
        # Verify current Data.fs can be reproduced exactly.
1455
        self.assertRestored()
1✔
1456

1457
    def assertRestored(self, correctpath='Data.fs', when=None):
1✔
1458
        # Do recovery to time 'when', and check that it's identical to
1459
        # correctpath.
1460
        # restore to Restored.fs
1461
        restoredfile = os.path.join(self.restoredir, 'Restored.fs')
1✔
1462
        argv = ['-Rr', self.backupdir, '-o', restoredfile]
1✔
1463
        if _NOISY:
1!
UNCOV
1464
            argv.insert(0, '-v')
×
1465
        if when is not None:
1✔
1466
            argv.append('-D')
1✔
1467
            argv.append(when)
1✔
1468
        self._callRepozoMain(argv)
1✔
1469

1470
        # check restored file content is equal to file that was backed up
1471
        fguts = _read_file(correctpath)
1✔
1472
        gguts = _read_file(restoredfile)
1✔
1473
        msg = ("guts don't match\ncorrectpath=%r when=%r\n cmd=%r" %
1✔
1474
               (correctpath, when, ' '.join(argv)))
1475
        self.assertEqual(fguts, gguts, msg)
1✔
1476

1477

1478
def test_suite():
1✔
1479
    loadTestsFromTestCase = unittest.defaultTestLoader.loadTestsFromTestCase
1✔
1480
    return unittest.TestSuite([
1✔
1481
        loadTestsFromTestCase(Test_parseargs),
1482
        loadTestsFromTestCase(Test_dofile),
1483
        loadTestsFromTestCase(Test_checksum),
1484
        loadTestsFromTestCase(Test_copyfile),
1485
        loadTestsFromTestCase(Test_concat),
1486
        loadTestsFromTestCase(Test_gen_filename),
1487
        loadTestsFromTestCase(Test_find_files),
1488
        loadTestsFromTestCase(Test_scandat),
1489
        loadTestsFromTestCase(Test_delete_old_backups),
1490
        loadTestsFromTestCase(Test_do_full_backup),
1491
        loadTestsFromTestCase(Test_do_incremental_backup),
1492
        # unittest.makeSuite(Test_do_backup),  #TODO
1493
        loadTestsFromTestCase(Test_do_full_recover),
1494
        loadTestsFromTestCase(Test_do_incremental_recover),
1495
        loadTestsFromTestCase(Test_do_verify),
1496
        # N.B.:  this test take forever to run (~40sec on a fast laptop),
1497
        # *and* it is non-deterministic.
1498
        loadTestsFromTestCase(MonteCarloTests),
1499
    ])
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc