From dfaea063a513c5a18120fc88cd2db00dcb7663e1 Mon Sep 17 00:00:00 2001 From: bigtedde Date: Tue, 4 Jul 2023 17:15:15 -0500 Subject: [PATCH] diff_cmd converted --- src/borg/testsuite/archiver/diff_cmd.py | 544 ++++++++++++------------ 1 file changed, 263 insertions(+), 281 deletions(-) diff --git a/src/borg/testsuite/archiver/diff_cmd.py b/src/borg/testsuite/archiver/diff_cmd.py index 2fc5a2f52..08257b9f8 100644 --- a/src/borg/testsuite/archiver/diff_cmd.py +++ b/src/borg/testsuite/archiver/diff_cmd.py @@ -2,321 +2,303 @@ import json import os import stat import time -import unittest from ...constants import * # NOQA from .. import are_symlinks_supported, are_hardlinks_supported from ..platform import is_win32, is_darwin -from . import ArchiverTestCaseBase, RemoteArchiverTestCaseBase, ArchiverTestCaseBinaryBase, RK_ENCRYPTION, BORG_EXES +from . import cmd, create_regular_file, RK_ENCRYPTION, assert_line_exists -class ArchiverTestCase(ArchiverTestCaseBase): - def test_basic_functionality(self): - # Setup files for the first snapshot - self.create_regular_file("empty", size=0) - self.create_regular_file("file_unchanged", size=128) - self.create_regular_file("file_removed", size=256) - self.create_regular_file("file_removed2", size=512) - self.create_regular_file("file_replaced", size=1024) - os.mkdir("input/dir_replaced_with_file") - os.chmod("input/dir_replaced_with_file", stat.S_IFDIR | 0o755) - os.mkdir("input/dir_removed") +def pytest_generate_tests(metafunc): + # Generates tests that run on local and remote repos, as well as with a binary base. + if "archivers" in metafunc.fixturenames: + metafunc.parametrize("archivers", ["archiver", "remote_archiver", "binary_archiver"]) + + +def test_basic_functionality(archivers, request): + archiver = request.getfixturevalue(archivers) + repo_location, input_path = archiver.repository_location, archiver.input_path + # Setup files for the first snapshot + create_regular_file(input_path, "empty", size=0) + create_regular_file(input_path, "file_unchanged", size=128) + create_regular_file(input_path, "file_removed", size=256) + create_regular_file(input_path, "file_removed2", size=512) + create_regular_file(input_path, "file_replaced", size=1024) + os.mkdir("input/dir_replaced_with_file") + os.chmod("input/dir_replaced_with_file", stat.S_IFDIR | 0o755) + os.mkdir("input/dir_removed") + if are_symlinks_supported(): + os.mkdir("input/dir_replaced_with_link") + os.symlink("input/dir_replaced_with_file", "input/link_changed") + os.symlink("input/file_unchanged", "input/link_removed") + os.symlink("input/file_removed2", "input/link_target_removed") + os.symlink("input/empty", "input/link_target_contents_changed") + os.symlink("input/empty", "input/link_replaced_by_file") + if are_hardlinks_supported(): + os.link("input/file_replaced", "input/hardlink_target_replaced") + os.link("input/empty", "input/hardlink_contents_changed") + os.link("input/file_removed", "input/hardlink_removed") + os.link("input/file_removed2", "input/hardlink_target_removed") + + cmd(archiver, f"--repo={repo_location}", "rcreate", RK_ENCRYPTION) + + # Create the first snapshot + cmd(archiver, f"--repo={repo_location}", "create", "test0", "input") + + # Setup files for the second snapshot + create_regular_file(input_path, "file_added", size=2048) + create_regular_file(input_path, "file_empty_added", size=0) + os.unlink("input/file_replaced") + create_regular_file(input_path, "file_replaced", contents=b"0" * 4096) + os.unlink("input/file_removed") + os.unlink("input/file_removed2") + os.rmdir("input/dir_replaced_with_file") + create_regular_file(input_path, "dir_replaced_with_file", size=8192) + os.chmod("input/dir_replaced_with_file", stat.S_IFREG | 0o755) + os.mkdir("input/dir_added") + os.rmdir("input/dir_removed") + if are_symlinks_supported(): + os.rmdir("input/dir_replaced_with_link") + os.symlink("input/dir_added", "input/dir_replaced_with_link") + os.unlink("input/link_changed") + os.symlink("input/dir_added", "input/link_changed") + os.symlink("input/dir_added", "input/link_added") + os.unlink("input/link_replaced_by_file") + create_regular_file(input_path, "link_replaced_by_file", size=16384) + os.unlink("input/link_removed") + if are_hardlinks_supported(): + os.unlink("input/hardlink_removed") + os.link("input/file_added", "input/hardlink_added") + + with open("input/empty", "ab") as fd: + fd.write(b"appended_data") + + # Create the second snapshot + cmd(archiver, f"--repo={repo_location}", "create", "test1a", "input") + cmd(archiver, f"--repo={repo_location}", "create", "test1b", "input", "--chunker-params", "16,18,17,4095") + + def do_asserts(output, can_compare_ids, content_only=False): + lines: list = output.splitlines() + assert "file_replaced" in output # added to debug #3494 + change = "modified.*B" if can_compare_ids else r"modified: \(can't get size\)" + assert_line_exists(lines, f"{change}.*input/file_replaced") + # File unchanged + assert "input/file_unchanged" not in output + + # Directory replaced with a regular file + if "BORG_TESTS_IGNORE_MODES" not in os.environ and not is_win32 and not content_only: + assert_line_exists(lines, "[drwxr-xr-x -> -rwxr-xr-x].*input/dir_replaced_with_file") + + # Basic directory cases + assert "added directory input/dir_added" in output + assert "removed directory input/dir_removed" in output + if are_symlinks_supported(): - os.mkdir("input/dir_replaced_with_link") - os.symlink("input/dir_replaced_with_file", "input/link_changed") - os.symlink("input/file_unchanged", "input/link_removed") - os.symlink("input/file_removed2", "input/link_target_removed") - os.symlink("input/empty", "input/link_target_contents_changed") - os.symlink("input/empty", "input/link_replaced_by_file") + # Basic symlink cases + assert_line_exists(lines, "changed link.*input/link_changed") + assert_line_exists(lines, "added link.*input/link_added") + assert_line_exists(lines, "removed link.*input/link_removed") + + # Symlink replacing or being replaced + if not content_only: + assert "input/dir_replaced_with_link" in output + assert "input/link_replaced_by_file" in output + + # Symlink target removed. Should not affect the symlink at all. + assert "input/link_target_removed" not in output + + # The inode has two links and the file contents changed. Borg + # should notice the changes in both links. However, the symlink + # pointing to the file is not changed. + change = "modified.*0 B" if can_compare_ids else r"modified: \(can't get size\)" + assert_line_exists(lines, f"{change}.*input/empty") if are_hardlinks_supported(): - os.link("input/file_replaced", "input/hardlink_target_replaced") - os.link("input/empty", "input/hardlink_contents_changed") - os.link("input/file_removed", "input/hardlink_removed") - os.link("input/file_removed2", "input/hardlink_target_removed") - - self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION) - - # Create the first snapshot - self.cmd(f"--repo={self.repository_location}", "create", "test0", "input") - - # Setup files for the second snapshot - self.create_regular_file("file_added", size=2048) - self.create_regular_file("file_empty_added", size=0) - os.unlink("input/file_replaced") - self.create_regular_file("file_replaced", contents=b"0" * 4096) - os.unlink("input/file_removed") - os.unlink("input/file_removed2") - os.rmdir("input/dir_replaced_with_file") - self.create_regular_file("dir_replaced_with_file", size=8192) - os.chmod("input/dir_replaced_with_file", stat.S_IFREG | 0o755) - os.mkdir("input/dir_added") - os.rmdir("input/dir_removed") + assert_line_exists(lines, f"{change}.*input/hardlink_contents_changed") if are_symlinks_supported(): - os.rmdir("input/dir_replaced_with_link") - os.symlink("input/dir_added", "input/dir_replaced_with_link") - os.unlink("input/link_changed") - os.symlink("input/dir_added", "input/link_changed") - os.symlink("input/dir_added", "input/link_added") - os.unlink("input/link_replaced_by_file") - self.create_regular_file("link_replaced_by_file", size=16384) - os.unlink("input/link_removed") + assert "input/link_target_contents_changed" not in output + + # Added a new file and a hard link to it. Both links to the same + # inode should appear as separate files. + assert "added: 2.05 kB input/file_added" in output if are_hardlinks_supported(): - os.unlink("input/hardlink_removed") - os.link("input/file_added", "input/hardlink_added") + assert "added: 2.05 kB input/hardlink_added" in output - with open("input/empty", "ab") as fd: - fd.write(b"appended_data") + # check if a diff between nonexistent and empty new file is found + assert "added: 0 B input/file_empty_added" in output - # Create the second snapshot - self.cmd(f"--repo={self.repository_location}", "create", "test1a", "input") - self.cmd(f"--repo={self.repository_location}", "create", "test1b", "input", "--chunker-params", "16,18,17,4095") + # The inode has two links and both of them are deleted. They should + # appear as two deleted files. + assert "removed: 256 B input/file_removed" in output + if are_hardlinks_supported(): + assert "removed: 256 B input/hardlink_removed" in output - def do_asserts(output, can_compare_ids, content_only=False): - lines: list = output.splitlines() - assert "file_replaced" in output # added to debug #3494 - change = "modified.*B" if can_compare_ids else r"modified: \(can't get size\)" - self.assert_line_exists(lines, f"{change}.*input/file_replaced") - # File unchanged - assert "input/file_unchanged" not in output + if are_hardlinks_supported() and content_only: + # Another link (marked previously as the source in borg) to the + # same inode was removed. This should only change the ctime since removing + # the link would result in the decrementation of the inode's hard-link count. + assert "input/hardlink_target_removed" not in output - # Directory replaced with a regular file - if "BORG_TESTS_IGNORE_MODES" not in os.environ and not is_win32 and not content_only: - self.assert_line_exists(lines, "[drwxr-xr-x -> -rwxr-xr-x].*input/dir_replaced_with_file") + # Another link (marked previously as the source in borg) to the + # same inode was replaced with a new regular file. This should only change + # its ctime. This should not be reflected in the output if content-only is set + assert "input/hardlink_target_replaced" not in output - # Basic directory cases - assert "added directory input/dir_added" in output - assert "removed directory input/dir_removed" in output + def do_json_asserts(output, can_compare_ids, content_only=False): + def get_changes(filename, data): + chgsets = [j["changes"] for j in data if j["path"] == filename] + assert len(chgsets) < 2 + # return a flattened list of changes for given filename + return sum(chgsets, []) - if are_symlinks_supported(): - # Basic symlink cases - self.assert_line_exists(lines, "changed link.*input/link_changed") - self.assert_line_exists(lines, "added link.*input/link_added") - self.assert_line_exists(lines, "removed link.*input/link_removed") + # convert output to list of dicts + joutput = [json.loads(line) for line in output.split("\n") if line] - # Symlink replacing or being replaced - if not content_only: - assert "input/dir_replaced_with_link" in output - assert "input/link_replaced_by_file" in output + # File contents changed (deleted and replaced with a new file) + expected = {"type": "modified", "added": 4096, "removed": 1024} if can_compare_ids else {"type": "modified"} + assert expected in get_changes("input/file_replaced", joutput) - # Symlink target removed. Should not affect the symlink at all. - assert "input/link_target_removed" not in output + # File unchanged + assert not any(get_changes("input/file_unchanged", joutput)) - # The inode has two links and the file contents changed. Borg - # should notice the changes in both links. However, the symlink - # pointing to the file is not changed. - change = "modified.*0 B" if can_compare_ids else r"modified: \(can't get size\)" - self.assert_line_exists(lines, f"{change}.*input/empty") - if are_hardlinks_supported(): - self.assert_line_exists(lines, f"{change}.*input/hardlink_contents_changed") - if are_symlinks_supported(): - assert "input/link_target_contents_changed" not in output + # Directory replaced with a regular file + if "BORG_TESTS_IGNORE_MODES" not in os.environ and not is_win32 and not content_only: + assert {"type": "changed mode", "item1": "drwxr-xr-x", "item2": "-rwxr-xr-x"} in get_changes( + "input/dir_replaced_with_file", joutput + ) - # Added a new file and a hard link to it. Both links to the same - # inode should appear as separate files. - assert "added: 2.05 kB input/file_added" in output - if are_hardlinks_supported(): - assert "added: 2.05 kB input/hardlink_added" in output + # Basic directory cases + assert {"type": "added directory"} in get_changes("input/dir_added", joutput) + assert {"type": "removed directory"} in get_changes("input/dir_removed", joutput) - # check if a diff between nonexistent and empty new file is found - assert "added: 0 B input/file_empty_added" in output + if are_symlinks_supported(): + # Basic symlink cases + assert {"type": "changed link"} in get_changes("input/link_changed", joutput) + assert {"type": "added link"} in get_changes("input/link_added", joutput) + assert {"type": "removed link"} in get_changes("input/link_removed", joutput) - # The inode has two links and both of them are deleted. They should - # appear as two deleted files. - assert "removed: 256 B input/file_removed" in output - if are_hardlinks_supported(): - assert "removed: 256 B input/hardlink_removed" in output + # Symlink replacing or being replaced - if are_hardlinks_supported() and content_only: - # Another link (marked previously as the source in borg) to the - # same inode was removed. This should only change the ctime since removing - # the link would result in the decrementation of the inode's hard-link count. - assert "input/hardlink_target_removed" not in output + if not content_only: + assert any( + chg["type"] == "changed mode" and chg["item1"].startswith("d") and chg["item2"].startswith("l") + for chg in get_changes("input/dir_replaced_with_link", joutput) + ), get_changes("input/dir_replaced_with_link", joutput) + assert any( + chg["type"] == "changed mode" and chg["item1"].startswith("l") and chg["item2"].startswith("-") + for chg in get_changes("input/link_replaced_by_file", joutput) + ), get_changes("input/link_replaced_by_file", joutput) - # Another link (marked previously as the source in borg) to the - # same inode was replaced with a new regular file. This should only change - # its ctime. This should not be reflected in the output if content-only is set - assert "input/hardlink_target_replaced" not in output + # Symlink target removed. Should not affect the symlink at all. + assert not any(get_changes("input/link_target_removed", joutput)) - def do_json_asserts(output, can_compare_ids, content_only=False): - def get_changes(filename, data): - chgsets = [j["changes"] for j in data if j["path"] == filename] - assert len(chgsets) < 2 - # return a flattened list of changes for given filename - return sum(chgsets, []) + # The inode has two links and the file contents changed. Borg + # should notice the changes in both links. However, the symlink + # pointing to the file is not changed. + expected = {"type": "modified", "added": 13, "removed": 0} if can_compare_ids else {"type": "modified"} + assert expected in get_changes("input/empty", joutput) + if are_hardlinks_supported(): + assert expected in get_changes("input/hardlink_contents_changed", joutput) + if are_symlinks_supported(): + assert not any(get_changes("input/link_target_contents_changed", joutput)) - # convert output to list of dicts - joutput = [json.loads(line) for line in output.split("\n") if line] + # Added a new file and a hard link to it. Both links to the same + # inode should appear as separate files. + assert {"added": 2048, "removed": 0, "type": "added"} in get_changes("input/file_added", joutput) + if are_hardlinks_supported(): + assert {"added": 2048, "removed": 0, "type": "added"} in get_changes("input/hardlink_added", joutput) - # File contents changed (deleted and replaced with a new file) - expected = {"type": "modified", "added": 4096, "removed": 1024} if can_compare_ids else {"type": "modified"} - assert expected in get_changes("input/file_replaced", joutput) + # check if a diff between nonexistent and empty new file is found + assert {"added": 0, "removed": 0, "type": "added"} in get_changes("input/file_empty_added", joutput) - # File unchanged - assert not any(get_changes("input/file_unchanged", joutput)) + # The inode has two links and both of them are deleted. They should + # appear as two deleted files. + assert {"added": 0, "removed": 256, "type": "removed"} in get_changes("input/file_removed", joutput) + if are_hardlinks_supported(): + assert {"added": 0, "removed": 256, "type": "removed"} in get_changes("input/hardlink_removed", joutput) - # Directory replaced with a regular file - if "BORG_TESTS_IGNORE_MODES" not in os.environ and not is_win32 and not content_only: - assert {"type": "changed mode", "item1": "drwxr-xr-x", "item2": "-rwxr-xr-x"} in get_changes( - "input/dir_replaced_with_file", joutput - ) + if are_hardlinks_supported() and content_only: + # Another link (marked previously as the source in borg) to the + # same inode was removed. This should only change the ctime since removing + # the link would result in the decrementation of the inode's hard-link count. + assert not any(get_changes("input/hardlink_target_removed", joutput)) - # Basic directory cases - assert {"type": "added directory"} in get_changes("input/dir_added", joutput) - assert {"type": "removed directory"} in get_changes("input/dir_removed", joutput) + # Another link (marked previously as the source in borg) to the + # same inode was replaced with a new regular file. This should only change + # its ctime. This should not be reflected in the output if content-only is set + assert not any(get_changes("input/hardlink_target_replaced", joutput)) - if are_symlinks_supported(): - # Basic symlink cases - assert {"type": "changed link"} in get_changes("input/link_changed", joutput) - assert {"type": "added link"} in get_changes("input/link_added", joutput) - assert {"type": "removed link"} in get_changes("input/link_removed", joutput) + output = cmd(archiver, f"--repo={repo_location}", "diff", "test0", "test1a") + do_asserts(output, True) + # We expect exit_code=1 due to the chunker params warning + output = cmd(archiver, f"--repo={repo_location}", "diff", "test0", "test1b", "--content-only", exit_code=1) + do_asserts(output, False, content_only=True) - # Symlink replacing or being replaced + output = cmd(archiver, f"--repo={repo_location}", "diff", "test0", "test1a", "--json-lines") + do_json_asserts(output, True) - if not content_only: - assert any( - chg["type"] == "changed mode" and chg["item1"].startswith("d") and chg["item2"].startswith("l") - for chg in get_changes("input/dir_replaced_with_link", joutput) - ), get_changes("input/dir_replaced_with_link", joutput) - assert any( - chg["type"] == "changed mode" and chg["item1"].startswith("l") and chg["item2"].startswith("-") - for chg in get_changes("input/link_replaced_by_file", joutput) - ), get_changes("input/link_replaced_by_file", joutput) - - # Symlink target removed. Should not affect the symlink at all. - assert not any(get_changes("input/link_target_removed", joutput)) - - # The inode has two links and the file contents changed. Borg - # should notice the changes in both links. However, the symlink - # pointing to the file is not changed. - expected = {"type": "modified", "added": 13, "removed": 0} if can_compare_ids else {"type": "modified"} - assert expected in get_changes("input/empty", joutput) - if are_hardlinks_supported(): - assert expected in get_changes("input/hardlink_contents_changed", joutput) - if are_symlinks_supported(): - assert not any(get_changes("input/link_target_contents_changed", joutput)) - - # Added a new file and a hard link to it. Both links to the same - # inode should appear as separate files. - assert {"added": 2048, "removed": 0, "type": "added"} in get_changes("input/file_added", joutput) - if are_hardlinks_supported(): - assert {"added": 2048, "removed": 0, "type": "added"} in get_changes("input/hardlink_added", joutput) - - # check if a diff between nonexistent and empty new file is found - assert {"added": 0, "removed": 0, "type": "added"} in get_changes("input/file_empty_added", joutput) - - # The inode has two links and both of them are deleted. They should - # appear as two deleted files. - assert {"added": 0, "removed": 256, "type": "removed"} in get_changes("input/file_removed", joutput) - if are_hardlinks_supported(): - assert {"added": 0, "removed": 256, "type": "removed"} in get_changes("input/hardlink_removed", joutput) - - if are_hardlinks_supported() and content_only: - # Another link (marked previously as the source in borg) to the - # same inode was removed. This should only change the ctime since removing - # the link would result in the decrementation of the inode's hard-link count. - assert not any(get_changes("input/hardlink_target_removed", joutput)) - - # Another link (marked previously as the source in borg) to the - # same inode was replaced with a new regular file. This should only change - # its ctime. This should not be reflected in the output if content-only is set - assert not any(get_changes("input/hardlink_target_replaced", joutput)) - - output = self.cmd(f"--repo={self.repository_location}", "diff", "test0", "test1a") - do_asserts(output, True) - # We expect exit_code=1 due to the chunker params warning - output = self.cmd( - f"--repo={self.repository_location}", "diff", "test0", "test1b", "--content-only", exit_code=1 - ) - do_asserts(output, False, content_only=True) - - output = self.cmd(f"--repo={self.repository_location}", "diff", "test0", "test1a", "--json-lines") - do_json_asserts(output, True) - - output = self.cmd( - f"--repo={self.repository_location}", "diff", "test0", "test1a", "--json-lines", "--content-only" - ) - do_json_asserts(output, True, content_only=True) - - def test_time_diffs(self): - self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION) - self.create_regular_file("test_file", size=10) - self.cmd(f"--repo={self.repository_location}", "create", "archive1", "input") - time.sleep(0.1) - os.unlink("input/test_file") - if is_win32: - # Sleeping for 15s because Windows doesn't refresh ctime if file is deleted and recreated within 15 seconds. - time.sleep(15) - elif is_darwin: - time.sleep(1) # HFS has a 1s timestamp granularity - self.create_regular_file("test_file", size=15) - self.cmd(f"--repo={self.repository_location}", "create", "archive2", "input") - output = self.cmd( - f"--repo={self.repository_location}", - "diff", - "archive1", - "archive2", - "--format", - "'{mtime}{ctime} {path}{NL}'", - ) - self.assert_in("mtime", output) - self.assert_in("ctime", output) # Should show up on windows as well since it is a new file. - if is_darwin: - time.sleep(1) # HFS has a 1s timestamp granularity - os.chmod("input/test_file", 0o777) - self.cmd(f"--repo={self.repository_location}", "create", "archive3", "input") - output = self.cmd( - f"--repo={self.repository_location}", - "diff", - "archive2", - "archive3", - "--format", - "'{mtime}{ctime} {path}{NL}'", - ) - self.assert_not_in("mtime", output) - # Checking platform because ctime should not be shown on windows since it wasn't recreated. - if not is_win32: - self.assert_in("ctime", output) - else: - self.assert_not_in("ctime", output) - - def test_sort_option(self): - self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION) - - self.create_regular_file("a_file_removed", size=8) - self.create_regular_file("f_file_removed", size=16) - self.create_regular_file("c_file_changed", size=32) - self.create_regular_file("e_file_changed", size=64) - self.cmd(f"--repo={self.repository_location}", "create", "test0", "input") - - os.unlink("input/a_file_removed") - os.unlink("input/f_file_removed") - os.unlink("input/c_file_changed") - os.unlink("input/e_file_changed") - self.create_regular_file("c_file_changed", size=512) - self.create_regular_file("e_file_changed", size=1024) - self.create_regular_file("b_file_added", size=128) - self.create_regular_file("d_file_added", size=256) - self.cmd(f"--repo={self.repository_location}", "create", "test1", "input") - - output = self.cmd(f"--repo={self.repository_location}", "diff", "test0", "test1", "--sort", "--content-only") - expected = [ - "a_file_removed", - "b_file_added", - "c_file_changed", - "d_file_added", - "e_file_changed", - "f_file_removed", - ] - assert isinstance(output, str) - outputs = output.splitlines() - assert len(outputs) == len(expected) - assert all(x in line for x, line in zip(expected, outputs)) + output = cmd(archiver, f"--repo={repo_location}", "diff", "test0", "test1a", "--json-lines", "--content-only") + do_json_asserts(output, True, content_only=True) -class RemoteArchiverTestCase(RemoteArchiverTestCaseBase, ArchiverTestCase): - """run the same tests, but with a remote repository""" +def test_time_diffs(archivers, request): + archiver = request.getfixturevalue(archivers) + repo_location, input_path = archiver.repository_location, archiver.input_path + cmd(archiver, f"--repo={repo_location}", "rcreate", RK_ENCRYPTION) + create_regular_file(input_path, "test_file", size=10) + cmd(archiver, f"--repo={repo_location}", "create", "archive1", "input") + time.sleep(0.1) + os.unlink("input/test_file") + if is_win32: + # Sleeping for 15s because Windows doesn't refresh ctime if file is deleted and recreated within 15 seconds. + time.sleep(15) + elif is_darwin: + time.sleep(1) # HFS has a 1s timestamp granularity + create_regular_file(input_path, "test_file", size=15) + cmd(archiver, f"--repo={repo_location}", "create", "archive2", "input") + output = cmd( + archiver, f"--repo={repo_location}", "diff", "archive1", "archive2", "--format", "'{mtime}{ctime} {path}{NL}'" + ) + assert "mtime" in output + assert "ctime" in output # Should show up on Windows as well since it is a new file. + if is_darwin: + time.sleep(1) # HFS has a 1s timestamp granularity + os.chmod("input/test_file", 0o777) + cmd(archiver, f"--repo={repo_location}", "create", "archive3", "input") + output = cmd( + archiver, f"--repo={repo_location}", "diff", "archive2", "archive3", "--format", "'{mtime}{ctime} {path}{NL}'" + ) + assert "mtime" not in output + # Checking platform because ctime should not be shown on Windows since it wasn't recreated. + if not is_win32: + assert "ctime" in output + else: + assert "ctime" not in output -@unittest.skipUnless("binary" in BORG_EXES, "no borg.exe available") -class ArchiverTestCaseBinary(ArchiverTestCaseBinaryBase, ArchiverTestCase): - """runs the same tests, but via the borg binary""" +def test_sort_option(archivers, request): + archiver = request.getfixturevalue(archivers) + repo_location, input_path = archiver.repository_location, archiver.input_path + cmd(archiver, f"--repo={repo_location}", "rcreate", RK_ENCRYPTION) + + create_regular_file(input_path, "a_file_removed", size=8) + create_regular_file(input_path, "f_file_removed", size=16) + create_regular_file(input_path, "c_file_changed", size=32) + create_regular_file(input_path, "e_file_changed", size=64) + cmd(archiver, f"--repo={repo_location}", "create", "test0", "input") + + os.unlink("input/a_file_removed") + os.unlink("input/f_file_removed") + os.unlink("input/c_file_changed") + os.unlink("input/e_file_changed") + create_regular_file(input_path, "c_file_changed", size=512) + create_regular_file(input_path, "e_file_changed", size=1024) + create_regular_file(input_path, "b_file_added", size=128) + create_regular_file(input_path, "d_file_added", size=256) + cmd(archiver, f"--repo={repo_location}", "create", "test1", "input") + + output = cmd(archiver, f"--repo={repo_location}", "diff", "test0", "test1", "--sort", "--content-only") + expected = ["a_file_removed", "b_file_added", "c_file_changed", "d_file_added", "e_file_changed", "f_file_removed"] + assert isinstance(output, str) + outputs = output.splitlines() + assert len(outputs) == len(expected) + assert all(x in line for x, line in zip(expected, outputs))