From db33bc44e26b2de5399eaeae3c53c4ed2b5204e4 Mon Sep 17 00:00:00 2001 From: Ken Kundert Date: Mon, 6 Jan 2025 09:15:45 -0800 Subject: [PATCH 1/9] Allow timespan to be specified with common time units https://github.com/borgbackup/borg/issues/8624 --- docs/usage/general/date-time.rst.inc | 3 +- src/borg/helpers/parseformat.py | 4 +- src/borg/helpers/time.py | 10 ++- src/borg/testsuite/archiver/check_cmd_test.py | 72 +++++++++++++----- .../testsuite/archiver/repo_list_cmd_test.py | 76 ++++++++++++++----- 5 files changed, 126 insertions(+), 39 deletions(-) diff --git a/docs/usage/general/date-time.rst.inc b/docs/usage/general/date-time.rst.inc index 58f594a97b..bf5aa73409 100644 --- a/docs/usage/general/date-time.rst.inc +++ b/docs/usage/general/date-time.rst.inc @@ -13,5 +13,6 @@ Internally, we store and process date and time as UTC. .. rubric:: TIMESPAN Some options accept a TIMESPAN parameter, which can be given as a -number of days (e.g. ``7d``) or months (e.g. ``12m``). +number of years (e.g. ``2Y``), months (e.g. ``12m``), days (e.g. ``7d``), +hours (e.g. ``8H``), minutes (e.g. ``30M``), or seconds (e.g. ``84600s``). diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 0f3f397da6..9a1ac43af9 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -579,10 +579,10 @@ def validator(text): def relative_time_marker_validator(text: str): - time_marker_regex = r"^\d+[md]$" + time_marker_regex = r"^\d+[YmdHMS]$" match = re.compile(time_marker_regex).search(text) if not match: - raise argparse.ArgumentTypeError(f"Invalid relative time marker used: {text}") + raise argparse.ArgumentTypeError(f"Invalid relative time marker used: {text}, choose from Y, m, d, H, M, S") else: return text diff --git a/src/borg/helpers/time.py b/src/borg/helpers/time.py index 5fe148796c..0dd1c42a45 100644 --- a/src/borg/helpers/time.py +++ b/src/borg/helpers/time.py @@ -119,7 +119,7 @@ def calculate_relative_offset(format_string, from_ts, earlier=False): from_ts = archive_ts_now() if format_string is not None: - offset_regex = re.compile(r"(?P\d+)(?P[md])") + offset_regex = re.compile(r"(?P\d+)(?P[YmdHMS])") match = offset_regex.search(format_string) if match: @@ -131,6 +131,14 @@ def calculate_relative_offset(format_string, from_ts, earlier=False): return from_ts + timedelta(days=offset) elif unit == "m": return offset_n_months(from_ts, offset) + elif unit == "Y": + return from_ts.replace(year=from_ts.year+offset) + elif unit == "H": + return from_ts + timedelta(seconds=offset*60*60) + elif unit == "M": + return from_ts + timedelta(seconds=offset*60) + elif unit == "S": + return from_ts + timedelta(seconds=offset) raise ValueError(f"Invalid relative ts offset format: {format_string}") diff --git a/src/borg/testsuite/archiver/check_cmd_test.py b/src/borg/testsuite/archiver/check_cmd_test.py index cc2ee31e5c..e9f189e650 100644 --- a/src/borg/testsuite/archiver/check_cmd_test.py +++ b/src/borg/testsuite/archiver/check_cmd_test.py @@ -58,32 +58,70 @@ def test_date_matching(archivers, request): shutil.rmtree(archiver.repository_path) cmd(archiver, "repo-create", RK_ENCRYPTION) - earliest_ts = "2022-11-20T23:59:59" - ts_in_between = "2022-12-18T23:59:59" - create_src_archive(archiver, "archive1", ts=earliest_ts) - create_src_archive(archiver, "archive2", ts=ts_in_between) - create_src_archive(archiver, "archive3") + create_src_archive(archiver, "archive-2022-11-20", ts="2022-11-20T23:59:59") + create_src_archive(archiver, "archive-2022-12-18", ts="2022-12-18T23:59:59") + create_src_archive(archiver, "archive-now") cmd(archiver, "check", "-v", "--archives-only", "--oldest=23e", exit_code=2) + output = cmd(archiver, "check", "-v", "--archives-only", "--oldest=1Y", exit_code=0) + assert "archive-2022-11-20" in output + assert "archive-2022-12-18" in output + assert "archive-now" not in output + + output = cmd(archiver, "check", "-v", "--archives-only", "--newest=1Y", exit_code=0) + assert "archive-2022-11-20" not in output + assert "archive-2022-12-18" not in output + assert "archive-now" in output + output = cmd(archiver, "check", "-v", "--archives-only", "--oldest=1m", exit_code=0) - assert "archive1" in output - assert "archive2" in output - assert "archive3" not in output + assert "archive-2022-11-20" in output + assert "archive-2022-12-18" in output + assert "archive-now" not in output output = cmd(archiver, "check", "-v", "--archives-only", "--newest=1m", exit_code=0) - assert "archive3" in output - assert "archive2" not in output - assert "archive1" not in output + assert "archive-2022-11-20" not in output + assert "archive-2022-12-18" not in output + assert "archive-now" in output output = cmd(archiver, "check", "-v", "--archives-only", "--newer=1d", exit_code=0) - assert "archive3" in output - assert "archive1" not in output - assert "archive2" not in output + assert "archive-2022-11-20" not in output + assert "archive-2022-12-18" not in output + assert "archive-now" in output output = cmd(archiver, "check", "-v", "--archives-only", "--older=1d", exit_code=0) - assert "archive1" in output - assert "archive2" in output - assert "archive3" not in output + assert "archive-2022-11-20" in output + assert "archive-2022-12-18" in output + assert "archive-now" not in output + + output = cmd(archiver, "check", "-v", "--archives-only", "--newer=24H", exit_code=0) + assert "archive-2022-11-20" not in output + assert "archive-2022-12-18" not in output + assert "archive-now" in output + + output = cmd(archiver, "check", "-v", "--archives-only", "--older=24H", exit_code=0) + assert "archive-2022-11-20" in output + assert "archive-2022-12-18" in output + assert "archive-now" not in output + + output = cmd(archiver, "check", "-v", "--archives-only", "--newer=1440M", exit_code=0) + assert "archive-2022-11-20" not in output + assert "archive-2022-12-18" not in output + assert "archive-now" in output + + output = cmd(archiver, "check", "-v", "--archives-only", "--older=1440M", exit_code=0) + assert "archive-2022-11-20" in output + assert "archive-2022-12-18" in output + assert "archive-now" not in output + + output = cmd(archiver, "check", "-v", "--archives-only", "--newer=86400S", exit_code=0) + assert "archive-2022-11-20" not in output + assert "archive-2022-12-18" not in output + assert "archive-now" in output + + output = cmd(archiver, "check", "-v", "--archives-only", "--older=86400S", exit_code=0) + assert "archive-2022-11-20" in output + assert "archive-2022-12-18" in output + assert "archive-now" not in output # check for output when timespan older than the earliest archive is given. Issue #1711 output = cmd(archiver, "check", "-v", "--archives-only", "--older=9999m", exit_code=0) diff --git a/src/borg/testsuite/archiver/repo_list_cmd_test.py b/src/borg/testsuite/archiver/repo_list_cmd_test.py index 66cc9bf63a..1f7e0b8424 100644 --- a/src/borg/testsuite/archiver/repo_list_cmd_test.py +++ b/src/borg/testsuite/archiver/repo_list_cmd_test.py @@ -57,32 +57,72 @@ def test_size_nfiles(archivers, request): def test_date_matching(archivers, request): archiver = request.getfixturevalue(archivers) cmd(archiver, "repo-create", RK_ENCRYPTION) - earliest_ts = "2022-11-20T23:59:59" - ts_in_between = "2022-12-18T23:59:59" - create_src_archive(archiver, "archive1", ts=earliest_ts) - create_src_archive(archiver, "archive2", ts=ts_in_between) - create_src_archive(archiver, "archive3") - cmd(archiver, "repo-list", "-v", "--oldest=23e", exit_code=2) + + create_src_archive(archiver, "archive-2022-11-20", ts="2022-11-20T23:59:59") + create_src_archive(archiver, "archive-2022-12-18", ts="2022-12-18T23:59:59") + create_src_archive(archiver, "archive-now") + + cmd(archiver, "check", "-v", "--oldest=23e", exit_code=2) + + output = cmd(archiver, "repo-list", "-v", "--oldest=1Y", exit_code=0) + assert "archive-2022-11-20" in output + assert "archive-2022-12-18" in output + assert "archive-now" not in output + + output = cmd(archiver, "repo-list", "-v", "--newest=1Y", exit_code=0) + assert "archive-2022-11-20" not in output + assert "archive-2022-12-18" not in output + assert "archive-now" in output output = cmd(archiver, "repo-list", "-v", "--oldest=1m", exit_code=0) - assert "archive1" in output - assert "archive2" in output - assert "archive3" not in output + assert "archive-2022-11-20" in output + assert "archive-2022-12-18" in output + assert "archive-now" not in output output = cmd(archiver, "repo-list", "-v", "--newest=1m", exit_code=0) - assert "archive3" in output - assert "archive2" not in output - assert "archive1" not in output + assert "archive-2022-11-20" not in output + assert "archive-2022-12-18" not in output + assert "archive-now" in output output = cmd(archiver, "repo-list", "-v", "--newer=1d", exit_code=0) - assert "archive3" in output - assert "archive1" not in output - assert "archive2" not in output + assert "archive-2022-11-20" not in output + assert "archive-2022-12-18" not in output + assert "archive-now" in output output = cmd(archiver, "repo-list", "-v", "--older=1d", exit_code=0) - assert "archive1" in output - assert "archive2" in output - assert "archive3" not in output + assert "archive-2022-11-20" in output + assert "archive-2022-12-18" in output + assert "archive-now" not in output + + output = cmd(archiver, "repo-list", "-v", "--newer=24H", exit_code=0) + assert "archive-2022-11-20" not in output + assert "archive-2022-12-18" not in output + assert "archive-now" in output + + output = cmd(archiver, "repo-list", "-v", "--older=24H", exit_code=0) + assert "archive-2022-11-20" in output + assert "archive-2022-12-18" in output + assert "archive-now" not in output + + output = cmd(archiver, "repo-list", "-v", "--newer=1440M", exit_code=0) + assert "archive-2022-11-20" not in output + assert "archive-2022-12-18" not in output + assert "archive-now" in output + + output = cmd(archiver, "repo-list", "-v", "--older=1440M", exit_code=0) + assert "archive-2022-11-20" in output + assert "archive-2022-12-18" in output + assert "archive-now" not in output + + output = cmd(archiver, "repo-list", "-v", "--newer=86400S", exit_code=0) + assert "archive-2022-11-20" not in output + assert "archive-2022-12-18" not in output + assert "archive-now" in output + + output = cmd(archiver, "repo-list", "-v", "--older=86400S", exit_code=0) + assert "archive-2022-11-20" in output + assert "archive-2022-12-18" in output + assert "archive-now" not in output def test_repo_list_json(archivers, request): From ddf51cb7a94b4c0531c3305f33a0a6962c42ecb0 Mon Sep 17 00:00:00 2001 From: Ken Kundert Date: Mon, 6 Jan 2025 09:30:10 -0800 Subject: [PATCH 2/9] fix style with black --- src/borg/helpers/time.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/borg/helpers/time.py b/src/borg/helpers/time.py index 0dd1c42a45..68fa024b43 100644 --- a/src/borg/helpers/time.py +++ b/src/borg/helpers/time.py @@ -132,11 +132,11 @@ def calculate_relative_offset(format_string, from_ts, earlier=False): elif unit == "m": return offset_n_months(from_ts, offset) elif unit == "Y": - return from_ts.replace(year=from_ts.year+offset) + return from_ts.replace(year=from_ts.year + offset) elif unit == "H": - return from_ts + timedelta(seconds=offset*60*60) + return from_ts + timedelta(seconds=offset * 60 * 60) elif unit == "M": - return from_ts + timedelta(seconds=offset*60) + return from_ts + timedelta(seconds=offset * 60) elif unit == "S": return from_ts + timedelta(seconds=offset) From b3dc5a5603b00eef83f708f85545719babc030be Mon Sep 17 00:00:00 2001 From: Ken Kundert Date: Mon, 6 Jan 2025 12:44:32 -0800 Subject: [PATCH 3/9] Add support for weeks in timespans, change Y to y. This is initial response to comments made on pull request. Will look at --keep-within in a bit. --- docs/usage/general/date-time.rst.inc | 8 ++++---- src/borg/helpers/parseformat.py | 4 ++-- src/borg/helpers/time.py | 4 +++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/usage/general/date-time.rst.inc b/docs/usage/general/date-time.rst.inc index bf5aa73409..62a6d5c463 100644 --- a/docs/usage/general/date-time.rst.inc +++ b/docs/usage/general/date-time.rst.inc @@ -12,7 +12,7 @@ Internally, we store and process date and time as UTC. .. rubric:: TIMESPAN -Some options accept a TIMESPAN parameter, which can be given as a -number of years (e.g. ``2Y``), months (e.g. ``12m``), days (e.g. ``7d``), -hours (e.g. ``8H``), minutes (e.g. ``30M``), or seconds (e.g. ``84600s``). - +Some options accept a TIMESPAN parameter, which can be given as a number of +years (e.g. ``2y``), months (e.g. ``12m``), weeks (e.g. ``2w``), +days (e.g. ``7d``), hours (e.g. ``8H``), minutes (e.g. ``30M``), +or seconds (e.g. ``150S``). diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 9a1ac43af9..9636758e28 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -579,10 +579,10 @@ def validator(text): def relative_time_marker_validator(text: str): - time_marker_regex = r"^\d+[YmdHMS]$" + time_marker_regex = r"^\d+[ymwdHMS]$" match = re.compile(time_marker_regex).search(text) if not match: - raise argparse.ArgumentTypeError(f"Invalid relative time marker used: {text}, choose from Y, m, d, H, M, S") + raise argparse.ArgumentTypeError(f"Invalid relative time marker used: {text}, choose from y, m, w, d, H, M, S") else: return text diff --git a/src/borg/helpers/time.py b/src/borg/helpers/time.py index 68fa024b43..3e4f9989de 100644 --- a/src/borg/helpers/time.py +++ b/src/borg/helpers/time.py @@ -131,8 +131,10 @@ def calculate_relative_offset(format_string, from_ts, earlier=False): return from_ts + timedelta(days=offset) elif unit == "m": return offset_n_months(from_ts, offset) - elif unit == "Y": + elif unit == "y": return from_ts.replace(year=from_ts.year + offset) + elif unit == "w": + return from_ts + timedelta(days=offset * 7) elif unit == "H": return from_ts + timedelta(seconds=offset * 60 * 60) elif unit == "M": From f7bea3ccb506d5becfb90cf37bea23ea76a250eb Mon Sep 17 00:00:00 2001 From: Ken Kundert Date: Mon, 6 Jan 2025 12:52:53 -0800 Subject: [PATCH 4/9] fix bug --- src/borg/helpers/time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/helpers/time.py b/src/borg/helpers/time.py index 3e4f9989de..95ae796d6a 100644 --- a/src/borg/helpers/time.py +++ b/src/borg/helpers/time.py @@ -119,7 +119,7 @@ def calculate_relative_offset(format_string, from_ts, earlier=False): from_ts = archive_ts_now() if format_string is not None: - offset_regex = re.compile(r"(?P\d+)(?P[YmdHMS])") + offset_regex = re.compile(r"(?P\d+)(?P[ymwdHMS])") match = offset_regex.search(format_string) if match: From ea2d3441eabe6c40283182c674461685201a45a5 Mon Sep 17 00:00:00 2001 From: Ken Kundert Date: Mon, 6 Jan 2025 13:23:12 -0800 Subject: [PATCH 5/9] supplement tests --- src/borg/testsuite/archiver/check_cmd_test.py | 14 ++++++++++++-- src/borg/testsuite/archiver/repo_list_cmd_test.py | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/borg/testsuite/archiver/check_cmd_test.py b/src/borg/testsuite/archiver/check_cmd_test.py index e9f189e650..4b628b44bf 100644 --- a/src/borg/testsuite/archiver/check_cmd_test.py +++ b/src/borg/testsuite/archiver/check_cmd_test.py @@ -63,12 +63,12 @@ def test_date_matching(archivers, request): create_src_archive(archiver, "archive-now") cmd(archiver, "check", "-v", "--archives-only", "--oldest=23e", exit_code=2) - output = cmd(archiver, "check", "-v", "--archives-only", "--oldest=1Y", exit_code=0) + output = cmd(archiver, "check", "-v", "--archives-only", "--oldest=1y", exit_code=0) assert "archive-2022-11-20" in output assert "archive-2022-12-18" in output assert "archive-now" not in output - output = cmd(archiver, "check", "-v", "--archives-only", "--newest=1Y", exit_code=0) + output = cmd(archiver, "check", "-v", "--archives-only", "--newest=1y", exit_code=0) assert "archive-2022-11-20" not in output assert "archive-2022-12-18" not in output assert "archive-now" in output @@ -83,6 +83,16 @@ def test_date_matching(archivers, request): assert "archive-2022-12-18" not in output assert "archive-now" in output + output = cmd(archiver, "check", "-v", "--archives-only", "--oldest=4w", exit_code=0) + assert "archive-2022-11-20" in output + assert "archive-2022-12-18" in output + assert "archive-now" not in output + + output = cmd(archiver, "check", "-v", "--archives-only", "--newest=4w", exit_code=0) + assert "archive-2022-11-20" not in output + assert "archive-2022-12-18" not in output + assert "archive-now" in output + output = cmd(archiver, "check", "-v", "--archives-only", "--newer=1d", exit_code=0) assert "archive-2022-11-20" not in output assert "archive-2022-12-18" not in output diff --git a/src/borg/testsuite/archiver/repo_list_cmd_test.py b/src/borg/testsuite/archiver/repo_list_cmd_test.py index 1f7e0b8424..6f8d02565f 100644 --- a/src/borg/testsuite/archiver/repo_list_cmd_test.py +++ b/src/borg/testsuite/archiver/repo_list_cmd_test.py @@ -64,12 +64,12 @@ def test_date_matching(archivers, request): cmd(archiver, "check", "-v", "--oldest=23e", exit_code=2) - output = cmd(archiver, "repo-list", "-v", "--oldest=1Y", exit_code=0) + output = cmd(archiver, "repo-list", "-v", "--oldest=1y", exit_code=0) assert "archive-2022-11-20" in output assert "archive-2022-12-18" in output assert "archive-now" not in output - output = cmd(archiver, "repo-list", "-v", "--newest=1Y", exit_code=0) + output = cmd(archiver, "repo-list", "-v", "--newest=1y", exit_code=0) assert "archive-2022-11-20" not in output assert "archive-2022-12-18" not in output assert "archive-now" in output @@ -84,6 +84,16 @@ def test_date_matching(archivers, request): assert "archive-2022-12-18" not in output assert "archive-now" in output + output = cmd(archiver, "repo-list", "-v", "--oldest=4w", exit_code=0) + assert "archive-2022-11-20" in output + assert "archive-2022-12-18" in output + assert "archive-now" not in output + + output = cmd(archiver, "repo-list", "-v", "--newest=4w", exit_code=0) + assert "archive-2022-11-20" not in output + assert "archive-2022-12-18" not in output + assert "archive-now" in output + output = cmd(archiver, "repo-list", "-v", "--newer=1d", exit_code=0) assert "archive-2022-11-20" not in output assert "archive-2022-12-18" not in output From 9a00d538dda25bcc538000f343e25b161e6b6f55 Mon Sep 17 00:00:00 2001 From: Ken Kundert Date: Mon, 6 Jan 2025 18:46:22 -0800 Subject: [PATCH 6/9] Add support for seconds and minutes to prune --keep-within --- src/borg/archiver/prune_cmd.py | 12 ++++----- src/borg/helpers/parseformat.py | 39 +++++++++++++++++++++--------- src/borg/helpers/time.py | 8 +++--- src/borg/testsuite/helpers_test.py | 24 ++++++++++++------ 4 files changed, 55 insertions(+), 28 deletions(-) diff --git a/src/borg/archiver/prune_cmd.py b/src/borg/archiver/prune_cmd.py index 435f084e46..a4922b46a0 100644 --- a/src/borg/archiver/prune_cmd.py +++ b/src/borg/archiver/prune_cmd.py @@ -18,8 +18,8 @@ logger = create_logger() -def prune_within(archives, hours, kept_because): - target = datetime.now(timezone.utc) - timedelta(seconds=hours * 3600) +def prune_within(archives, seconds, kept_because): + target = datetime.now(timezone.utc) - timedelta(seconds=seconds) kept_counter = 0 result = [] for a in archives: @@ -241,10 +241,10 @@ def build_parser_prune(self, subparsers, common_parser, mid_common_parser): series. The ``--keep-within`` option takes an argument of the form "", - where char is "H", "d", "w", "m", "y". For example, ``--keep-within 2d`` means - to keep all archives that were created within the past 48 hours. - "1m" is taken to mean "31d". The archives kept with this option do not - count towards the totals specified by any other options. + where char is "y", "m", "w", "d", "H", "M", or "S". For example, + ``--keep-within 2d`` means to keep all archives that were created within + the past 2 days. "1m" is taken to mean "31d". The archives kept with + this option do not count towards the totals specified by any other options. A good procedure is to thin out more and more the older your backups get. As an example, ``--keep-daily 7`` means to keep the latest backup on each day, diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 9636758e28..781f10fe15 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -126,26 +126,40 @@ def positive_int_validator(value): def interval(s): - """Convert a string representing a valid interval to a number of hours.""" - multiplier = {"H": 1, "d": 24, "w": 24 * 7, "m": 24 * 31, "y": 24 * 365} + """Convert a string representing a valid interval to a number of seconds.""" + seconds_in_a_minute = 60 + seconds_in_an_hour = 60 * seconds_in_a_minute + seconds_in_a_day = 24 * seconds_in_an_hour + seconds_in_a_week = 7 * seconds_in_a_day + seconds_in_a_month = 31 * seconds_in_a_day + seconds_in_a_year = 365 * seconds_in_a_day + multiplier = dict( + S = 1, + M = seconds_in_a_minute, + H = seconds_in_an_hour, + d = seconds_in_a_day, + w = seconds_in_a_week, + m = seconds_in_a_month, + y = seconds_in_a_year, + ) if s.endswith(tuple(multiplier.keys())): number = s[:-1] suffix = s[-1] else: - # range suffixes in ascending multiplier order - ranges = [k for k, v in sorted(multiplier.items(), key=lambda t: t[1])] - raise argparse.ArgumentTypeError(f'Unexpected interval time unit "{s[-1]}": expected one of {ranges!r}') + raise argparse.ArgumentTypeError( + f'Unexpected time unit "{s[-1]}": choose from {", ".join(multiplier)}' + ) try: - hours = int(number) * multiplier[suffix] + seconds = int(number) * multiplier[suffix] except ValueError: - hours = -1 + seconds = -1 - if hours <= 0: - raise argparse.ArgumentTypeError('Unexpected interval number "%s": expected an integer greater than 0' % number) + if seconds <= 0: + raise argparse.ArgumentTypeError(f'Invalid number "{number}": expected positive integer') - return hours + return seconds def ChunkerParams(s): @@ -565,7 +579,10 @@ def with_timestamp(self, timestamp): def location_validator(proto=None, other=False): def validator(text): try: - loc = Location(text, other=other) + try: + loc = Location(text, other=other) + except Exception as e: + assert False except ValueError as err: raise argparse.ArgumentTypeError(str(err)) from None if proto is not None and loc.proto != proto: diff --git a/src/borg/helpers/time.py b/src/borg/helpers/time.py index 95ae796d6a..5e2e85ee6d 100644 --- a/src/borg/helpers/time.py +++ b/src/borg/helpers/time.py @@ -127,14 +127,14 @@ def calculate_relative_offset(format_string, from_ts, earlier=False): offset = int(match.group("offset")) offset *= -1 if earlier else 1 - if unit == "d": - return from_ts + timedelta(days=offset) + if unit == "y": + return from_ts.replace(year=from_ts.year + offset) elif unit == "m": return offset_n_months(from_ts, offset) - elif unit == "y": - return from_ts.replace(year=from_ts.year + offset) elif unit == "w": return from_ts + timedelta(days=offset * 7) + elif unit == "d": + return from_ts + timedelta(days=offset) elif unit == "H": return from_ts + timedelta(seconds=offset * 60 * 60) elif unit == "M": diff --git a/src/borg/testsuite/helpers_test.py b/src/borg/testsuite/helpers_test.py index 102ead54b5..5db850e7f9 100644 --- a/src/borg/testsuite/helpers_test.py +++ b/src/borg/testsuite/helpers_test.py @@ -553,17 +553,25 @@ def test_prune_split_no_archives(): assert kept_because == {} -@pytest.mark.parametrize("timeframe, num_hours", [("1H", 1), ("1d", 24), ("1w", 168), ("1m", 744), ("1y", 8760)]) -def test_interval(timeframe, num_hours): - assert interval(timeframe) == num_hours +@pytest.mark.parametrize("timeframe, num_hours", [ + ("5S", 5), + ("2M", 120), + ("1H", 3600), + ("1d", 86_400), + ("1w", 604_800), + ("1m", 2_678_400), + ("1y", 31_536_000) +]) +def test_interval(timeframe, num_secs): + assert interval(timeframe) == num_secs @pytest.mark.parametrize( "invalid_interval, error_tuple", [ - ("H", ('Unexpected interval number "": expected an integer greater than 0',)), - ("-1d", ('Unexpected interval number "-1": expected an integer greater than 0',)), - ("food", ('Unexpected interval number "foo": expected an integer greater than 0',)), + ("H", ('Unexpected number "": expected positive integer',)), + ("-1d", ('Unexpected number "-1": expected positive integer',)), + ("food", ('Unexpectedinterval number "foo": expected positive integer',)), ], ) def test_interval_time_unit(invalid_interval, error_tuple): @@ -575,7 +583,7 @@ def test_interval_time_unit(invalid_interval, error_tuple): def test_interval_number(): with pytest.raises(ArgumentTypeError) as exc: interval("5") - assert exc.value.args == ("Unexpected interval time unit \"5\": expected one of ['H', 'd', 'w', 'm', 'y']",) + assert exc.value.args == ('Unexpected time unit "5": expected one of S, M, H, d, w, m, y',) def test_prune_within(): @@ -595,6 +603,8 @@ def dotest(test_archives, within, indices): test_dates = [now - timedelta(seconds=s) for s in test_offsets] test_archives = [MockArchive(date, i) for i, date in enumerate(test_dates)] + dotest(test_archives, "15S", []) + dotest(test_archives, "2M", [0]) dotest(test_archives, "1H", [0]) dotest(test_archives, "2H", [0, 1]) dotest(test_archives, "3H", [0, 1, 2]) From c7d8670c75d7ae8e6ba54c02459753de608c3851 Mon Sep 17 00:00:00 2001 From: Ken Kundert Date: Tue, 7 Jan 2025 09:33:19 -0800 Subject: [PATCH 7/9] remove debugging code --- src/borg/helpers/parseformat.py | 23 +++++++++-------------- src/borg/testsuite/helpers_test.py | 23 +++++++++++++---------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 781f10fe15..705b4e290c 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -134,22 +134,20 @@ def interval(s): seconds_in_a_month = 31 * seconds_in_a_day seconds_in_a_year = 365 * seconds_in_a_day multiplier = dict( - S = 1, - M = seconds_in_a_minute, - H = seconds_in_an_hour, - d = seconds_in_a_day, - w = seconds_in_a_week, - m = seconds_in_a_month, - y = seconds_in_a_year, + y=seconds_in_a_year, + m=seconds_in_a_month, + w=seconds_in_a_week, + d=seconds_in_a_day, + H=seconds_in_an_hour, + M=seconds_in_a_minute, + S=1, ) if s.endswith(tuple(multiplier.keys())): number = s[:-1] suffix = s[-1] else: - raise argparse.ArgumentTypeError( - f'Unexpected time unit "{s[-1]}": choose from {", ".join(multiplier)}' - ) + raise argparse.ArgumentTypeError(f'Unexpected time unit "{s[-1]}": choose from {", ".join(multiplier)}') try: seconds = int(number) * multiplier[suffix] @@ -579,10 +577,7 @@ def with_timestamp(self, timestamp): def location_validator(proto=None, other=False): def validator(text): try: - try: - loc = Location(text, other=other) - except Exception as e: - assert False + loc = Location(text, other=other) except ValueError as err: raise argparse.ArgumentTypeError(str(err)) from None if proto is not None and loc.proto != proto: diff --git a/src/borg/testsuite/helpers_test.py b/src/borg/testsuite/helpers_test.py index 5db850e7f9..f56d0ebe0f 100644 --- a/src/borg/testsuite/helpers_test.py +++ b/src/borg/testsuite/helpers_test.py @@ -553,15 +553,18 @@ def test_prune_split_no_archives(): assert kept_because == {} -@pytest.mark.parametrize("timeframe, num_hours", [ - ("5S", 5), - ("2M", 120), - ("1H", 3600), - ("1d", 86_400), - ("1w", 604_800), - ("1m", 2_678_400), - ("1y", 31_536_000) -]) +@pytest.mark.parametrize( + "timeframe, num_hours", + [ + ("5S", 5), + ("2M", 2 * 60), + ("1H", 60 * 60), + ("1d", 24 * 60 * 60), + ("1w", 7 * 24 * 68 * 60), + ("1m", 31 * 24 * 68 * 60), + ("1y", 365 * 24 * 68 * 60), + ], +) def test_interval(timeframe, num_secs): assert interval(timeframe) == num_secs @@ -571,7 +574,7 @@ def test_interval(timeframe, num_secs): [ ("H", ('Unexpected number "": expected positive integer',)), ("-1d", ('Unexpected number "-1": expected positive integer',)), - ("food", ('Unexpectedinterval number "foo": expected positive integer',)), + ("food", ('Unexpected number "foo": expected positive integer',)), ], ) def test_interval_time_unit(invalid_interval, error_tuple): From 83717e4d4ca6b2525ba583cd719415d5062e5bdb Mon Sep 17 00:00:00 2001 From: Ken Kundert Date: Tue, 7 Jan 2025 19:03:13 -0800 Subject: [PATCH 8/9] fix bug in test --- src/borg/testsuite/helpers_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/borg/testsuite/helpers_test.py b/src/borg/testsuite/helpers_test.py index f56d0ebe0f..8e75035fca 100644 --- a/src/borg/testsuite/helpers_test.py +++ b/src/borg/testsuite/helpers_test.py @@ -554,7 +554,7 @@ def test_prune_split_no_archives(): @pytest.mark.parametrize( - "timeframe, num_hours", + "timeframe, num_secs", [ ("5S", 5), ("2M", 2 * 60), From ea65cae1a0ca583dc5acf9655da9ac3421367567 Mon Sep 17 00:00:00 2001 From: Ken Kundert Date: Tue, 7 Jan 2025 21:41:21 -0800 Subject: [PATCH 9/9] clean up the tests --- src/borg/testsuite/helpers_test.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/borg/testsuite/helpers_test.py b/src/borg/testsuite/helpers_test.py index 8e75035fca..1ceee8952e 100644 --- a/src/borg/testsuite/helpers_test.py +++ b/src/borg/testsuite/helpers_test.py @@ -560,9 +560,9 @@ def test_prune_split_no_archives(): ("2M", 2 * 60), ("1H", 60 * 60), ("1d", 24 * 60 * 60), - ("1w", 7 * 24 * 68 * 60), - ("1m", 31 * 24 * 68 * 60), - ("1y", 365 * 24 * 68 * 60), + ("1w", 7 * 24 * 60 * 60), + ("1m", 31 * 24 * 60 * 60), + ("1y", 365 * 24 * 60 * 60), ], ) def test_interval(timeframe, num_secs): @@ -572,9 +572,9 @@ def test_interval(timeframe, num_secs): @pytest.mark.parametrize( "invalid_interval, error_tuple", [ - ("H", ('Unexpected number "": expected positive integer',)), - ("-1d", ('Unexpected number "-1": expected positive integer',)), - ("food", ('Unexpected number "foo": expected positive integer',)), + ("H", ('Invalid number "": expected positive integer',)), + ("-1d", ('Invalid number "-1": expected positive integer',)), + ("food", ('Invalid number "foo": expected positive integer',)), ], ) def test_interval_time_unit(invalid_interval, error_tuple): @@ -586,7 +586,7 @@ def test_interval_time_unit(invalid_interval, error_tuple): def test_interval_number(): with pytest.raises(ArgumentTypeError) as exc: interval("5") - assert exc.value.args == ('Unexpected time unit "5": expected one of S, M, H, d, w, m, y',) + assert exc.value.args == ('Unexpected time unit "5": choose from y, m, w, d, H, M, S',) def test_prune_within():