Ver Fonte

Merge pull request #7171 from RayyanAnsari/archiver-tests-win

Fix archiver tests on Windows
TW há 2 anos atrás
pai
commit
edb28691d5

+ 1 - 1
docs/usage/tar.rst

@@ -34,7 +34,7 @@ Outputs a script that copies all archives from repo1 to repo2:
 
 
 ::
 ::
 
 
-    for A T in `borg list --format='{archive} {time:%Y-%m-%dT%H:%M:%S}{LF}'`
+    for A T in `borg list --format='{archive} {time:%Y-%m-%dT%H:%M:%S}{NL}'`
     do
     do
       echo "borg -r repo1 export-tar --tar-format=BORG $A - | borg -r repo2 import-tar --timestamp=$T $A -"
       echo "borg -r repo1 export-tar --tar-format=BORG $A - | borg -r repo2 import-tar --timestamp=$T $A -"
     done
     done

+ 7 - 2
src/borg/archiver/create_cmd.py

@@ -28,6 +28,7 @@ from ..helpers import sig_int, ignore_sigint
 from ..helpers import iter_separated
 from ..helpers import iter_separated
 from ..manifest import Manifest
 from ..manifest import Manifest
 from ..patterns import PatternMatcher
 from ..patterns import PatternMatcher
+from ..platform import is_win32
 from ..platform import get_flags
 from ..platform import get_flags
 from ..platform import uid2user, gid2group
 from ..platform import uid2user, gid2group
 
 
@@ -68,7 +69,9 @@ class CreateMixIn:
                 if not dry_run:
                 if not dry_run:
                     try:
                     try:
                         try:
                         try:
-                            proc = subprocess.Popen(args.paths, stdout=subprocess.PIPE, preexec_fn=ignore_sigint)
+                            proc = subprocess.Popen(
+                                args.paths, stdout=subprocess.PIPE, preexec_fn=None if is_win32 else ignore_sigint
+                            )
                         except (FileNotFoundError, PermissionError) as e:
                         except (FileNotFoundError, PermissionError) as e:
                             self.print_error("Failed to execute command: %s", e)
                             self.print_error("Failed to execute command: %s", e)
                             return self.exit_code
                             return self.exit_code
@@ -89,7 +92,9 @@ class CreateMixIn:
                 paths_sep = eval_escapes(args.paths_delimiter) if args.paths_delimiter is not None else "\n"
                 paths_sep = eval_escapes(args.paths_delimiter) if args.paths_delimiter is not None else "\n"
                 if args.paths_from_command:
                 if args.paths_from_command:
                     try:
                     try:
-                        proc = subprocess.Popen(args.paths, stdout=subprocess.PIPE, preexec_fn=ignore_sigint)
+                        proc = subprocess.Popen(
+                            args.paths, stdout=subprocess.PIPE, preexec_fn=None if is_win32 else ignore_sigint
+                        )
                     except (FileNotFoundError, PermissionError) as e:
                     except (FileNotFoundError, PermissionError) as e:
                         self.print_error("Failed to execute command: %s", e)
                         self.print_error("Failed to execute command: %s", e)
                         return self.exit_code
                         return self.exit_code

+ 2 - 0
src/borg/archiver/key_cmds.py

@@ -101,6 +101,8 @@ class KeysMixIn:
             manager.export_paperkey(args.path)
             manager.export_paperkey(args.path)
         else:
         else:
             try:
             try:
+                if os.path.isdir(args.path):
+                    raise IsADirectoryError
                 if args.qr:
                 if args.qr:
                     manager.export_qr(args.path)
                     manager.export_qr(args.path)
                 else:
                 else:

+ 2 - 2
src/borg/helpers/parseformat.py

@@ -593,8 +593,8 @@ class BaseFormatter:
         "TAB": "\t",
         "TAB": "\t",
         "CR": "\r",
         "CR": "\r",
         "NUL": "\0",
         "NUL": "\0",
-        "NEWLINE": os.linesep,
-        "NL": os.linesep,
+        "NEWLINE": "\n",
+        "NL": "\n",  # \n is automatically converted to os.linesep on write
     }
     }
 
 
     def get_item_data(self, item):
     def get_item_data(self, item):

+ 2 - 2
src/borg/helpers/process.py

@@ -341,7 +341,7 @@ def create_filter_process(cmd, stream, stream_close, inbound=True):
                 stdin=filter_stream,
                 stdin=filter_stream,
                 log_prefix="filter-process: ",
                 log_prefix="filter-process: ",
                 env=env,
                 env=env,
