3 bup_python="$(dirname "$0")/../cmd/bup-python" || exit $?
4 exec "$bup_python" "$0" ${1+"$@"}
8 from __future__ import print_function
9 from collections import defaultdict
10 from difflib import unified_diff
11 from itertools import chain, dropwhile, groupby, takewhile
12 from os import environ, chdir
13 from os.path import abspath, dirname
14 from pipes import quote
15 from random import choice, randint
16 from shutil import copytree, rmtree
17 from subprocess import PIPE, Popen, check_call
18 from sys import stderr
19 from time import localtime, strftime, time
20 import os, random, sys
22 script_home = abspath(dirname(sys.argv[0] or '.'))
23 sys.path[:0] = [abspath(script_home + '/../lib'), abspath(script_home + '/..')]
25 bup_cmd = top + '/bup'
27 from buptest import test_tempdir
28 from wvtest import wvfail, wvpass, wvpasseq, wvpassne, wvstart
30 from bup.helpers import partition, period_as_secs, readpipe
34 if isinstance(cmd, basestring):
35 print(cmd, file=stderr)
37 print(' '.join(map(quote, cmd)), file=stderr)
39 def exc(cmd, shell=False):
41 check_call(cmd, shell=shell)
43 def exo(cmd, stdin=None, stdout=True, stderr=False, shell=False, check=True):
47 stdout=(PIPE if stdout else None),
50 out, err = p.communicate()
51 if check and p.returncode != 0:
52 raise Exception('subprocess %r failed with status %d, stderr: %r'
53 % (' '.join(argv), p.returncode, err))
57 return exo((bup_cmd,) + args)[0]
60 return exc((bup_cmd,) + args)
62 def create_older_random_saves(n, start_utc, end_utc):
63 with open('foo', 'w') as f:
65 exc(['git', 'add', 'foo'])
66 utcs = sorted(randint(start_utc, end_utc) for x in xrange(n))
68 with open('foo', 'w') as f:
69 f.write(str(utc) + '\n')
70 exc(['git', 'commit', '--date', str(utc), '-qam', str(utc)])
71 exc(['git', 'gc', '--aggressive'])
74 # There is corresponding code in bup for some of this, but the
75 # computation method is different here, in part so that the test can
76 # provide a more effective cross-check.
78 period_kinds = ['all', 'dailies', 'monthlies', 'yearlies']
79 period_scale = {'s': 1,
83 'w': 60 * 60 * 24 * 7,
84 'm': 60 * 60 * 24 * 31,
85 'y': 60 * 60 * 24 * 366}
86 period_scale_kinds = period_scale.keys()
88 def expected_retentions(utcs, utc_start, spec):
91 utcs = sorted(utcs, reverse=True)
92 period_start = dict(spec)
93 for kind, duration in period_start.iteritems():
94 period_start[kind] = utc_start - period_as_secs(duration)
95 period_start = defaultdict(lambda: float('inf'), period_start)
97 all = list(takewhile(lambda x: x >= period_start['all'], utcs))
98 utcs = list(dropwhile(lambda x: x >= period_start['all'], utcs))
100 matches = takewhile(lambda x: x >= period_start['dailies'], utcs)
101 dailies = [min(day_utcs) for yday, day_utcs
102 in groupby(matches, lambda x: localtime(x).tm_yday)]
103 utcs = list(dropwhile(lambda x: x >= period_start['dailies'], utcs))
105 matches = takewhile(lambda x: x >= period_start['monthlies'], utcs)
106 monthlies = [min(month_utcs) for month, month_utcs
107 in groupby(matches, lambda x: localtime(x).tm_mon)]
108 utcs = dropwhile(lambda x: x >= period_start['monthlies'], utcs)
110 matches = takewhile(lambda x: x >= period_start['yearlies'], utcs)
111 yearlies = [min(year_utcs) for year, year_utcs
112 in groupby(matches, lambda x: localtime(x).tm_year)]
114 return chain(all, dailies, monthlies, yearlies)
116 def period_spec(start_utc, end_utc):
117 global period_kinds, period_scale, period_scale_kinds
119 desired_specs = randint(1, 2 * len(period_kinds))
120 assert(desired_specs >= 1) # At least one --keep argument is required
121 while len(result) < desired_specs:
123 if randint(1, 100) <= 5:
126 assert(end_utc > start_utc)
127 period_secs = randint(1, end_utc - start_utc)
128 scale = choice(period_scale_kinds)
129 mag = int(float(period_secs) / period_scale[scale])
131 period = str(mag) + scale
133 result += [(choice(period_kinds), period)]
136 def unique_period_specs(n, start_utc, end_utc):
138 while len(invocations) < n:
139 invocations.add(period_spec(start_utc, end_utc))
140 return tuple(invocations)
142 def period_spec_to_period_args(spec):
143 return tuple(chain(*(('--keep-' + kind + '-for', period)
144 for kind, period in spec)))
146 def result_diffline(x):
147 return str(x) + strftime(' %Y-%m-%d-%H%M%S', localtime(x)) + '\n'
149 def check_prune_result(expected):
150 actual = sorted([int(x)
151 for x in exo(['git', 'log',
152 '--pretty=format:%at'])[0].splitlines()])
153 if expected != actual:
155 print('ex:', x, strftime('%Y-%m-%d-%H%M%S', localtime(x)),
157 for line in unified_diff([result_diffline(x) for x in expected],
158 [result_diffline(x) for x in actual],
159 fromfile='expected', tofile='actual'):
160 sys.stderr.write(line)
161 wvpass(expected == actual)
164 seed = int(environ.get('BUP_TEST_SEED', time()))
166 print('random seed:', seed, file=stderr)
168 save_population = int(environ.get('BUP_TEST_PRUNE_OLDER_SAVES', 2000))
169 prune_cycles = int(environ.get('BUP_TEST_PRUNE_OLDER_CYCLES', 20))
170 prune_gc_cycles = int(environ.get('BUP_TEST_PRUNE_OLDER_GC_CYCLES', 10))
172 with test_tempdir('prune-older-') as tmpdir:
173 environ['BUP_DIR'] = tmpdir + '/work/.git'
174 environ['GIT_DIR'] = tmpdir + '/work/.git'
176 three_years_ago = now - (60 * 60 * 24 * 366 * 3)
178 exc(['git', 'init', 'work'])
180 wvstart('generating ' + str(save_population) + ' random saves')
181 chdir(tmpdir + '/work')
182 save_utcs = create_older_random_saves(save_population, three_years_ago, now)
184 test_set_hash = exo(['git', 'show-ref', '-s', 'master'])[0].rstrip()
185 ls_saves = bup('ls', 'master').splitlines()
186 wvpasseq(save_population + 1, len(ls_saves))
188 wvstart('ensure everything kept, if no keep arguments')
189 exc(['git', 'reset', '--hard', test_set_hash])
190 _, errmsg, proc = exo((bup_cmd,
191 'prune-older', '-v', '--unsafe', '--no-gc',
194 stdout=False, stderr=True, check=False)
195 wvpassne(proc.returncode, 0)
196 wvpass('at least one keep argument is required' in errmsg)
197 check_prune_result(save_utcs)
200 wvstart('running %d generative no-gc tests on %d saves' % (prune_cycles,
202 for spec in unique_period_specs(prune_cycles,
203 # Make it more likely we'll have
204 # some outside the save range.
205 three_years_ago - period_scale['m'],
207 exc(['git', 'reset', '--hard', test_set_hash])
208 expected = sorted(expected_retentions(save_utcs, now, spec))
210 'prune-older', '-v', '--unsafe', '--no-gc', '--wrt', str(now)) \
211 + period_spec_to_period_args(spec) \
213 check_prune_result(expected)
216 # More expensive because we have to recreate the repo each time
217 wvstart('running %d generative gc tests on %d saves' % (prune_gc_cycles,
219 exc(['git', 'reset', '--hard', test_set_hash])
220 copytree('work/.git', 'clean-test-repo', symlinks=True)
221 for spec in unique_period_specs(prune_gc_cycles,
222 # Make it more likely we'll have
223 # some outside the save range.
224 three_years_ago - period_scale['m'],
227 copytree('clean-test-repo', 'work/.git')
228 expected = sorted(expected_retentions(save_utcs, now, spec))
230 'prune-older', '-v', '--unsafe', '--wrt', str(now)) \
231 + period_spec_to_period_args(spec) \
233 check_prune_result(expected)