Skip to content

Commit 6a72b12

Browse files
Merge pull request borgbackup#9272 from ThomasWaldmann/windows-ci
windows CI
2 parents c7073ca + 85b2ca2 commit 6a72b12

File tree

10 files changed

+85
-70
lines changed

10 files changed

+85
-70
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -544,10 +544,10 @@ jobs:
544544

545545
windows_tests:
546546

547-
if: false # can be used to temporarily disable the build
547+
if: true # can be used to temporarily disable the build
548548
runs-on: windows-latest
549-
timeout-minutes: 120
550-
needs: native_tests
549+
timeout-minutes: 90
550+
needs: [lint, security]
551551

552552
env:
553553
PY_COLORS: 1
@@ -583,14 +583,15 @@ jobs:
583583
# build borg.exe
584584
. env/bin/activate
585585
pip install -e .
586-
pyinstaller -y scripts/borg.exe.spec
586+
mkdir -p dist/binary
587+
pyinstaller -y --clean --distpath=dist/binary scripts/borg.exe.spec
587588
# build sdist and wheel in dist/...
588589
python -m build
589590
590591
- uses: actions/upload-artifact@v4
591592
with:
592593
name: borg-windows
593-
path: dist/borg.exe
594+
path: dist/binary/borg.exe
594595

595596
- name: Run tests
596597
run: |

src/borg/helpers/parseformat.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from string import Formatter
1919

2020
from ..logger import create_logger
21+
from ..platformflags import is_win32
2122

2223
logger = create_logger()
2324