-                preexec_fn=ignore_sigint,
+                preexec_fn=None if is_win32 else ignore_sigint,
             )
             )
         else:
         else:
             proc = popen_with_error_handling(
             proc = popen_with_error_handling(
@@ -350,7 +350,7 @@ def create_filter_process(cmd, stream, stream_close, inbound=True):
                 stdout=filter_stream,
                 stdout=filter_stream,
                 log_prefix="filter-process: ",
                 log_prefix="filter-process: ",
                 env=env,
                 env=env,
-                preexec_fn=ignore_sigint,
+                preexec_fn=None if is_win32 else ignore_sigint,
             )
             )
         if not proc:
         if not proc:
             raise Error(f"filter {cmd}: process creation failed")
             raise Error(f"filter {cmd}: process creation failed")

+ 4 - 4
src/borg/platform/windows.pyx

@@ -16,22 +16,22 @@ cdef extern from 'windows.h':
 
 
 @lru_cache(maxsize=None)
 @lru_cache(maxsize=None)
 def uid2user(uid, default=None):
 def uid2user(uid, default=None):
-    return default
+    return "root"
 
 
 
 
 @lru_cache(maxsize=None)
 @lru_cache(maxsize=None)
 def user2uid(user, default=None):
 def user2uid(user, default=None):
-    return default
+    return 0
 
 
 
 
 @lru_cache(maxsize=None)
 @lru_cache(maxsize=None)
 def gid2group(gid, default=None):
 def gid2group(gid, default=None):
-    return default
+    return "root"
 
 
 
 
 @lru_cache(maxsize=None)
 @lru_cache(maxsize=None)
 def group2gid(group, default=None):
 def group2gid(group, default=None):
-    return default
+    return 0
 
 
 
 
 def getosusername():
 def getosusername():

+ 21 - 16
src/borg/testsuite/archiver/__init__.py

@@ -28,6 +28,7 @@ from ...repository import Repository
 from .. import has_lchflags
 from .. import has_lchflags
 from .. import BaseTestCase, changedir, environment_variable
 from .. import BaseTestCase, changedir, environment_variable
 from .. import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported
 from .. import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported
+from ..platform import is_win32
 
 
 RK_ENCRYPTION = "--encryption=repokey-aes-ocb"
 RK_ENCRYPTION = "--encryption=repokey-aes-ocb"
 KF_ENCRYPTION = "--encryption=keyfile-chacha20-poly1305"
 KF_ENCRYPTION = "--encryption=keyfile-chacha20-poly1305"
@@ -230,23 +231,27 @@ class ArchiverTestCaseBase(BaseTestCase):
             os.mkfifo(os.path.join(self.input_path, "fifo1"))
             os.mkfifo(os.path.join(self.input_path, "fifo1"))
         if has_lchflags:
         if has_lchflags:
             platform.set_flags(os.path.join(self.input_path, "flagfile"), stat.UF_NODUMP)
             platform.set_flags(os.path.join(self.input_path, "flagfile"), stat.UF_NODUMP)
-        try:
-            # Block device
-            os.mknod("input/bdev", 0o600 | stat.S_IFBLK, os.makedev(10, 20))
-            # Char device
-            os.mknod("input/cdev", 0o600 | stat.S_IFCHR, os.makedev(30, 40))
-            # File owner
-            os.chown("input/file1", 100, 200)  # raises OSError invalid argument on cygwin
-            # File mode
-            os.chmod("input/dir2", 0o555)  # if we take away write perms, we need root to remove contents
-            have_root = True  # we have (fake)root
-        except PermissionError:
-            have_root = False
-        except OSError as e:
-            # Note: ENOSYS "Function not implemented" happens as non-root on Win 10 Linux Subsystem.
-            if e.errno not in (errno.EINVAL, errno.ENOSYS):
-                raise
+
+        if is_win32:
             have_root = False
             have_root = False
+        else:
+            try:
+                # Block device
+                os.mknod("input/bdev", 0o600 | stat.S_IFBLK, os.makedev(10, 20))
+                # Char device
+                os.mknod("input/cdev", 0o600 | stat.S_IFCHR, os.makedev(30, 40))
+                # File owner
+                os.chown("input/file1", 100, 200)  # raises OSError invalid argument on cygwin
+                # File mode
+                os.chmod("input/dir2", 0o555)  # if we take away write perms, we need root to remove contents
+                have_root = True  # we have (fake)root
+            except PermissionError:
+                have_root = False
+            except OSError as e:
+                # Note: ENOSYS "Function not implemented" happens as non-root on Win 10 Linux Subsystem.
+                if e.errno not in (errno.EINVAL, errno.ENOSYS):
+                    raise
+                have_root = False
         time.sleep(1)  # "empty" must have newer timestamp than other files
         time.sleep(1)  # "empty" must have newer timestamp than other files
         self.create_regular_file("empty", size=0)
         self.create_regular_file("empty", size=0)
         return have_root
         return have_root

