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 subprocess import PIPE
17 from sys import stderr
18 from time import localtime, strftime, time
19 import os, random, sys
21 script_home = abspath(dirname(sys.argv[0] or '.'))
22 sys.path[:0] = [abspath(script_home + '/../lib'), abspath(script_home + '/..')]
24 bup_cmd = top + '/bup'
26 from buptest import exc, exo, test_tempdir
27 from wvtest import wvfail, wvpass, wvpasseq, wvpassne, wvstart
29 from bup.helpers import partition, period_as_secs, readpipe
33 return exo((bup_cmd,) + args).out
36 return exc((bup_cmd,) + args)
38 def create_older_random_saves(n, start_utc, end_utc):
39 with open('foo', 'w') as f:
41 exc(['git', 'add', 'foo'])
44 utcs.add(randint(start_utc, end_utc))
47 with open('foo', 'w') as f:
48 f.write(str(utc) + '\n')
49 exc(['git', 'commit', '--date', str(utc), '-qam', str(utc)])
50 exc(['git', 'gc', '--aggressive'])
53 # There is corresponding code in bup for some of this, but the
54 # computation method is different here, in part so that the test can
55 # provide a more effective cross-check.
57 period_kinds = ['all', 'dailies', 'monthlies', 'yearlies']
58 period_scale = {'s': 1,
62 'w': 60 * 60 * 24 * 7,
63 'm': 60 * 60 * 24 * 31,
64 'y': 60 * 60 * 24 * 366}
65 period_scale_kinds = period_scale.keys()
67 def expected_retentions(utcs, utc_start, spec):
70 utcs = sorted(utcs, reverse=True)
71 period_start = dict(spec)
72 for kind, duration in period_start.iteritems():
73 period_start[kind] = utc_start - period_as_secs(duration)
74 period_start = defaultdict(lambda: float('inf'), period_start)
76 all = list(takewhile(lambda x: x >= period_start['all'], utcs))
77 utcs = list(dropwhile(lambda x: x >= period_start['all'], utcs))
79 matches = takewhile(lambda x: x >= period_start['dailies'], utcs)
80 dailies = [max(day_utcs) for yday, day_utcs
81 in groupby(matches, lambda x: localtime(x).tm_yday)]
82 utcs = list(dropwhile(lambda x: x >= period_start['dailies'], utcs))
84 matches = takewhile(lambda x: x >= period_start['monthlies'], utcs)
85 monthlies = [max(month_utcs) for month, month_utcs
86 in groupby(matches, lambda x: localtime(x).tm_mon)]
87 utcs = dropwhile(lambda x: x >= period_start['monthlies'], utcs)
89 matches = takewhile(lambda x: x >= period_start['yearlies'], utcs)
90 yearlies = [max(year_utcs) for year, year_utcs
91 in groupby(matches, lambda x: localtime(x).tm_year)]
93 return chain(all, dailies, monthlies, yearlies)
95 def period_spec(start_utc, end_utc):
96 global period_kinds, period_scale, period_scale_kinds
98 desired_specs = randint(1, 2 * len(period_kinds))
99 assert(desired_specs >= 1) # At least one --keep argument is required
100 while len(result) < desired_specs:
102 if randint(1, 100) <= 5:
105 assert(end_utc > start_utc)
106 period_secs = randint(1, end_utc - start_utc)
107 scale = choice(period_scale_kinds)
108 mag = int(float(period_secs) / period_scale[scale])
110 period = str(mag) + scale
112 result += [(choice(period_kinds), period)]
115 def unique_period_specs(n, start_utc, end_utc):
117 while len(invocations) < n:
118 invocations.add(period_spec(start_utc, end_utc))
119 return tuple(invocations)
121 def period_spec_to_period_args(spec):
122 return tuple(chain(*(('--keep-' + kind + '-for', period)
123 for kind, period in spec)))
125 def result_diffline(x):
126 return str(x) + strftime(' %Y-%m-%d-%H%M%S', localtime(x)) + '\n'
128 def check_prune_result(expected):
129 actual = sorted([int(x)
130 for x in exo(['git', 'log',
131 '--pretty=format:%at']).out.splitlines()])
132 if expected != actual:
134 print('ex:', x, strftime('%Y-%m-%d-%H%M%S', localtime(x)),
136 for line in unified_diff([result_diffline(x) for x in expected],
137 [result_diffline(x) for x in actual],
138 fromfile='expected', tofile='actual'):
139 sys.stderr.write(line)
140 wvpass(expected == actual)
143 environ['GIT_AUTHOR_NAME'] = 'bup test'
144 environ['GIT_COMMITTER_NAME'] = 'bup test'
145 environ['GIT_AUTHOR_EMAIL'] = 'bup@a425bc70a02811e49bdf73ee56450e6f'
146 environ['GIT_COMMITTER_EMAIL'] = 'bup@a425bc70a02811e49bdf73ee56450e6f'
148 seed = int(environ.get('BUP_TEST_SEED', time()))
150 print('random seed:', seed, file=stderr)
152 save_population = int(environ.get('BUP_TEST_PRUNE_OLDER_SAVES', 2000))
153 prune_cycles = int(environ.get('BUP_TEST_PRUNE_OLDER_CYCLES', 20))
154 prune_gc_cycles = int(environ.get('BUP_TEST_PRUNE_OLDER_GC_CYCLES', 10))
156 with test_tempdir('prune-older-') as tmpdir:
157 environ['BUP_DIR'] = tmpdir + '/work/.git'
158 environ['GIT_DIR'] = tmpdir + '/work/.git'
160 three_years_ago = now - (60 * 60 * 24 * 366 * 3)
162 exc(['git', 'init', 'work'])
164 wvstart('generating ' + str(save_population) + ' random saves')
165 chdir(tmpdir + '/work')
166 save_utcs = create_older_random_saves(save_population, three_years_ago, now)
168 test_set_hash = exo(['git', 'show-ref', '-s', 'master']).out.rstrip()
169 ls_saves = bup('ls', 'master').splitlines()
170 wvpasseq(save_population + 1, len(ls_saves))
172 wvstart('ensure everything kept, if no keep arguments')
173 exc(['git', 'reset', '--hard', test_set_hash])
175 'prune-older', '-v', '--unsafe', '--no-gc',
178 stdout=None, stderr=PIPE, check=False)
180 wvpass('at least one keep argument is required' in proc.err)
181 check_prune_result(save_utcs)
184 wvstart('running %d generative no-gc tests on %d saves' % (prune_cycles,
186 for spec in unique_period_specs(prune_cycles,
187 # Make it more likely we'll have
188 # some outside the save range.
189 three_years_ago - period_scale['m'],
191 exc(['git', 'reset', '--hard', test_set_hash])
192 expected = sorted(expected_retentions(save_utcs, now, spec))
194 'prune-older', '-v', '--unsafe', '--no-gc', '--wrt', str(now)) \
195 + period_spec_to_period_args(spec) \
197 check_prune_result(expected)
200 # More expensive because we have to recreate the repo each time
201 wvstart('running %d generative gc tests on %d saves' % (prune_gc_cycles,
203 exc(['git', 'reset', '--hard', test_set_hash])
204 copytree('work/.git', 'clean-test-repo', symlinks=True)
205 for spec in unique_period_specs(prune_gc_cycles,
206 # Make it more likely we'll have
207 # some outside the save range.
208 three_years_ago - period_scale['m'],
211 copytree('clean-test-repo', 'work/.git')
212 expected = sorted(expected_retentions(save_utcs, now, spec))
214 'prune-older', '-v', '--unsafe', '--wrt', str(now)) \
215 + period_spec_to_period_args(spec) \
217 check_prune_result(expected)