Skip to content

Commit 138afbe

Browse files
Merge pull request #8863 from ThomasWaldmann/split-helpers-tests
Split helpers tests
2 parents 34cc0b2 + 881eaca commit 138afbe

File tree

15 files changed

+1707
-1648
lines changed

15 files changed

+1707
-1648
lines changed

src/borg/testsuite/archiver/prune_cmd_test.py

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import re
2-
from datetime import datetime
2+
from datetime import datetime, timezone, timedelta
3+
4+
import pytest
35

46
from ...constants import * # NOQA
7+
from ...archiver.prune_cmd import prune_split, prune_within
58
from . import cmd, RK_ENCRYPTION, src_dir, generate_archiver_tests
9+
from ...helpers import interval
610

711
pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA
812

@@ -257,3 +261,142 @@ def test_prune_ignore_protected(archivers, request):
257261
output = cmd(archiver, "repo-list")
258262
assert "archive1" in output # @PROT protected archive1 from deletion
259263
assert "archive3" in output # last one
264+
265+
266+
class MockArchive:
267+
def __init__(self, ts, id):
268+
self.ts = ts
269+
self.id = id
270+
271+
def __repr__(self):
272+
return f"{self.id}: {self.ts.isoformat()}"
273+
274+
275+
# This is the local timezone of the system running the tests.
276+
# We need this e.g. to construct archive timestamps for the prune tests,
277+
# because borg prune operates in the local timezone (it first converts the
278+
# archive timestamp to the local timezone). So, if we want the y/m/d/h/m/s
279+
# values which prune uses to be exactly the ones we give [and NOT shift them
280+
# by tzoffset], we need to give the timestamps in the same local timezone.
281+
# Please note that the timestamps in a real borg archive or manifest are
282+
# stored in UTC timezone.
283+
local_tz = datetime.now(tz=timezone.utc).astimezone(tz=None).tzinfo
284+
285+
286+
def test_prune_within():
287+
def subset(lst, indices):
288+
return {lst[i] for i in indices}
289+
290+
def dotest(test_archives, within, indices):
291+
for ta in test_archives, reversed(test_archives):
292+
kept_because = {}
293+
keep = prune_within(ta, interval(within), kept_because)
294+
assert set(keep) == subset(test_archives, indices)
295+
assert all("within" == kept_because[a.id][0] for a in keep)
296+
297+
# 1 minute, 1.5 hours, 2.5 hours, 3.5 hours, 25 hours, 49 hours
298+
test_offsets = [60, 90 * 60, 150 * 60, 210 * 60, 25 * 60 * 60, 49 * 60 * 60]
299+
now = datetime.now(timezone.utc)
300+
test_dates = [now - timedelta(seconds=s) for s in test_offsets]
301+
test_archives = [MockArchive(date, i) for i, date in enumerate(test_dates)]
302+
303+
dotest(test_archives, "15S", [])
304+
dotest(test_archives, "2M", [0])
305+
dotest(test_archives, "1H", [0])
306+
dotest(test_archives, "2H", [0, 1])
307+
dotest(test_archives, "3H", [0, 1, 2])
308+
dotest(test_archives, "24H", [0, 1, 2, 3])
309+
dotest(test_archives, "26H", [0, 1, 2, 3, 4])
310+
dotest(test_archives, "2d", [0, 1, 2, 3, 4])
311+
dotest(test_archives, "50H", [0, 1, 2, 3, 4, 5])
312+
dotest(test_archives, "3d", [0, 1, 2, 3, 4, 5])
313+
dotest(test_archives, "1w", [0, 1, 2, 3, 4, 5])
314+
dotest(test_archives, "1m", [0, 1, 2, 3, 4, 5])
315+
dotest(test_archives, "1y", [0, 1, 2, 3, 4, 5])
316+
317+
318+
@pytest.mark.parametrize(
319+
"rule,num_to_keep,expected_ids",
320+
[
321+
("yearly", 3, (13, 2, 1)),
322+
("monthly", 3, (13, 8, 4)),
323+
("weekly", 2, (13, 8)),
324+
("daily", 3, (13, 8, 7)),
325+
("hourly", 3, (13, 10, 8)),
326+
("minutely", 3, (13, 10, 9)),
327+
("secondly", 4, (13, 12, 11, 10)),
328+
("daily", 0, []),
329+
],
330+
)
331+
def test_prune_split(rule, num_to_keep, expected_ids):
332+
def subset(lst, ids):
333+
return {i for i in lst if i.id in ids}
334+
335+
archives = [
336+
# years apart
337+
MockArchive(datetime(2015, 1, 1, 10, 0, 0, tzinfo=local_tz), 1),
338+
MockArchive(datetime(2016, 1, 1, 10, 0, 0, tzinfo=local_tz), 2),
339+
MockArchive(datetime(2017, 1, 1, 10, 0, 0, tzinfo=local_tz), 3),
340+
# months apart
341+
MockArchive(datetime(2017, 2, 1, 10, 0, 0, tzinfo=local_tz), 4),
342+
MockArchive(datetime(2017, 3, 1, 10, 0, 0, tzinfo=local_tz), 5),
343+
# days apart
344+
MockArchive(datetime(2017, 3, 2, 10, 0, 0, tzinfo=local_tz), 6),
345+
MockArchive(datetime(2017, 3, 3, 10, 0, 0, tzinfo=local_tz), 7),
346+
MockArchive(datetime(2017, 3, 4, 10, 0, 0, tzinfo=local_tz), 8),
347+
# minutes apart
348+
MockArchive(datetime(2017, 10, 1, 9, 45, 0, tzinfo=local_tz), 9),
349+
MockArchive(datetime(2017, 10, 1, 9, 55, 0, tzinfo=local_tz), 10),
350+
# seconds apart
351+
MockArchive(datetime(2017, 10, 1, 10, 0, 1, tzinfo=local_tz), 11),
352+
MockArchive(datetime(2017, 10, 1, 10, 0, 3, tzinfo=local_tz), 12),
353+
MockArchive(datetime(2017, 10, 1, 10, 0, 5, tzinfo=local_tz), 13),
354+
]
355+
kept_because = {}
356+
keep = prune_split(archives, rule, num_to_keep, kept_because)
357+
358+
assert set(keep) == subset(archives, expected_ids)
359+
for item in keep:
360+
assert kept_because[item.id][0] == rule
361+
362+
363+
def test_prune_split_keep_oldest():
364+
def subset(lst, ids):
365+
return {i for i in lst if i.id in ids}
366+
367+
archives = [
368+
# oldest backup, but not last in its year
369+
MockArchive(datetime(2018, 1, 1, 10, 0, 0, tzinfo=local_tz), 1),
370+
# an interim backup
371+
MockArchive(datetime(2018, 12, 30, 10, 0, 0, tzinfo=local_tz), 2),
372+
# year-end backups
373+
MockArchive(datetime(2018, 12, 31, 10, 0, 0, tzinfo=local_tz), 3),
374+
MockArchive(datetime(2019, 12, 31, 10, 0, 0, tzinfo=local_tz), 4),
375+
]
376+
377+
# Keep oldest when retention target can't otherwise be met
378+
kept_because = {}
379+
keep = prune_split(archives, "yearly", 3, kept_because)
380+
381+
assert set(keep) == subset(archives, [1, 3, 4])
382+
assert kept_because[1][0] == "yearly[oldest]"
383+
assert kept_because[3][0] == "yearly"
384+
assert kept_because[4][0] == "yearly"
385+
386+
# Otherwise, prune it
387+
kept_because = {}
388+
keep = prune_split(archives, "yearly", 2, kept_because)
389+
390+
assert set(keep) == subset(archives, [3, 4])
391+
assert kept_because[3][0] == "yearly"
392+
assert kept_because[4][0] == "yearly"
393+
394+
395+
def test_prune_split_no_archives():
396+
archives = []
397+
398+
kept_because = {}
399+
keep = prune_split(archives, "yearly", 3, kept_because)
400+
401+
assert keep == []
402+
assert kept_because == {}