+ 2 - 2
src/borg/testsuite/archiver/check_cmd.py

@@ -71,7 +71,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase):
         self.assert_in("New missing file chunk detected", output)
         self.assert_in("New missing file chunk detected", output)
         self.cmd(f"--repo={self.repository_location}", "check", exit_code=0)
         self.cmd(f"--repo={self.repository_location}", "check", exit_code=0)
         output = self.cmd(
         output = self.cmd(
-            f"--repo={self.repository_location}", "list", "archive1", "--format={health}#{path}{LF}", exit_code=0
+            f"--repo={self.repository_location}", "list", "archive1", "--format={health}#{path}{NL}", exit_code=0
         )
         )
         self.assert_in("broken#", output)
         self.assert_in("broken#", output)
         # check that the file in the old archives has now a different chunk list without the killed chunk
         # check that the file in the old archives has now a different chunk list without the killed chunk
@@ -104,7 +104,7 @@ class ArchiverCheckTestCase(ArchiverTestCaseBase):
                     self.fail("should not happen")
                     self.fail("should not happen")
         # list is also all-healthy again
         # list is also all-healthy again
         output = self.cmd(
         output = self.cmd(
-            f"--repo={self.repository_location}", "list", "archive1", "--format={health}#{path}{LF}", exit_code=0
+            f"--repo={self.repository_location}", "list", "archive1", "--format={health}#{path}{NL}", exit_code=0
         )
         )
         self.assert_not_in("broken#", output)
         self.assert_not_in("broken#", output)
 
 

+ 3 - 3
src/borg/testsuite/archiver/config_cmd.py

@@ -24,17 +24,17 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.assert_in("No option ", output)
         self.assert_in("No option ", output)
         self.cmd(f"--repo={self.repository_location}", "config", "last_segment_checked", "123")
         self.cmd(f"--repo={self.repository_location}", "config", "last_segment_checked", "123")
         output = self.cmd(f"--repo={self.repository_location}", "config", "last_segment_checked")
         output = self.cmd(f"--repo={self.repository_location}", "config", "last_segment_checked")
-        assert output == "123" + "\n"
+        assert output == "123" + os.linesep
         output = self.cmd(f"--repo={self.repository_location}", "config", "--list")
         output = self.cmd(f"--repo={self.repository_location}", "config", "--list")
         self.assert_in("last_segment_checked", output)
         self.assert_in("last_segment_checked", output)
         self.cmd(f"--repo={self.repository_location}", "config", "--delete", "last_segment_checked")
         self.cmd(f"--repo={self.repository_location}", "config", "--delete", "last_segment_checked")
 
 
         for cfg_key, cfg_value in [("additional_free_space", "2G"), ("repository.append_only", "1")]:
         for cfg_key, cfg_value in [("additional_free_space", "2G"), ("repository.append_only", "1")]:
             output = self.cmd(f"--repo={self.repository_location}", "config", cfg_key)
             output = self.cmd(f"--repo={self.repository_location}", "config", cfg_key)
-            assert output == "0" + "\n"
+            assert output == "0" + os.linesep
             self.cmd(f"--repo={self.repository_location}", "config", cfg_key, cfg_value)
             self.cmd(f"--repo={self.repository_location}", "config", cfg_key, cfg_value)
             output = self.cmd(f"--repo={self.repository_location}", "config", cfg_key)
             output = self.cmd(f"--repo={self.repository_location}", "config", cfg_key)
-            assert output == cfg_value + "\n"
+            assert output == cfg_value + os.linesep
             self.cmd(f"--repo={self.repository_location}", "config", "--delete", cfg_key)
             self.cmd(f"--repo={self.repository_location}", "config", "--delete", cfg_key)
             self.cmd(f"--repo={self.repository_location}", "config", cfg_key, exit_code=1)
             self.cmd(f"--repo={self.repository_location}", "config", cfg_key, exit_code=1)
 
 

+ 1 - 1
src/borg/testsuite/archiver/corruption.py

@@ -60,7 +60,7 @@ class ArchiverCorruptionTestCase(ArchiverTestCaseBase):
     def test_chunks_archive(self):
     def test_chunks_archive(self):
         self.cmd(f"--repo={self.repository_location}", "create", "test1", "input")
         self.cmd(f"--repo={self.repository_location}", "create", "test1", "input")
         # Find ID of test1 so we can corrupt it later :)
         # Find ID of test1 so we can corrupt it later :)
