Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/borg/testsuite/archiver/diff_cmd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pathlib import Path
import stat
import time
import pytest

from ...constants import * # NOQA
from .. import are_symlinks_supported, are_hardlinks_supported
Expand Down Expand Up @@ -318,3 +319,88 @@ def test_sort_option(archivers, request):
outputs = output.splitlines()
assert len(outputs) == len(expected)
assert all(x in line for x, line in zip(expected, outputs))


@pytest.mark.skipif(not are_hardlinks_supported(), reason="hardlinks not supported")
def test_hard_link_deletion_and_replacement(archivers, request):
archiver = request.getfixturevalue(archivers)

# repo-create changes umask, so create the repo first to avoid any
# unexpected permission changes.
cmd(archiver, "repo-create", RK_ENCRYPTION)
path_a = os.path.join(archiver.input_path, "a")
path_b = os.path.join(archiver.input_path, "b")
os.mkdir(path_a)
os.mkdir(path_b)
hl_a = os.path.join(path_a, "hardlink")
hl_b = os.path.join(path_b, "hardlink")
create_regular_file(archiver.input_path, hl_a, contents=b"123456")
os.link(hl_a, hl_b)

cmd(archiver, "create", "test0", "input")
os.unlink(hl_a) # Don't duplicate warning message - one is enough.
cmd(archiver, "create", "test1", "input")

# Moral equivalent of test_multiple_link_exclusion in borg v1.x... see #8344
# Borg v2 doesn't have this issue comparing hard-links, so we'll defer to
# POSIX behavior:
# https://pubs.opengroup.org/onlinepubs/9799919799/functions/unlink.html
# Upon successful completion, unlink() shall mark for update the last data modification
# and last file status change timestamps of the parent directory. Also, if the
# file's link count is not 0, the last file status change timestamp of the
# file shall be marked for update.
output = cmd(
archiver, "diff", "--pattern=+ fm:input/b", "--pattern=! **/", "test0", "test1", exit_code=EXIT_SUCCESS
)
lines = output.splitlines()
# Directory was excluded.
assert_line_not_exists(lines, "input/a$")
# Remaining hardlink
assert_line_exists(lines, "ctime:.*input/b/hardlink")
assert_line_not_exists(lines, ".*mtime:.*input/b/hardlink")
# Deleted hardlink was excluded
assert_line_not_exists(lines, "input/a/hardlink$")

# Now try again, except with no patterns!
output = cmd(archiver, "diff", "test0", "test1", exit_code=EXIT_SUCCESS)
lines = output.splitlines()
# Directory... preferably, let's not care about order differences are presented.
assert_line_exists(lines, "[cm]time:.*[cm]time:.*input/a")
# Remaining hardlink
assert_line_exists(lines, "ctime:.*input/b/hardlink")
assert_line_not_exists(lines, ".*mtime:.*input/b/hardlink")
# Deleted hardlink
assert_line_exists(lines, "removed:.*input/a/hardlink")

# Now recreate the unlinked file as a different entity with identical
# contents.
create_regular_file(archiver.input_path, hl_a, contents=b"123456")
cmd(archiver, "create", "test2", "input")

# Compare test0 and test2.
output = cmd(archiver, "diff", "test0", "test2", exit_code=EXIT_SUCCESS)
lines = output.splitlines()
# Adding a file changes c/mtime.
assert_line_exists(lines, "[cm]time:.*[cm]time:.*input/a$")
# Different c/mtime but no apparent changes (i.e. perms) or content
# modifications should be a hint that something hard-link related is going on.
assert_line_exists(lines, "[cm]time:.*[cm]time:.*input/a/hardlink")
assert_line_not_exists(lines, "modified.*B.*input/a/hardlink")
assert_line_not_exists(lines, "-[r-][w-][x-].*input/a/hardlink")
# ctime changed because the hard-link count went down. But no mtime changes
# because file content isn't modified. No permissions changes either.
# This is another hint that something hard-link related changed.
assert_line_exists(lines, "ctime:.*input/b/hardlink")
assert_line_not_exists(lines, ".*mtime:.*input/b/hardlink")
assert_line_not_exists(lines, "modified.*B.*input/b/hardlink")
assert_line_not_exists(lines, "-[r-][w-][x-].*input/b/hardlink")

# Finally, compare test1 and test2.
output = cmd(archiver, "diff", "test1", "test2", exit_code=EXIT_SUCCESS)
lines = output.splitlines()
# Same situation applies as previous diff for a.
assert_line_exists(lines, "[cm]time:.*[cm]time:.*input/a$")
# From test1 to test2's POV, the a/hardlink file is a fresh new file.
assert_line_exists(lines, "added.*B.*input/a/hardlink")
# But the b/hardlink file was not modified at all.
assert_line_not_exists(lines, ".*input/b/hardlink")
Loading