@@ -453,8 +454,8 @@ class Location:
453454
(?P<path>.+)
454455
"""
455456

456-
# abs_path must start with a slash.
457-
abs_path_re = r"(?P<path>/.+)"
457+
# abs_path must start with a slash (or drive letter on Windows).
458+
abs_path_re = r"(?P<path>[A-Za-z]:/.+)" if is_win32 else r"(?P<path>/.+)"
458459

459460
# path may or may not start with a slash.
460461
abs_or_rel_path_re = r"(?P<path>.+)"
@@ -493,7 +494,8 @@ class Location:
493494

494495
rclone_re = re.compile(r"(?P<proto>rclone):(?P<path>(.*))", re.VERBOSE)
495496

496-
file_or_socket_re = re.compile(r"(?P<proto>(file|socket))://" + abs_path_re, re.VERBOSE)
497+
sl = "/" if is_win32 else ""
498+
file_or_socket_re = re.compile(r"(?P<proto>(file|socket))://" + sl + abs_path_re, re.VERBOSE)
497499

498500
local_re = re.compile(local_path_re, re.VERBOSE)
499501

src/borg/legacyrepository.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from .repoobj import RepoObj
2828
from .checksums import crc32, StreamingXXH64
2929
from .crypto.file_integrity import IntegrityCheckedFile, FileIntegrityError
30+
from .repository import _local_abspath_to_file_url
3031

3132
logger = create_logger(__name__)
3233

@@ -191,7 +192,7 @@ class PathPermissionDenied(Error):
191192

192193
def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=True, send_log_cb=None):
193194
self.path = os.path.abspath(path)
194-
self._location = Location("file://%s" % self.path)
195+
self._location = Location(_local_abspath_to_file_url(self.path))
195196
self.version = None
196197
# long-running repository methods which emit log or progress output are responsible for calling
197198
# the ._send_log method periodically to get log and progress output transferred to the borg client

src/borg/repository.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def __init__(
106106
if isinstance(path_or_location, Location):
107107
location = path_or_location
108108
if location.proto == "file":
109-
url = _local_abspath_to_file_url(location.path) # frequently users give without file:// prefix
109+
url = _local_abspath_to_file_url(location.path)
110110
else:
111111
url = location.processed # location as given by user, processed placeholders
112112
else:

src/borg/testsuite/archiver/diff_cmd_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,8 +428,8 @@ def test_sort_by_all_keys_with_directions(archivers, request, sort_key):
428428

429429

430430
@pytest.mark.skipif(
431-
not are_hardlinks_supported() or is_freebsd or is_netbsd,
432-
reason="hardlinks not supported or test failing on freebsd and netbsd",
431+
not are_hardlinks_supported() or is_freebsd or is_netbsd or is_win32,
432+
reason="hardlinks not supported or test failing on freebsd, netbsd and windows",
433433
)
434434
def test_hard_link_deletion_and_replacement(archivers, request):
435435
archiver = request.getfixturevalue(archivers)

src/borg/testsuite/archiver/lock_cmds_test.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from ...constants import * # NOQA
99
from . import cmd, generate_archiver_tests, RK_ENCRYPTION
1010
from ...helpers import CommandError
11-
from ...platformflags import is_haiku
11+
from ...platformflags import is_haiku, is_win32
12+
from ...repository import _local_abspath_to_file_url
1213

1314
pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local,remote,binary") # NOQA
1415

@@ -19,11 +20,11 @@ def test_break_lock(archivers, request):
1920
cmd(archiver, "break-lock")
2021

2122

22-
@pytest.mark.skipif(is_haiku, reason="does not find borg python module on Haiku OS")
23+
@pytest.mark.skipif(is_haiku or is_win32, reason="does not find borg python module on Haiku OS and Windows")
2324
def test_with_lock(tmp_path):
2425
repo_path = tmp_path / "repo"
2526
env = os.environ.copy()
26-
env["BORG_REPO"] = "file://" + str(repo_path)
27+
env["BORG_REPO"] = _local_abspath_to_file_url(str(repo_path.absolute()))
2728
# test debug output:
2829
print("sys.path: %r" % sys.path)
2930
print("PYTHONPATH: %s" % env.get("PYTHONPATH", ""))

src/borg/testsuite/fslocking_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
NotLocked,
2121
NotMyLock,
2222
)
23+
from ..platformflags import is_win32
2324

2425
ID1 = "foo", 1, 1
2526
ID2 = "bar", 2, 2
@@ -105,6 +106,7 @@ def test_migrate_lock(self, lockpath):
105106
assert lock.by_me() # we still have the lock
106107
assert old_unique_name != new_unique_name # Locking filename is different now.
107108

109+
@pytest.mark.skipif(is_win32, reason="broken on windows")
108110
def test_race_condition(self, lockpath):
109111
class SynchronizedCounter:
110112
def __init__(self, count=0):

src/borg/testsuite/helpers/fs_test.py

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -375,19 +375,20 @@ def open_dir(path):
375375
assert dir_is_tagged(path=str(normal_dir), exclude_caches=False) == []
376376

377377
# Test 2: exclude_caches with file-descriptor-based operations
378-
with open_dir(str(cache_dir)) as fd:
379-
assert dir_is_tagged(dir_fd=fd, exclude_caches=True) == [CACHE_TAG_NAME]
380-
with open_dir(str(invalid_cache_dir)) as fd:
381-
assert dir_is_tagged(dir_fd=fd, exclude_caches=True) == []
382-
with open_dir(str(normal_dir)) as fd:
383-
assert dir_is_tagged(dir_fd=fd, exclude_caches=True) == []
384-
385-
with open_dir(str(cache_dir)) as fd:
386-
assert dir_is_tagged(dir_fd=fd, exclude_caches=False) == []
387-
with open_dir(str(invalid_cache_dir)) as fd:
388-
assert dir_is_tagged(dir_fd=fd, exclude_caches=False) == []
389-
with open_dir(str(normal_dir)) as fd:
390-
assert dir_is_tagged(dir_fd=fd, exclude_caches=False) == []
378+
if not is_win32:
379+
with open_dir(str(cache_dir)) as fd:
380+
assert dir_is_tagged(dir_fd=fd, exclude_caches=True) == [CACHE_TAG_NAME]
381+
with open_dir(str(invalid_cache_dir)) as fd:
382+
assert dir_is_tagged(dir_fd=fd, exclude_caches=True) == []
383+
with open_dir(str(normal_dir)) as fd:
384+
assert dir_is_tagged(dir_fd=fd, exclude_caches=True) == []
385+
386+
with open_dir(str(cache_dir)) as fd:
387+
assert dir_is_tagged(dir_fd=fd, exclude_caches=False) == []
388+
with open_dir(str(invalid_cache_dir)) as fd:
389+
assert dir_is_tagged(dir_fd=fd, exclude_caches=False) == []
390+
with open_dir(str(normal_dir)) as fd:
391+
assert dir_is_tagged(dir_fd=fd, exclude_caches=False) == []
391392

392393
# Test 3: exclude_if_present with path-based operations
393394
tags = [".NOBACKUP"]
@@ -401,21 +402,22 @@ def open_dir(path):
401402
assert dir_is_tagged(path=str(normal_dir), exclude_if_present=tags) == []
402403

403404
# Test 4: exclude_if_present with file descriptor-based operations
404-
tags = [".NOBACKUP"]
405-
with open_dir(str(tagged_dir)) as fd:
406-
assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == [".NOBACKUP"]
407-
with open_dir(str(other_tagged_dir)) as fd:
408-
assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == []
409-
with open_dir(str(normal_dir)) as fd:
410-
assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == []
411-
412-
tags = [".NOBACKUP", ".DONOTBACKUP"]
413-
with open_dir(str(tagged_dir)) as fd:
414-
assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == [".NOBACKUP"]
415-
with open_dir(str(other_tagged_dir)) as fd:
416-
assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == [".DONOTBACKUP"]
417-
with open_dir(str(normal_dir)) as fd:
418-
assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == []
405+
if not is_win32:
406+
tags = [".NOBACKUP"]
407+
with open_dir(str(tagged_dir)) as fd:
408+
assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == [".NOBACKUP"]
409+
with open_dir(str(other_tagged_dir)) as fd:
410+
assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == []
411+
with open_dir(str(normal_dir)) as fd:
412+
assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == []
413+
414+
tags = [".NOBACKUP", ".DONOTBACKUP"]
415+
with open_dir(str(tagged_dir)) as fd:
416+
assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == [".NOBACKUP"]
417+
with open_dir(str(other_tagged_dir)) as fd:
418+
assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == [".DONOTBACKUP"]
419+
with open_dir(str(normal_dir)) as fd:
420+
assert dir_is_tagged(dir_fd=fd, exclude_if_present=tags) == []
419421

420422
# Test 5: both exclude types with path-based operations
421423
assert sorted(dir_is_tagged(path=str(both_dir), exclude_caches=True, exclude_if_present=[".NOBACKUP"])) == [
@@ -427,14 +429,15 @@ def open_dir(path):
427429
assert dir_is_tagged(path=str(normal_dir), exclude_caches=True, exclude_if_present=[".NOBACKUP"]) == []
428430

429431
# Test 6: both exclude types with file descriptor-based operations
430-
with open_dir(str(both_dir)) as fd:
431-
assert sorted(dir_is_tagged(dir_fd=fd, exclude_caches=True, exclude_if_present=[".NOBACKUP"])) == [
432-
".NOBACKUP",
433-
CACHE_TAG_NAME,
434-
]
435-
with open_dir(str(cache_dir)) as fd:
436-
assert dir_is_tagged(dir_fd=fd, exclude_caches=True, exclude_if_present=[".NOBACKUP"]) == [CACHE_TAG_NAME]
437-
with open_dir(str(tagged_dir)) as fd:
438-
assert dir_is_tagged(dir_fd=fd, exclude_caches=True, exclude_if_present=[".NOBACKUP"]) == [".NOBACKUP"]
439-
with open_dir(str(normal_dir)) as fd:
440-
assert dir_is_tagged(dir_fd=fd, exclude_caches=True, exclude_if_present=[".NOBACKUP"]) == []
432+
if not is_win32:
433+
with open_dir(str(both_dir)) as fd:
434+
assert sorted(dir_is_tagged(dir_fd=fd, exclude_caches=True, exclude_if_present=[".NOBACKUP"])) == [
435+
".NOBACKUP",
436+
CACHE_TAG_NAME,
437+
]
438+
with open_dir(str(cache_dir)) as fd:
439+
assert dir_is_tagged(dir_fd=fd, exclude_caches=True, exclude_if_present=[".NOBACKUP"]) == [CACHE_TAG_NAME]
440+
with open_dir(str(tagged_dir)) as fd:
441+
assert dir_is_tagged(dir_fd=fd, exclude_caches=True, exclude_if_present=[".NOBACKUP"]) == [".NOBACKUP"]
442+
with open_dir(str(normal_dir)) as fd:
443+
assert dir_is_tagged(dir_fd=fd, exclude_caches=True, exclude_if_present=[".NOBACKUP"]) == []

src/borg/testsuite/helpers/parseformat_test.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
ChunkerParams,
2727
)
2828
from ...helpers.time import format_timedelta, parse_timestamp
29+
from ...platformflags import is_win32
2930

3031

3132
def test_bin_to_hex():
@@ -194,31 +195,31 @@ def test_sftp(self, monkeypatch, keys_dir):
194195

195196
def test_socket(self, monkeypatch, keys_dir):
196197
monkeypatch.delenv("BORG_REPO", raising=False)
198+
url = "socket:///c:/repo/path" if is_win32 else "socket:///repo/path"
199+
path = "c:/repo/path" if is_win32 else "/repo/path"
197200
assert (
198-
repr(Location("socket:///repo/path"))
199-
== "Location(proto='socket', user=None, pass=None, host=None, port=None, path='/repo/path')"
201+
repr(Location(url))
202+
== f"Location(proto='socket', user=None, pass=None, host=None, port=None, path='{path}')"
200203
)
201-
assert Location("socket:///some/path").to_key_filename() == keys_dir + "_some_path"
204+
assert Location(url).to_key_filename().endswith("_repo_path")
202205

203206
def test_file(self, monkeypatch, keys_dir):
204207
monkeypatch.delenv("BORG_REPO", raising=False)
208+
url = "file:///c:/repo/path" if is_win32 else "file:///repo/path"
209+
path = "c:/repo/path" if is_win32 else "/repo/path"
205210
assert (
206-
repr(Location("file:///some/path"))
207-
== "Location(proto='file', user=None, pass=None, host=None, port=None, path='/some/path')"
211+
repr(Location(url)) == f"Location(proto='file', user=None, pass=None, host=None, port=None, path='{path}')"
208212
)
209-
assert (
210-
repr(Location("file:///some/path"))
211-
== "Location(proto='file', user=None, pass=None, host=None, port=None, path='/some/path')"
212-
)
213-
assert Location("file:///some/path").to_key_filename() == keys_dir + "_some_path"
213+
assert Location(url).to_key_filename().endswith("_repo_path")
214214

215+
@pytest.mark.skipif(is_win32, reason="still broken")
215216
def test_smb(self, monkeypatch, keys_dir):
216217
monkeypatch.delenv("BORG_REPO", raising=False)
217218
assert (
218219
repr(Location("file:////server/share/path"))
219220
== "Location(proto='file', user=None, pass=None, host=None, port=None, path='//server/share/path')"
220221
)
221-
assert Location("file:////server/share/path").to_key_filename() == keys_dir + "__server_share_path"
222+
assert Location("file:////server/share/path").to_key_filename().endswith("__server_share_path")
222223

223224
def test_folder(self, monkeypatch, keys_dir):
224225
monkeypatch.delenv("BORG_REPO", raising=False)
@@ -230,6 +231,7 @@ def test_folder(self, monkeypatch, keys_dir):
230231
)
231232
assert Location("path").to_key_filename().endswith(rel_path)
232233

234+
@pytest.mark.skipif(is_win32, reason="Windows has drive letters in abs paths")
233235
def test_abspath(self, monkeypatch, keys_dir):
234236
monkeypatch.delenv("BORG_REPO", raising=False)
235237
assert (
@@ -259,6 +261,7 @@ def test_relpath(self, monkeypatch, keys_dir):
259261
)
260262
assert Location("ssh://user@host/relative/path").to_key_filename() == keys_dir + "host__relative_path"
261263

264+
@pytest.mark.skipif(is_win32, reason="Windows does not support colons in paths")
262265
def test_with_colons(self, monkeypatch, keys_dir):
263266
monkeypatch.delenv("BORG_REPO", raising=False)
264267
assert (
@@ -281,9 +284,6 @@ def test_canonical_path(self, monkeypatch):
281284
monkeypatch.delenv("BORG_REPO", raising=False)
282285
locations = [
283286
"relative/path",
284-
"/absolute/path",
285-
"file:///absolute/path",
286-
"socket:///absolute/path",
287287
"ssh://host/relative/path",
288288
"ssh://host//absolute/path",
289289
"ssh://user@host:1234/relative/path",
@@ -292,6 +292,9 @@ def test_canonical_path(self, monkeypatch):
292292
"sftp://user@host:1234/relative/path",
293293
"rclone:remote:path",
294294
]
295+
locations.insert(1, "c:/absolute/path" if is_win32 else "/absolute/path")
296+
locations.insert(2, "file:///c:/absolute/path" if is_win32 else "file:///absolute/path")
297+
locations.insert(3, "socket:///c:/absolute/path" if is_win32 else "socket:///absolute/path")
295298
for location in locations:
296299
assert (
297300
Location(location).canonical_path() == Location(Location(location).canonical_path()).canonical_path()

src/borg/testsuite/storelocking_test.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@
44

55
from borgstore.store import Store
66

7+
from ..repository import _local_abspath_to_file_url
78
from ..storelocking import Lock, NotLocked, LockTimeout
89

910
ID1 = "foo", 1, 1
1011
ID2 = "bar", 2, 2
1112

1213

1314
@pytest.fixture()
14-
def lockstore(tmpdir):
15-
store = Store("file://" + str(tmpdir / "lockstore"), levels={"locks/": [0]})
15+
def lockstore(tmp_path):
16+
lockstore_path = tmp_path / "lockstore"
17+
store = Store(_local_abspath_to_file_url(str(lockstore_path.absolute())), levels={"locks/": [0]})
1618
store.create()
1719
with store:
1820
yield store

0 commit comments

Comments
 (0)