-        target_id = self.cmd(f"--repo={self.repository_location}", "rlist", "--format={id}{LF}").strip()
+        target_id = self.cmd(f"--repo={self.repository_location}", "rlist", "--format={id}{NL}").strip()
         self.cmd(f"--repo={self.repository_location}", "create", "test2", "input")
         self.cmd(f"--repo={self.repository_location}", "create", "test2", "input")
 
 
         # Force cache sync, creating archive chunks of test1 and test2 in chunks.archive.d
         # Force cache sync, creating archive chunks of test1 and test2 in chunks.archive.d

+ 25 - 8
src/borg/testsuite/archiver/create_cmd.py

@@ -13,7 +13,7 @@ import pytest
 from ... import platform
 from ... import platform
 from ...constants import *  # NOQA
 from ...constants import *  # NOQA
 from ...manifest import Manifest
 from ...manifest import Manifest
-from ...platform import is_cygwin
+from ...platform import is_cygwin, is_win32
 from ...repository import Repository
 from ...repository import Repository
 from .. import has_lchflags
 from .. import has_lchflags
 from .. import changedir
 from .. import changedir
@@ -119,6 +119,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         # we have all fs items exactly once!
         # we have all fs items exactly once!
         assert sorted(paths) == ["input", "input/a", "input/a/hardlink", "input/b", "input/b/hardlink"]
         assert sorted(paths) == ["input", "input/a", "input/a/hardlink", "input/b", "input/b/hardlink"]
 
 
+    @pytest.mark.skipif(is_win32, reason="unix sockets not available on windows")
     def test_unix_socket(self):
     def test_unix_socket(self):
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
         try:
         try:
@@ -204,14 +205,14 @@ class ArchiverTestCase(ArchiverTestCaseBase):
             "exit 73;",
             "exit 73;",
             exit_code=2,
             exit_code=2,
         )
         )
-        assert output.endswith("Command 'sh' exited with status 73\n")
+        assert output.endswith("Command 'sh' exited with status 73" + os.linesep)
         archive_list = json.loads(self.cmd(f"--repo={self.repository_location}", "rlist", "--json"))
         archive_list = json.loads(self.cmd(f"--repo={self.repository_location}", "rlist", "--json"))
         assert archive_list["archives"] == []
         assert archive_list["archives"] == []
 
 
     def test_create_content_from_command_missing_command(self):
     def test_create_content_from_command_missing_command(self):
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
         output = self.cmd(f"--repo={self.repository_location}", "create", "test", "--content-from-command", exit_code=2)
         output = self.cmd(f"--repo={self.repository_location}", "create", "test", "--content-from-command", exit_code=2)
-        assert output.endswith("No command given.\n")
+        assert output.endswith("No command given." + os.linesep)
 
 
     def test_create_paths_from_stdin(self):
     def test_create_paths_from_stdin(self):
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
@@ -242,9 +243,20 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.create_regular_file("file4", size=1024 * 80)
         self.create_regular_file("file4", size=1024 * 80)
 
 
         input_data = "input/file1\ninput/file2\ninput/file3"
         input_data = "input/file1\ninput/file2\ninput/file3"
+        if is_win32:
+            with open("filenames.cmd", "w") as script:
+                for filename in input_data.splitlines():
+                    script.write(f"@echo {filename}\n")
         self.cmd(
         self.cmd(
-            f"--repo={self.repository_location}", "create", "--paths-from-command", "test", "--", "echo", input_data
+            f"--repo={self.repository_location}",
+            "create",
+            "--paths-from-command",
+            "test",
+            "--",
+            "filenames.cmd" if is_win32 else "echo",
+            input_data,
         )
         )
+
         archive_list = self.cmd(f"--repo={self.repository_location}", "list", "test", "--json-lines")
         archive_list = self.cmd(f"--repo={self.repository_location}", "list", "test", "--json-lines")
         paths = [json.loads(line)["path"] for line in archive_list.split("\n") if line]
         paths = [json.loads(line)["path"] for line in archive_list.split("\n") if line]
         assert paths == ["input/file1", "input/file2", "input/file3"]
         assert paths == ["input/file1", "input/file2", "input/file3"]
@@ -262,14 +274,14 @@ class ArchiverTestCase(ArchiverTestCaseBase):
             "exit 73;",
             "exit 73;",
             exit_code=2,
             exit_code=2,
         )
         )
