-
-
Notifications
You must be signed in to change notification settings - Fork 864
testsuite: add UpgraderFrom12To20 unit tests, refs #9556 #9675
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ThomasWaldmann
merged 2 commits into
borgbackup:master
from
mr-raj12:legacy-upgrade-phase7
May 30, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| """Tests for borg.legacy.upgrade (UpgraderFrom12To20).""" | ||
|
|
||
| import stat | ||
| import zlib | ||
| from argparse import Namespace | ||
| from struct import Struct | ||
| from unittest.mock import MagicMock | ||
|
|
||
| from ..compress import ZLIB, ObfuscateSize | ||
| from ..constants import CH_BUZHASH, REQUIRED_ITEM_KEYS | ||
| from ..item import Item | ||
| from ..legacy.upgrade import UpgraderFrom12To20 | ||
|
|
||
| CHUNK_ID = b"\xab" * 32 | ||
| CHUNK_SIZE = 512 | ||
|
|
||
|
|
||
| # ── helpers ─────────────────────────────────────────────────────────────────── | ||
|
|
||
|
|
||
| def _upgrader(*, rechunk=None): | ||
| """Return (upgrader, mock_cache, mock_archive) ready for upgrade calls.""" | ||
| cache = MagicMock() | ||
| args = Namespace(chunker_params=rechunk) | ||
| u = UpgraderFrom12To20(cache=cache, args=args) | ||
| archive = MagicMock() | ||
| u.new_archive(archive=archive) | ||
| return u, cache, archive | ||
|
|
||
|
|
||
| def _item(**kwargs): | ||
| """Build an Item via internal_dict to allow borg1-only keys (hardlink_master, source, etc.).""" | ||
| base = {"path": "dir/file", "mode": stat.S_IFREG | 0o644, "mtime": 0} | ||
| base.update(kwargs) | ||
| return Item(internal_dict=base) | ||
|
|
||
|
|
||
| def _run_upgrade_archive_metadata(attrs, *, rechunk=None): | ||
| u, _, _ = _upgrader(rechunk=rechunk) | ||
| return u.upgrade_archive_metadata(metadata=Namespace(**attrs)) | ||
|
|
||
|
|
||
| # ── upgrade_item ────────────────────────────────────────────────────────────── | ||
|
|
||
|
|
||
| def test_regular_file_passes_through(): | ||
| u, _, _ = _upgrader() | ||
| item = _item() | ||
| result = u.upgrade_item(item=item) | ||
| assert result.path == "dir/file" | ||
| assert result.mode == stat.S_IFREG | 0o644 | ||
| assert all(k in result for k in REQUIRED_ITEM_KEYS) | ||
|
|
||
|
|
||
| def test_whitelist_strips_legacy_keys(): | ||
| # 'acl' = attic <= 0.13 bug; 'chunks_healthy' and 'hardlink_master' are borg1-only | ||
| u, _, _ = _upgrader() | ||
| item = _item(acl=b"bad", chunks_healthy=[], hardlink_master=False) | ||
| result = u.upgrade_item(item=item) | ||
| d = result.as_dict() | ||
| assert "acl" not in d | ||
| assert "chunks_healthy" not in d | ||
| assert "hardlink_master" not in d | ||
|
|
||
|
|
||
| def test_user_group_none_removed(): | ||
| u, _, _ = _upgrader() | ||
| item = _item(user=None, group=None) | ||
| result = u.upgrade_item(item=item) | ||
| d = result.as_dict() | ||
| assert "user" not in d | ||
| assert "group" not in d | ||
|
|
||
|
|
||
| def test_symlink_source_renamed_to_target(): | ||
| u, _, _ = _upgrader() | ||
| item = _item(path="dir/link", mode=stat.S_IFLNK | 0o777, source="/etc/hosts") | ||
| result = u.upgrade_item(item=item) | ||
| d = result.as_dict() | ||
| assert d["target"] == "/etc/hosts" | ||
| assert "source" not in d | ||
|
|
||
|
|
||
| def test_hardlink_master_gets_hlid_and_strips_hardlink_master_key(): | ||
| u, _, _ = _upgrader() | ||
| item = _item(hardlink_master=True, chunks=[[CHUNK_ID, CHUNK_SIZE]]) | ||
| result = u.upgrade_item(item=item) | ||
| d = result.as_dict() | ||
| assert "hlid" in d | ||
| assert "hardlink_master" not in d | ||
| assert d["chunks"] == [[CHUNK_ID, CHUNK_SIZE]] | ||
|
|
||
|
|
||
| def test_hardlink_slave_gets_master_hlid_and_chunks(): | ||
| u, cache, archive = _upgrader() | ||
| master = _item(hardlink_master=True, chunks=[[CHUNK_ID, CHUNK_SIZE]]) | ||
| master_result = u.upgrade_item(item=master) | ||
|
|
||
| slave = _item(path="dir/link2", source="dir/file") | ||
| slave_result = u.upgrade_item(item=slave) | ||
|
|
||
| d = slave_result.as_dict() | ||
| assert "hlid" in d | ||
| assert "source" not in d | ||
| assert d["chunks"] == [[CHUNK_ID, CHUNK_SIZE]] | ||
| assert slave_result.hlid == master_result.hlid | ||
| cache.reuse_chunk.assert_called_once_with(CHUNK_ID, CHUNK_SIZE, archive.stats) | ||
|
|
||
|
|
||
| def test_required_item_keys_always_present(): | ||
| u, _, _ = _upgrader() | ||
| item = _item() | ||
| result = u.upgrade_item(item=item) | ||
| assert all(k in result for k in REQUIRED_ITEM_KEYS) | ||
|
|
||
|
|
||
| # ── upgrade_archive_metadata ────────────────────────────────────────────────── | ||
|
|
||
|
|
||
| def test_cmdline_list_becomes_command_line_string(): | ||
| result = _run_upgrade_archive_metadata({"cmdline": ["borg", "create", "::arch", "/home"]}) | ||
| assert result["command_line"] == "borg create ::arch /home" | ||
| assert "cmdline" not in result | ||
|
|
||
|
|
||
| def test_recreate_cmdline_becomes_recreate_command_line_string(): | ||
| result = _run_upgrade_archive_metadata({"recreate_cmdline": ["borg", "recreate", "--recompress"]}) | ||
| assert result["recreate_command_line"] == "borg recreate --recompress" | ||
| assert "recreate_cmdline" not in result | ||
|
|
||
|
|
||
| def test_time_gets_utc_offset_appended(): | ||
| result = _run_upgrade_archive_metadata({"time": "2021-01-01T12:00:00.000000"}) | ||
| assert result["time"] == "2021-01-01T12:00:00.000000+00:00" | ||
|
|
||
|
|
||
| def test_old_4tuple_chunker_params_gets_buzhash_prefix(): | ||
| old = (10, 23, 16, 4095) | ||
| result = _run_upgrade_archive_metadata({"chunker_params": old}) | ||
| assert result["chunker_params"] == (CH_BUZHASH,) + old | ||
|
|
||
|
|
||
| def test_new_5tuple_chunker_params_unchanged(): | ||
| new = (CH_BUZHASH, 10, 23, 16, 4095) | ||
| result = _run_upgrade_archive_metadata({"chunker_params": new}) | ||
| assert result["chunker_params"] == new | ||
|
|
||
|
|
||
| def test_rechunking_overrides_stored_chunker_params(): | ||
| override = (CH_BUZHASH, 12, 25, 18, 4095) | ||
| result = _run_upgrade_archive_metadata({"chunker_params": (10, 23, 16, 4095)}, rechunk=override) | ||
| assert result["chunker_params"] == override | ||
|
|
||
|
|
||
| def test_recreate_fields_dropped(): | ||
| result = _run_upgrade_archive_metadata( | ||
| { | ||
| "recreate_source_id": b"\x01" * 32, | ||
| "recreate_args": ["--some-arg"], | ||
| "recreate_partial_chunks": [[b"\x02" * 32, 100]], | ||
| } | ||
| ) | ||
| assert "recreate_source_id" not in result | ||
| assert "recreate_args" not in result | ||
| assert "recreate_partial_chunks" not in result | ||
|
|
||
|
|
||
| def test_tags_always_set_to_empty_list(): | ||
| result = _run_upgrade_archive_metadata({}) | ||
| assert result["tags"] == [] | ||
|
|
||
|
|
||
| def test_missing_optional_attrs_not_in_result(): | ||
| result = _run_upgrade_archive_metadata({}) | ||
| assert "command_line" not in result | ||
| assert "time" not in result | ||
| assert "chunker_params" not in result | ||
|
|
||
|
|
||
| # ── upgrade_compressed_chunk ────────────────────────────────────────────────── | ||
|
|
||
|
|
||
| def test_zlib_legacy_detected_and_ctype_promoted(): | ||
| # Raw zlib bytes (no ctype/clevel prefix) — ZLIB_legacy.detect() matches them | ||
| raw = zlib.compress(b"hello world") | ||
| u, _, _ = _upgrader() | ||
| meta, out = u.upgrade_compressed_chunk({}, raw) | ||
| assert meta["ctype"] == ZLIB.ID | ||
| assert meta["clevel"] == 0xFF | ||
| assert meta["csize"] == len(raw) | ||
| assert out == raw # data is unchanged; only metadata is upgraded | ||
|
|
||
|
|
||
| def test_non_zlib_two_prefix_bytes_stripped(): | ||
| # Any compressor with explicit ctype/clevel bytes (e.g. LZ4 ID=0x01) | ||
| payload = b"lz4_compressed_payload" | ||
| data = bytes([0x01, 0xFF]) + payload | ||
| u, _, _ = _upgrader() | ||
| meta, out = u.upgrade_compressed_chunk({}, data) | ||
| assert meta["ctype"] == 0x01 | ||
| assert meta["clevel"] == 0xFF | ||
| assert out == payload | ||
| assert meta["csize"] == len(payload) | ||
|
|
||
|
|
||
| def test_obfuscate_old_big_endian_csize_is_upgraded(): | ||
| # Borg 1.x ObfuscateSize stored csize as a 4-byte big-endian field directly in the | ||
| # chunk header (bytes 2-5), followed by the compressed payload and zero padding. | ||
| # Borg 2 has no inline csize field; chunk sizing lives in the msgpack metadata dict. | ||
| # The upgrader extracts csize from the fixed header and promotes it to metadata. | ||
| inner = zlib.compress(b"secret data") | ||
| csize = len(inner) | ||
| big_endian_csize = Struct(">I").pack(csize) | ||
| padding = bytes(16) | ||
| data = bytes([ObfuscateSize.ID, 0xFF]) + big_endian_csize + inner + padding | ||
|
|
||
| u, _, _ = _upgrader() | ||
| meta, out = u.upgrade_compressed_chunk({}, data) | ||
|
|
||
| assert meta["psize"] == csize | ||
| assert meta["ctype"] == ZLIB.ID # inner was ZLIB_legacy → promoted | ||
| assert meta["clevel"] == 0xFF | ||
| assert len(out) == csize + len(padding) | ||
| assert meta["csize"] == len(out) | ||
| assert out[csize:] == padding # trailing zeros preserved |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.