3 bup_python="$(dirname "$0")/../cmd/bup-python" || exit $?
4 exec "$bup_python" "$0" ${1+"$@"}
8 from __future__ import absolute_import, print_function
9 from collections import defaultdict
10 from difflib import unified_diff
11 from itertools import chain, dropwhile, groupby, takewhile
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 # For buptest, wvtest, ...
22 sys.path[:0] = (abspath(os.path.dirname(__file__) + '/..'),)
24 from buptest import ex, exo, test_tempdir
25 from wvtest import wvfail, wvpass, wvpasseq, wvpassne, wvstart
27 from bup import compat
28 from bup.compat import environ
29 from bup.helpers import partition, period_as_secs, readpipe
33 def create_older_random_saves(n, start_utc, end_utc):
34 with open(b'foo', 'wb') as f:
36 ex([b'git', b'add', b'foo'])
39 utcs.add(randint(start_utc, end_utc))
42 with open(b'foo', 'wb') as f:
43 f.write(b'%d\n' % utc)
44 ex([b'git', b'commit', b'--date', b'%d' % utc, b'-qam', b'%d' % utc])
45 ex([b'git', b'gc', b'--aggressive'])
48 # There is corresponding code in bup for some of this, but the
49 # computation method is different here, in part so that the test can
50 # provide a more effective cross-check.
52 period_kinds = [b'all', b'dailies', b'monthlies', b'yearlies']
53 period_scale = {b's': 1,
57 b'w': 60 * 60 * 24 * 7,
58 b'm': 60 * 60 * 24 * 31,
59 b'y': 60 * 60 * 24 * 366}
60 period_scale_kinds = list(period_scale.keys())
62 def expected_retentions(utcs, utc_start, spec):
65 utcs = sorted(utcs, reverse=True)
66 period_start = dict(spec)
67 for kind, duration in compat.items(period_start):
68 period_start[kind] = utc_start - period_as_secs(duration)
69 period_start = defaultdict(lambda: float('inf'), period_start)
71 all = list(takewhile(lambda x: x >= period_start[b'all'], utcs))
72 utcs = list(dropwhile(lambda x: x >= period_start[b'all'], utcs))
74 matches = takewhile(lambda x: x >= period_start[b'dailies'], utcs)
75 dailies = [max(day_utcs) for yday, day_utcs
76 in groupby(matches, lambda x: localtime(x).tm_yday)]
77 utcs = list(dropwhile(lambda x: x >= period_start[b'dailies'], utcs))
79 matches = takewhile(lambda x: x >= period_start[b'monthlies'], utcs)
80 monthlies = [max(month_utcs) for month, month_utcs
81 in groupby(matches, lambda x: localtime(x).tm_mon)]
82 utcs = dropwhile(lambda x: x >= period_start[b'monthlies'], utcs)
84 matches = takewhile(lambda x: x >= period_start[b'yearlies'], utcs)
85 yearlies = [max(year_utcs) for year, year_utcs
86 in groupby(matches, lambda x: localtime(x).tm_year)]
88 return chain(all, dailies, monthlies, yearlies)
90 def period_spec(start_utc, end_utc):
91 global period_kinds, period_scale, period_scale_kinds
93 desired_specs = randint(1, 2 * len(period_kinds))
94 assert(desired_specs >= 1) # At least one --keep argument is required
95 while len(result) < desired_specs:
97 if randint(1, 100) <= 5:
100 assert(end_utc > start_utc)
101 period_secs = randint(1, end_utc - start_utc)
102 scale = choice(period_scale_kinds)
103 mag = int(float(period_secs) / period_scale[scale])
105 period = (b'%d' % mag) + scale
107 result += [(choice(period_kinds), period)]
110 def unique_period_specs(n, start_utc, end_utc):
112 while len(invocations) < n:
113 invocations.add(period_spec(start_utc, end_utc))
114 return tuple(invocations)
116 def period_spec_to_period_args(spec):
117 return tuple(chain(*((b'--keep-' + kind + b'-for', period)
118 for kind, period in spec)))
120 def result_diffline(x):
122 % (x, strftime(' %Y-%m-%d-%H%M%S', localtime(x)).encode('ascii')))
124 def check_prune_result(expected):
125 actual = sorted([int(x)
126 for x in exo([b'git', b'log',
127 b'--pretty=format:%at']).out.splitlines()])
128 if expected != actual:
130 print('ex:', x, strftime('%Y-%m-%d-%H%M%S', localtime(x)),
132 for line in unified_diff([result_diffline(x) for x in expected],
133 [result_diffline(x) for x in actual],
134 fromfile='expected', tofile='actual'):
135 sys.stderr.write(line)
136 wvpass(expected == actual)
139 environ[b'GIT_AUTHOR_NAME'] = b'bup test'
140 environ[b'GIT_COMMITTER_NAME'] = b'bup test'
141 environ[b'GIT_AUTHOR_EMAIL'] = b'bup@a425bc70a02811e49bdf73ee56450e6f'
142 environ[b'GIT_COMMITTER_EMAIL'] = b'bup@a425bc70a02811e49bdf73ee56450e6f'
144 seed = int(environ.get(b'BUP_TEST_SEED', time()))
146 print('random seed:', seed, file=stderr)
148 save_population = int(environ.get(b'BUP_TEST_PRUNE_OLDER_SAVES', 2000))
149 prune_cycles = int(environ.get(b'BUP_TEST_PRUNE_OLDER_CYCLES', 20))
150 prune_gc_cycles = int(environ.get(b'BUP_TEST_PRUNE_OLDER_GC_CYCLES', 10))
152 bup_cmd = bup.path.exe()
154 with test_tempdir(b'prune-older-') as tmpdir:
155 environ[b'BUP_DIR'] = tmpdir + b'/work/.git'
156 environ[b'GIT_DIR'] = tmpdir + b'/work/.git'
158 three_years_ago = now - (60 * 60 * 24 * 366 * 3)
160 ex([b'git', b'init', b'work'])
161 ex([b'git', b'config', b'gc.autoDetach', b'false'])
163 wvstart('generating ' + str(save_population) + ' random saves')
164 chdir(tmpdir + b'/work')
165 save_utcs = create_older_random_saves(save_population, three_years_ago, now)
167 test_set_hash = exo([b'git', b'show-ref', b'-s', b'master']).out.rstrip()
168 ls_saves = exo((bup_cmd, b'ls', b'master')).out.splitlines()
169 wvpasseq(save_population + 1, len(ls_saves))
171 wvstart('ensure everything kept, if no keep arguments')
172 ex([b'git', b'reset', b'--hard', test_set_hash])
174 b'prune-older', b'-v', b'--unsafe', b'--no-gc',
175 b'--wrt', b'%d' % now) \
177 stdout=None, stderr=PIPE, check=False)
179 wvpass(b'at least one keep argument is required' in proc.err)
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[b'm'],
190 ex([b'git', b'reset', b'--hard', test_set_hash])
191 expected = sorted(expected_retentions(save_utcs, now, spec))
193 b'prune-older', b'-v', b'--unsafe', b'--no-gc', b'--wrt',
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 ex([b'git', b'reset', b'--hard', test_set_hash])
204 copytree(b'work/.git', b'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[b'm'],
211 copytree(b'clean-test-repo', b'work/.git')
212 expected = sorted(expected_retentions(save_utcs, now, spec))
214 b'prune-older', b'-v', b'--unsafe', b'--wrt', b'%d' % now) \
215 + period_spec_to_period_args(spec) \
217 check_prune_result(expected)