-        assert output.endswith("Command 'sh' exited with status 73\n")
+        assert output.endswith("Command 'sh' exited with status 73" + os.linesep)
         archive_list = json.loads(self.cmd(f"--repo={self.repository_location}", "rlist", "--json"))
         archive_list = json.loads(self.cmd(f"--repo={self.repository_location}", "rlist", "--json"))
         assert archive_list["archives"] == []
         assert archive_list["archives"] == []
 
 
     def test_create_paths_from_command_missing_command(self):
     def test_create_paths_from_command_missing_command(self):
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
         output = self.cmd(f"--repo={self.repository_location}", "create", "test", "--paths-from-command", exit_code=2)
         output = self.cmd(f"--repo={self.repository_location}", "create", "test", "--paths-from-command", exit_code=2)
-        assert output.endswith("No command given.\n")
+        assert output.endswith("No command given." + os.linesep)
 
 
     def test_create_without_root(self):
     def test_create_without_root(self):
         """test create without a root"""
         """test create without a root"""
@@ -500,6 +512,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd(f"--repo={self.repository_location}", "create", "test", "input", "input")
         self.cmd(f"--repo={self.repository_location}", "create", "test", "input", "input")
 
 
     @pytest.mark.skipif("BORG_TESTS_IGNORE_MODES" in os.environ, reason="modes unreliable")
     @pytest.mark.skipif("BORG_TESTS_IGNORE_MODES" in os.environ, reason="modes unreliable")
+    @pytest.mark.skipif(is_win32, reason="modes unavailable on Windows")
     def test_umask(self):
     def test_umask(self):
         self.create_regular_file("file1", size=1024 * 80)
         self.create_regular_file("file1", size=1024 * 80)
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
@@ -545,6 +558,9 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         # https://borgbackup.readthedocs.org/en/latest/faq.html#i-am-seeing-a-added-status-for-a-unchanged-file
         # https://borgbackup.readthedocs.org/en/latest/faq.html#i-am-seeing-a-added-status-for-a-unchanged-file
         self.assert_in("A input/file2", output)
         self.assert_in("A input/file2", output)
 
 
+    @pytest.mark.skipif(
+        is_win32, reason="ctime attribute is file creation time on Windows"
+    )  # see https://docs.python.org/3/library/os.html#os.stat_result.st_ctime
     def test_file_status_cs_cache_mode(self):
     def test_file_status_cs_cache_mode(self):
         """test that a changed file with faked "previous" mtime still gets backed up in ctime,size cache_mode"""
         """test that a changed file with faked "previous" mtime still gets backed up in ctime,size cache_mode"""
         self.create_regular_file("file1", contents=b"123")
         self.create_regular_file("file1", contents=b"123")
@@ -740,6 +756,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
                 extracted_data = f.read()
                 extracted_data = f.read()
         assert extracted_data == data
         assert extracted_data == data
 
 
+    @pytest.mark.skipif(not are_symlinks_supported(), reason="symlinks not supported")
     def test_create_read_special_broken_symlink(self):
     def test_create_read_special_broken_symlink(self):
         os.symlink("somewhere does not exist", os.path.join(self.input_path, "link"))
         os.symlink("somewhere does not exist", os.path.join(self.input_path, "link"))
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
@@ -784,7 +801,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
 
 
         # Test case set up: create a repository and a file
         # Test case set up: create a repository and a file
         self.cmd(f"--repo={self.repository_location}", "rcreate", "--encryption=none")
         self.cmd(f"--repo={self.repository_location}", "rcreate", "--encryption=none")
-        self.create_regular_file("testfile", contents=randbytes(15000000))  # more data might be needed for faster CPUs
+        self.create_regular_file("testfile", contents=randbytes(50000000))
         # Archive
         # Archive
         result = self.cmd(f"--repo={self.repository_location}", "create", "--stats", "test_archive", self.input_path)
         result = self.cmd(f"--repo={self.repository_location}", "create", "--stats", "test_archive", self.input_path)
         hashing_time = extract_hashing_time(result)
         hashing_time = extract_hashing_time(result)
@@ -802,7 +819,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
 
 
         # Test case set up: create a repository and a file
         # Test case set up: create a repository and a file
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
-        self.create_regular_file("testfile", contents=randbytes(10000000))
+        self.create_regular_file("testfile", contents=randbytes(50000000))
         # Archive
         # Archive
         result = self.cmd(f"--repo={self.repository_location}", "create", "--stats", "test_archive", self.input_path)
         result = self.cmd(f"--repo={self.repository_location}", "create", "--stats", "test_archive", self.input_path)
         chunking_time = extract_chunking_time(result)
         chunking_time = extract_chunking_time(result)

+ 1 - 1
src/borg/testsuite/archiver/debug_cmds.py