src/borg/testsuite/helpers/__init__.py

Whitespace-only changes.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import pytest
2+
3+
from ...constants import * # NOQA
4+
from ...helpers import classify_ec, max_ec
5+
6+
7+
@pytest.mark.parametrize(
8+
"ec_range,ec_class",
9+
(
10+
# inclusive range start, exclusive range end
11+
((0, 1), "success"),
12+
((1, 2), "warning"),
13+
((2, 3), "error"),
14+
((EXIT_ERROR_BASE, EXIT_WARNING_BASE), "error"),
15+
((EXIT_WARNING_BASE, EXIT_SIGNAL_BASE), "warning"),
16+
((EXIT_SIGNAL_BASE, 256), "signal"),
17+
),
18+
)
19+
def test_classify_ec(ec_range, ec_class):
20+
for ec in range(*ec_range):
21+
classify_ec(ec) == ec_class
22+
23+
24+
def test_ec_invalid():
25+
with pytest.raises(ValueError):
26+
classify_ec(666)
27+
with pytest.raises(ValueError):
28+
classify_ec(-1)
29+
with pytest.raises(TypeError):
30+
classify_ec(None)
31+
32+
33+
@pytest.mark.parametrize(
34+
"ec1,ec2,ec_max",
35+
(
36+
# same for modern / legacy
37+
(EXIT_SUCCESS, EXIT_SUCCESS, EXIT_SUCCESS),
38+
(EXIT_SUCCESS, EXIT_SIGNAL_BASE, EXIT_SIGNAL_BASE),
39+
# legacy exit codes
40+
(EXIT_SUCCESS, EXIT_WARNING, EXIT_WARNING),
41+
(EXIT_SUCCESS, EXIT_ERROR, EXIT_ERROR),
42+
(EXIT_WARNING, EXIT_SUCCESS, EXIT_WARNING),
43+
(EXIT_WARNING, EXIT_WARNING, EXIT_WARNING),
44+
(EXIT_WARNING, EXIT_ERROR, EXIT_ERROR),
45+
(EXIT_WARNING, EXIT_SIGNAL_BASE, EXIT_SIGNAL_BASE),
46+
(EXIT_ERROR, EXIT_SUCCESS, EXIT_ERROR),
47+
(EXIT_ERROR, EXIT_WARNING, EXIT_ERROR),
48+
(EXIT_ERROR, EXIT_ERROR, EXIT_ERROR),
49+
(EXIT_ERROR, EXIT_SIGNAL_BASE, EXIT_SIGNAL_BASE),
50+
# some modern codes
51+
(EXIT_SUCCESS, EXIT_WARNING_BASE, EXIT_WARNING_BASE),
52+
(EXIT_SUCCESS, EXIT_ERROR_BASE, EXIT_ERROR_BASE),
53+
(EXIT_WARNING_BASE, EXIT_SUCCESS, EXIT_WARNING_BASE),
54+
(EXIT_WARNING_BASE + 1, EXIT_WARNING_BASE + 2, EXIT_WARNING_BASE + 1),
55+
(EXIT_WARNING_BASE, EXIT_ERROR_BASE, EXIT_ERROR_BASE),
56+
(EXIT_WARNING_BASE, EXIT_SIGNAL_BASE, EXIT_SIGNAL_BASE),
57+
(EXIT_ERROR_BASE, EXIT_SUCCESS, EXIT_ERROR_BASE),
58+
(EXIT_ERROR_BASE, EXIT_WARNING_BASE, EXIT_ERROR_BASE),
59+
(EXIT_ERROR_BASE + 1, EXIT_ERROR_BASE + 2, EXIT_ERROR_BASE + 1),
60+
(EXIT_ERROR_BASE, EXIT_SIGNAL_BASE, EXIT_SIGNAL_BASE),
61+
),
62+
)
63+
def test_max_ec(ec1, ec2, ec_max):
64+
assert max_ec(ec1, ec2) == ec_max
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import hashlib
2+
import pytest
3+
4+
from ...helpers.datastruct import StableDict, Buffer
5+
from ...helpers import msgpack
6+
7+
8+
def test_stable_dict():
9+
d = StableDict(foo=1, bar=2, boo=3, baz=4)
10+
assert list(d.items()) == [("bar", 2), ("baz", 4), ("boo", 3), ("foo", 1)]
11+
assert hashlib.md5(msgpack.packb(d)).hexdigest() == "fc78df42cd60691b3ac3dd2a2b39903f"
12+
13+
14+
class TestBuffer:
15+
def test_type(self):
16+
buffer = Buffer(bytearray)
17+
assert isinstance(buffer.get(), bytearray)
18+
buffer = Buffer(bytes) # don't do that in practice
19+
assert isinstance(buffer.get(), bytes)
20+
21+
def test_len(self):
22+
buffer = Buffer(bytearray, size=0)
23+
b = buffer.get()
24+
assert len(buffer) == len(b) == 0
25+
buffer = Buffer(bytearray, size=1234)
26+
b = buffer.get()
27+
assert len(buffer) == len(b) == 1234
28+
29+
def test_resize(self):
30+
buffer = Buffer(bytearray, size=100)
31+
assert len(buffer) == 100
32+
b1 = buffer.get()
33+
buffer.resize(200)
34+
assert len(buffer) == 200
35+
b2 = buffer.get()
36+
assert b2 is not b1 # new, bigger buffer
37+
buffer.resize(100)
38+
assert len(buffer) >= 100
39+
b3 = buffer.get()
40+
assert b3 is b2 # still same buffer (200)
41+
buffer.resize(100, init=True)
42+
assert len(buffer) == 100 # except on init
43+
b4 = buffer.get()
44+
assert b4 is not b3 # new, smaller buffer
45+
46+
def test_limit(self):
47+
buffer = Buffer(bytearray, size=100, limit=200)
48+
buffer.resize(200)
49+
assert len(buffer) == 200
50+
with pytest.raises(Buffer.MemoryLimitExceeded):
51+
buffer.resize(201)
52+
assert len(buffer) == 200
53+
54+
def test_get(self):
55+
buffer = Buffer(bytearray, size=100, limit=200)
56+
b1 = buffer.get(50)
57+
assert len(b1) >= 50 # == 100
58+
b2 = buffer.get(100)
59+
assert len(b2) >= 100 # == 100
60+
assert b2 is b1 # did not need resizing yet
61+
b3 = buffer.get(200)
62+
assert len(b3) == 200
63+
assert b3 is not b2 # new, resized buffer
64+
with pytest.raises(Buffer.MemoryLimitExceeded):
65+
buffer.get(201) # beyond limit
66+
assert len(buffer) == 200

0 commit comments

Comments
 (0)