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 random import choice, randint
15 from shutil import copytree, rmtree
16 from sys import stderr
17 from time import localtime, strftime, time
18 import os, random, sys
20 script_home = abspath(dirname(sys.argv[0] or '.'))
21 sys.path[:0] = [abspath(script_home + '/../lib'), abspath(script_home + '/..')]
23 bup_cmd = top + '/bup'
25 from buptest import exc, exo, test_tempdir
26 from wvtest import wvfail, wvpass, wvpasseq, wvpassne, wvstart
28 from bup.helpers import partition, period_as_secs, readpipe
32 return exo((bup_cmd,) + args)[0]
35 return exc((bup_cmd,) + args)
37 def create_older_random_saves(n, start_utc, end_utc):
38 with open('foo', 'w') as f:
40 exc(['git', 'add', 'foo'])
43 utcs.add(randint(start_utc, end_utc))
46 with open('foo', 'w') as f:
47 f.write(str(utc) + '\n')
48 exc(['git', 'commit', '--date', str(utc), '-qam', str(utc)])
49 exc(['git', 'gc', '--aggressive'])
52 # There is corresponding code in bup for some of this, but the
53 # computation method is different here, in part so that the test can
54 # provide a more effective cross-check.
56 period_kinds = ['all', 'dailies', 'monthlies', 'yearlies']
57 period_scale = {'s': 1,
61 'w': 60 * 60 * 24 * 7,
62 'm': 60 * 60 * 24 * 31,
63 'y': 60 * 60 * 24 * 366}
64 period_scale_kinds = period_scale.keys()
66 def expected_retentions(utcs, utc_start, spec):
69 utcs = sorted(utcs, reverse=True)
70 period_start = dict(spec)
71 for kind, duration in period_start.iteritems():
72 period_start[kind] = utc_start - period_as_secs(duration)
73 period_start = defaultdict(lambda: float('inf'), period_start)
75 all = list(takewhile(lambda x: x >= period_start['all'], utcs))
76 utcs = list(dropwhile(lambda x: x >= period_start['all'], utcs))
78 matches = takewhile(lambda x: x >= period_start['dailies'], utcs)
79 dailies = [max(day_utcs) for yday, day_utcs
80 in groupby(matches, lambda x: localtime(x).tm_yday)]
81 utcs = list(dropwhile(lambda x: x >= period_start['dailies'], utcs))
83 matches = takewhile(lambda x: x >= period_start['monthlies'], utcs)
84 monthlies = [max(month_utcs) for month, month_utcs
85 in groupby(matches, lambda x: localtime(x).tm_mon)]
86 utcs = dropwhile(lambda x: x >= period_start['monthlies'], utcs)
88 matches = takewhile(lambda x: x >= period_start['yearlies'], utcs)
89 yearlies = [max(year_utcs) for year, year_utcs
90 in groupby(matches, lambda x: localtime(x).tm_year)]
92 return chain(all, dailies, monthlies, yearlies)
94 def period_spec(start_utc, end_utc):
95 global period_kinds, period_scale, period_scale_kinds
97 desired_specs = randint(1, 2 * len(period_kinds))
98 assert(desired_specs >= 1) # At least one --keep argument is required
99 while len(result) < desired_specs:
101 if randint(1, 100) <= 5:
104 assert(end_utc > start_utc)
105 period_secs = randint(1, end_utc - start_utc)
106 scale = choice(period_scale_kinds)
107 mag = int(float(period_secs) / period_scale[scale])
109 period = str(mag) + scale
111 result += [(choice(period_kinds), period)]
114 def unique_period_specs(n, start_utc, end_utc):
116 while len(invocations) < n:
117 invocations.add(period_spec(start_utc, end_utc))
118 return tuple(invocations)
120 def period_spec_to_period_args(spec):
121 return tuple(chain(*(('--keep-' + kind + '-for', period)
122 for kind, period in spec)))
124 def result_diffline(x):
125 return str(x) + strftime(' %Y-%m-%d-%H%M%S', localtime(x)) + '\n'
127 def check_prune_result(expected):
128 actual = sorted([int(x)
129 for x in exo(['git', 'log',
130 '--pretty=format:%at'])[0].splitlines()])
131 if expected != actual:
133 print('ex:', x, strftime('%Y-%m-%d-%H%M%S', localtime(x)),
135 for line in unified_diff([result_diffline(x) for x in expected],
136 [result_diffline(x) for x in actual],
137 fromfile='expected', tofile='actual'):
138 sys.stderr.write(line)
139 wvpass(expected == actual)
142 environ['GIT_AUTHOR_NAME'] = 'bup test'
143 environ['GIT_COMMITTER_NAME'] = 'bup test'
144 environ['GIT_AUTHOR_EMAIL'] = 'bup@a425bc70a02811e49bdf73ee56450e6f'
145 environ['GIT_COMMITTER_EMAIL'] = 'bup@a425bc70a02811e49bdf73ee56450e6f'
147 seed = int(environ.get('BUP_TEST_SEED', time()))
149 print('random seed:', seed, file=stderr)
151 save_population = int(environ.get('BUP_TEST_PRUNE_OLDER_SAVES', 2000))
152 prune_cycles = int(environ.get('BUP_TEST_PRUNE_OLDER_CYCLES', 20))
153 prune_gc_cycles = int(environ.get('BUP_TEST_PRUNE_OLDER_GC_CYCLES', 10))
155 with test_tempdir('prune-older-') as tmpdir:
156 environ['BUP_DIR'] = tmpdir + '/work/.git'
157 environ['GIT_DIR'] = tmpdir + '/work/.git'
159 three_years_ago = now - (60 * 60 * 24 * 366 * 3)
161 exc(['git', 'init', 'work'])
163 wvstart('generating ' + str(save_population) + ' random saves')
164 chdir(tmpdir + '/work')
165 save_utcs = create_older_random_saves(save_population, three_years_ago, now)
167 test_set_hash = exo(['git', 'show-ref', '-s', 'master'])[0].rstrip()
168 ls_saves = bup('ls', 'master').splitlines()
169 wvpasseq(save_population + 1, len(ls_saves))
171 wvstart('ensure everything kept, if no keep arguments')
172 exc(['git', 'reset', '--hard', test_set_hash])
173 _, errmsg, proc = exo((bup_cmd,
174 'prune-older', '-v', '--unsafe', '--no-gc',
177 stdout=False, stderr=True, check=False)
178 wvpassne(proc.returncode, 0)
179 wvpass('at least one keep argument is required' in errmsg)
180 check_prune_result(save_utcs)
183 wvstart('running %d generative no-gc tests on %d saves' % (prune_cycles,
185 for spec in unique_period_specs(prune_cycles,
186 # Make it more likely we'll have
187 # some outside the save range.
188 three_years_ago - period_scale['m'],
190 exc(['git', 'reset', '--hard', test_set_hash])
191 expected = sorted(expected_retentions(save_utcs, now, spec))
193 'prune-older', '-v', '--unsafe', '--no-gc', '--wrt', str(now)) \
194 + period_spec_to_period_args(spec) \
196 check_prune_result(expected)
199 # More expensive because we have to recreate the repo each time
200 wvstart('running %d generative gc tests on %d saves' % (prune_gc_cycles,
202 exc(['git', 'reset', '--hard', test_set_hash])
203 copytree('work/.git', 'clean-test-repo', symlinks=True)
204 for spec in unique_period_specs(prune_gc_cycles,
205 # Make it more likely we'll have
206 # some outside the save range.
207 three_years_ago - period_scale['m'],
210 copytree('clean-test-repo', 'work/.git')
211 expected = sorted(expected_retentions(save_utcs, now, spec))
213 'prune-older', '-v', '--unsafe', '--wrt', str(now)) \
214 + period_spec_to_period_args(spec) \
216 check_prune_result(expected)