@@ -107,7 +107,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
 
 
         # Invalid IDs do not abort or return an error
         # Invalid IDs do not abort or return an error
         output = self.cmd(f"--repo={self.repository_location}", "debug", "refcount-obj", "124", "xyza").strip()
         output = self.cmd(f"--repo={self.repository_location}", "debug", "refcount-obj", "124", "xyza").strip()
-        assert output == "object id 124 is invalid.\nobject id xyza is invalid."
+        assert output == "object id 124 is invalid." + os.linesep + "object id xyza is invalid."
 
 
     def test_debug_info(self):
     def test_debug_info(self):
         output = self.cmd("debug", "info")
         output = self.cmd("debug", "info")

+ 3 - 2
src/borg/testsuite/archiver/diff_cmd.py

@@ -5,6 +5,7 @@ import unittest
 
 
 from ...constants import *  # NOQA
 from ...constants import *  # NOQA
 from .. import are_symlinks_supported, are_hardlinks_supported
 from .. import are_symlinks_supported, are_hardlinks_supported
+from ..platform import is_win32
 from . import ArchiverTestCaseBase, RemoteArchiverTestCaseBase, ArchiverTestCaseBinaryBase, RK_ENCRYPTION, BORG_EXES
 from . import ArchiverTestCaseBase, RemoteArchiverTestCaseBase, ArchiverTestCaseBinaryBase, RK_ENCRYPTION, BORG_EXES
 
 
 
 
@@ -79,7 +80,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
             assert "input/file_unchanged" not in output
             assert "input/file_unchanged" not in output
 
 
             # Directory replaced with a regular file
             # Directory replaced with a regular file
-            if "BORG_TESTS_IGNORE_MODES" not in os.environ:
+            if "BORG_TESTS_IGNORE_MODES" not in os.environ and not is_win32:
                 assert "[drwxr-xr-x -> -rwxr-xr-x] input/dir_replaced_with_file" in output
                 assert "[drwxr-xr-x -> -rwxr-xr-x] input/dir_replaced_with_file" in output
 
 
             # Basic directory cases
             # Basic directory cases
@@ -153,7 +154,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
             assert not any(get_changes("input/file_unchanged", joutput))
             assert not any(get_changes("input/file_unchanged", joutput))
 
 
             # Directory replaced with a regular file
             # Directory replaced with a regular file
-            if "BORG_TESTS_IGNORE_MODES" not in os.environ:
+            if "BORG_TESTS_IGNORE_MODES" not in os.environ and not is_win32:
                 assert {"type": "mode", "old_mode": "drwxr-xr-x", "new_mode": "-rwxr-xr-x"} in get_changes(
                 assert {"type": "mode", "old_mode": "drwxr-xr-x", "new_mode": "-rwxr-xr-x"} in get_changes(
                     "input/dir_replaced_with_file", joutput
                     "input/dir_replaced_with_file", joutput
                 )
                 )

+ 3 - 4
src/borg/testsuite/archiver/extract_cmd.py

@@ -309,19 +309,18 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.create_regular_file("file3", size=1024 * 80)
         self.create_regular_file("file3", size=1024 * 80)
         self.create_regular_file("file4", size=1024 * 80)
         self.create_regular_file("file4", size=1024 * 80)
         self.create_regular_file("file333", size=1024 * 80)
         self.create_regular_file("file333", size=1024 * 80)
-        self.create_regular_file("aa:something", size=1024 * 80)
 
 
         # Create while excluding using mixed pattern styles
         # Create while excluding using mixed pattern styles
         with open(self.exclude_file_path, "wb") as fd:
         with open(self.exclude_file_path, "wb") as fd:
             fd.write(b"re:input/file4$\n")
             fd.write(b"re:input/file4$\n")
-            fd.write(b"fm:*aa:*thing\n")
+            fd.write(b"fm:*file3*\n")
 
 
         self.cmd(
         self.cmd(
             f"--repo={self.repository_location}", "create", "--exclude-from=" + self.exclude_file_path, "test", "input"
             f"--repo={self.repository_location}", "create", "--exclude-from=" + self.exclude_file_path, "test", "input"
         )
         )
         with changedir("output"):
         with changedir("output"):
             self.cmd(f"--repo={self.repository_location}", "extract", "test")
             self.cmd(f"--repo={self.repository_location}", "extract", "test")
-        self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2", "file3", "file333"])
+        self.assert_equal(sorted(os.listdir("output/input")), ["file1", "file2"])
         shutil.rmtree("output/input")
         shutil.rmtree("output/input")
 
 
         # Exclude using regular expression
         # Exclude using regular expression
@@ -346,7 +345,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
             self.cmd(
             self.cmd(
                 f"--repo={self.repository_location}", "extract", "test", "--exclude-from=" + self.exclude_file_path
                 f"--repo={self.repository_location}", "extract", "test", "--exclude-from=" + self.exclude_file_path
             )
             )
-        self.assert_equal(sorted(os.listdir("output/input")), ["file3"])
+        self.assert_equal(sorted(os.listdir("output/input")), [])
 
 
     def test_extract_with_pattern(self):
     def test_extract_with_pattern(self):
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)

+ 3 - 2
src/borg/testsuite/archiver/info_cmd.py

@@ -1,4 +1,5 @@
 import json
 import json
+import os
 import unittest
 import unittest
 
 
 from ...constants import *  # NOQA
 from ...constants import *  # NOQA
@@ -18,9 +19,9 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
         self.cmd(f"--repo={self.repository_location}", "create", "test", "input")
         self.cmd(f"--repo={self.repository_location}", "create", "test", "input")
         info_archive = self.cmd(f"--repo={self.repository_location}", "info", "-a", "test")
         info_archive = self.cmd(f"--repo={self.repository_location}", "info", "-a", "test")
-        assert "Archive name: test\n" in info_archive
+        assert "Archive name: test" + os.linesep in info_archive
         info_archive = self.cmd(f"--repo={self.repository_location}", "info", "--first", "1")
         info_archive = self.cmd(f"--repo={self.repository_location}", "info", "--first", "1")
-        assert "Archive name: test\n" in info_archive
+        assert "Archive name: test" + os.linesep in info_archive
 
 
     def test_info_json(self):
     def test_info_json(self):
         self.create_regular_file("file1", size=1024 * 80)
         self.create_regular_file("file1", size=1024 * 80)

+ 10 - 0
src/borg/testsuite/archiver/key_cmds.py

@@ -163,6 +163,16 @@ class ArchiverTestCase(ArchiverTestCaseBase):
 
 
         self.cmd(f"--repo={self.repository_location}", "key", "export", export_directory, exit_code=EXIT_ERROR)
         self.cmd(f"--repo={self.repository_location}", "key", "export", export_directory, exit_code=EXIT_ERROR)
 
 
+    def test_key_export_qr_directory(self):
+        export_directory = self.output_path + "/exported"
+        os.mkdir(export_directory)
+
+        self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
+
+        self.cmd(
+            f"--repo={self.repository_location}", "key", "export", "--qr-html", export_directory, exit_code=EXIT_ERROR
+        )
+
     def test_key_import_errors(self):
     def test_key_import_errors(self):
         export_file = self.output_path + "/exported"
         export_file = self.output_path + "/exported"
         self.cmd(f"--repo={self.repository_location}", "rcreate", KF_ENCRYPTION)
         self.cmd(f"--repo={self.repository_location}", "rcreate", KF_ENCRYPTION)

+ 2 - 2
src/borg/testsuite/archiver/recreate_cmd.py

@@ -265,7 +265,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd(f"--repo={self.repository_location}", "create", "test2", "input", "--comment", "this is the comment")
         self.cmd(f"--repo={self.repository_location}", "create", "test2", "input", "--comment", "this is the comment")
         self.cmd(f"--repo={self.repository_location}", "create", "test3", "input", "--comment", '"deleted" comment')
         self.cmd(f"--repo={self.repository_location}", "create", "test3", "input", "--comment", '"deleted" comment')
         self.cmd(f"--repo={self.repository_location}", "create", "test4", "input", "--comment", "preserved comment")
         self.cmd(f"--repo={self.repository_location}", "create", "test4", "input", "--comment", "preserved comment")
-        assert "Comment: \n" in self.cmd(f"--repo={self.repository_location}", "info", "-a", "test1")
+        assert "Comment: " + os.linesep in self.cmd(f"--repo={self.repository_location}", "info", "-a", "test1")
         assert "Comment: this is the comment" in self.cmd(f"--repo={self.repository_location}", "info", "-a", "test2")
         assert "Comment: this is the comment" in self.cmd(f"--repo={self.repository_location}", "info", "-a", "test2")
 
 
         self.cmd(f"--repo={self.repository_location}", "recreate", "-a", "test1", "--comment", "added comment")
         self.cmd(f"--repo={self.repository_location}", "recreate", "-a", "test1", "--comment", "added comment")
@@ -274,7 +274,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd(f"--repo={self.repository_location}", "recreate", "-a", "test4", "12345")
         self.cmd(f"--repo={self.repository_location}", "recreate", "-a", "test4", "12345")
         assert "Comment: added comment" in self.cmd(f"--repo={self.repository_location}", "info", "-a", "test1")
         assert "Comment: added comment" in self.cmd(f"--repo={self.repository_location}", "info", "-a", "test1")
         assert "Comment: modified comment" in self.cmd(f"--repo={self.repository_location}", "info", "-a", "test2")
         assert "Comment: modified comment" in self.cmd(f"--repo={self.repository_location}", "info", "-a", "test2")
-        assert "Comment: \n" in self.cmd(f"--repo={self.repository_location}", "info", "-a", "test3")
+        assert "Comment: " + os.linesep in self.cmd(f"--repo={self.repository_location}", "info", "-a", "test3")
         assert "Comment: preserved comment" in self.cmd(f"--repo={self.repository_location}", "info", "-a", "test4")
         assert "Comment: preserved comment" in self.cmd(f"--repo={self.repository_location}", "info", "-a", "test4")
 
 
 
 

+ 4 - 3
src/borg/testsuite/archiver/rlist_cmd.py

@@ -1,4 +1,5 @@
 import json
 import json
+import os
 import unittest
 import unittest
 
 
 from ...constants import *  # NOQA
 from ...constants import *  # NOQA
@@ -34,10 +35,10 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         )
         )
         self.assertEqual(output_1, output_2)
         self.assertEqual(output_1, output_2)
         output_1 = self.cmd(f"--repo={self.repository_location}", "rlist", "--short")
         output_1 = self.cmd(f"--repo={self.repository_location}", "rlist", "--short")
-        self.assertEqual(output_1, "test-1\ntest-2\n")
+        self.assertEqual(output_1, "test-1" + os.linesep + "test-2" + os.linesep)
         output_3 = self.cmd(f"--repo={self.repository_location}", "rlist", "--format", "{name} {comment}{NL}")
         output_3 = self.cmd(f"--repo={self.repository_location}", "rlist", "--format", "{name} {comment}{NL}")
-        self.assert_in("test-1 comment 1\n", output_3)
-        self.assert_in("test-2 comment 2\n", output_3)
+        self.assert_in("test-1 comment 1" + os.linesep, output_3)
+        self.assert_in("test-2 comment 2" + os.linesep, output_3)
 
 
     def test_rlist_consider_checkpoints(self):
     def test_rlist_consider_checkpoints(self):
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
         self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)

+ 9 - 1
src/borg/testsuite/archiver/transfer_cmd.py

@@ -7,6 +7,7 @@ import unittest
 
 
 from ...constants import *  # NOQA
 from ...constants import *  # NOQA
 from ...helpers.time import parse_timestamp
 from ...helpers.time import parse_timestamp
+from ..platform import is_win32
 from . import ArchiverTestCaseBase, RemoteArchiverTestCaseBase, ArchiverTestCaseBinaryBase, RK_ENCRYPTION, BORG_EXES
 from . import ArchiverTestCaseBase, RemoteArchiverTestCaseBase, ArchiverTestCaseBinaryBase, RK_ENCRYPTION, BORG_EXES
 
 
 
 
@@ -151,6 +152,11 @@ class ArchiverTestCase(ArchiverTestCaseBase):
                     # Note: size == 0 for all items without a size or chunks list (like e.g. directories)
                     # Note: size == 0 for all items without a size or chunks list (like e.g. directories)
                     # Note: healthy == True indicates the *absence* of the additional chunks_healthy list
                     # Note: healthy == True indicates the *absence* of the additional chunks_healthy list
                 del g["hlid"]
                 del g["hlid"]
+
+                if e["type"] == "b" and is_win32:
+                    # The S_IFBLK macro is broken on MINGW
+                    del e["type"], g["type"]
+                    del e["mode"], g["mode"]
                 assert g == e
                 assert g == e
 
 
             if name == "archive1":
             if name == "archive1":
@@ -250,7 +256,9 @@ class ArchiverTestCase(ArchiverTestCaseBase):
                         assert item.group in ("root", "wheel")
                         assert item.group in ("root", "wheel")
                         assert "hlid" not in item
                         assert "hlid" not in item
                     elif item.path.endswith("bdev_12_34"):
                     elif item.path.endswith("bdev_12_34"):
-                        assert stat.S_ISBLK(item.mode)
+                        if not is_win32:
+                            # The S_IFBLK macro is broken on MINGW
+                            assert stat.S_ISBLK(item.mode)
                         # looks like we can't use os.major/minor with data coming from another platform,
                         # looks like we can't use os.major/minor with data coming from another platform,
                         # thus we only do a rather rough check here:
                         # thus we only do a rather rough check here:
                         assert "rdev" in item and item.rdev != 0
                         assert "rdev" in item and item.rdev != 0