Compare commits

...

11 commits

Author SHA1 Message Date
ff13a4e0d3
Merge 13986655fb into b2f4e2d523 2025-01-27 10:51:42 +01:00
Alexis Métaireau
b2f4e2d523
Bump poetry.lock
Some checks are pending
Tests / windows (push) Blocked by required conditions
Tests / macOS (arch64) (push) Blocked by required conditions
Tests / macOS (x86_64) (push) Blocked by required conditions
Tests / build-deb (debian bookworm) (push) Blocked by required conditions
Tests / build-deb (debian bullseye) (push) Blocked by required conditions
Tests / build-deb (debian trixie) (push) Blocked by required conditions
Tests / build-deb (ubuntu 20.04) (push) Blocked by required conditions
Tests / build-deb (ubuntu 22.04) (push) Blocked by required conditions
Tests / build-deb (ubuntu 24.04) (push) Blocked by required conditions
Tests / build-deb (ubuntu 24.10) (push) Blocked by required conditions
Tests / install-deb (debian bookworm) (push) Blocked by required conditions
Tests / install-deb (debian bullseye) (push) Blocked by required conditions
Tests / install-deb (debian trixie) (push) Blocked by required conditions
Tests / install-deb (ubuntu 20.04) (push) Blocked by required conditions
Tests / install-deb (ubuntu 22.04) (push) Blocked by required conditions
Tests / install-deb (ubuntu 24.04) (push) Blocked by required conditions
Tests / install-deb (ubuntu 24.10) (push) Blocked by required conditions
Tests / build-install-rpm (fedora 40) (push) Blocked by required conditions
Tests / build-install-rpm (fedora 41) (push) Blocked by required conditions
Tests / run tests (debian bookworm) (push) Blocked by required conditions
Tests / run tests (debian bullseye) (push) Blocked by required conditions
Tests / run tests (debian trixie) (push) Blocked by required conditions
Tests / run tests (fedora 40) (push) Blocked by required conditions
Tests / run tests (fedora 41) (push) Blocked by required conditions
Tests / run tests (ubuntu 20.04) (push) Blocked by required conditions
Tests / run tests (ubuntu 22.04) (push) Blocked by required conditions
Tests / run tests (ubuntu 24.04) (push) Blocked by required conditions
Tests / run tests (ubuntu 24.10) (push) Blocked by required conditions
Scan latest app and container / security-scan-container (push) Waiting to run
Scan latest app and container / security-scan-app (push) Waiting to run
2025-01-23 16:26:07 +01:00
Alexis Métaireau
7409966253
Remove ${Python3:Depends} as it's not used at the moment. 2025-01-23 16:26:06 +01:00
Alexis Métaireau
40fb6579f6
Alternatives for debian/control 2025-01-23 16:26:06 +01:00
Alexis Métaireau
6ae91b024e
Use platformdirs to find user configuration files
The previous library we were using for this (`appdirs`) is dead upstream
and not supported anymore in debian testing.

Fixes #1058
2025-01-23 16:26:06 +01:00
Alexis Métaireau
c2841dcc08
Run ruff format
Some checks failed
Tests / build-container-image (push) Has been cancelled
Tests / Download and cache Tesseract data (push) Has been cancelled
Tests / windows (push) Has been cancelled
Tests / macOS (arch64) (push) Has been cancelled
Tests / macOS (x86_64) (push) Has been cancelled
Tests / build-deb (debian bookworm) (push) Has been cancelled
Tests / build-deb (debian bullseye) (push) Has been cancelled
Tests / build-deb (debian trixie) (push) Has been cancelled
Tests / build-deb (ubuntu 20.04) (push) Has been cancelled
Tests / build-deb (ubuntu 22.04) (push) Has been cancelled
Tests / build-deb (ubuntu 24.04) (push) Has been cancelled
Tests / build-deb (ubuntu 24.10) (push) Has been cancelled
Tests / install-deb (debian bookworm) (push) Has been cancelled
Tests / install-deb (debian bullseye) (push) Has been cancelled
Tests / install-deb (debian trixie) (push) Has been cancelled
Tests / install-deb (ubuntu 20.04) (push) Has been cancelled
Tests / install-deb (ubuntu 22.04) (push) Has been cancelled
Tests / install-deb (ubuntu 24.04) (push) Has been cancelled
Tests / install-deb (ubuntu 24.10) (push) Has been cancelled
Tests / build-install-rpm (fedora 40) (push) Has been cancelled
Tests / build-install-rpm (fedora 41) (push) Has been cancelled
Tests / run tests (debian bookworm) (push) Has been cancelled
Tests / run tests (debian bullseye) (push) Has been cancelled
Tests / run tests (debian trixie) (push) Has been cancelled
Tests / run tests (fedora 40) (push) Has been cancelled
Tests / run tests (fedora 41) (push) Has been cancelled
Tests / run tests (ubuntu 20.04) (push) Has been cancelled
Tests / run tests (ubuntu 22.04) (push) Has been cancelled
Tests / run tests (ubuntu 24.04) (push) Has been cancelled
Tests / run tests (ubuntu 24.10) (push) Has been cancelled
2025-01-23 14:48:33 +01:00
Alexis Métaireau
df5ccb3f75
Fedora: bypass the shiboken specific version. 2025-01-23 14:39:50 +01:00
Alexis Métaireau
9c6c2e1051
build: pin shiboken6 to specific versions 2025-01-23 12:52:48 +01:00
Alexis Métaireau
23f3ad1f46
doc: bump the Docker Desktop version as part of the RELEASE procedure
Some checks failed
Tests / macOS (x86_64) (push) Has been cancelled
Scan latest app and container / security-scan-container (push) Has been cancelled
Scan latest app and container / security-scan-app (push) Has been cancelled
Tests / windows (push) Has been cancelled
Tests / macOS (arch64) (push) Has been cancelled
Tests / build-deb (debian bookworm) (push) Has been cancelled
Tests / build-deb (debian bullseye) (push) Has been cancelled
Tests / build-deb (debian trixie) (push) Has been cancelled
Tests / build-deb (ubuntu 20.04) (push) Has been cancelled
Tests / build-deb (ubuntu 22.04) (push) Has been cancelled
Tests / build-deb (ubuntu 24.04) (push) Has been cancelled
Tests / build-deb (ubuntu 24.10) (push) Has been cancelled
Tests / install-deb (debian bookworm) (push) Has been cancelled
Tests / install-deb (debian bullseye) (push) Has been cancelled
Tests / install-deb (debian trixie) (push) Has been cancelled
Tests / install-deb (ubuntu 20.04) (push) Has been cancelled
Tests / install-deb (ubuntu 22.04) (push) Has been cancelled
Tests / install-deb (ubuntu 24.04) (push) Has been cancelled
Tests / install-deb (ubuntu 24.10) (push) Has been cancelled
Tests / build-install-rpm (fedora 40) (push) Has been cancelled
Tests / build-install-rpm (fedora 41) (push) Has been cancelled
Tests / run tests (debian bookworm) (push) Has been cancelled
Tests / run tests (debian bullseye) (push) Has been cancelled
Tests / run tests (debian trixie) (push) Has been cancelled
Tests / run tests (fedora 40) (push) Has been cancelled
Tests / run tests (fedora 41) (push) Has been cancelled
Tests / run tests (ubuntu 20.04) (push) Has been cancelled
Tests / run tests (ubuntu 22.04) (push) Has been cancelled
Tests / run tests (ubuntu 24.04) (push) Has been cancelled
Tests / run tests (ubuntu 24.10) (push) Has been cancelled
2025-01-21 10:21:24 +01:00
Alexis Métaireau
970a82f432
Bind Alert instances to the main window alert property 2025-01-21 10:21:24 +01:00
Alexis Métaireau
3d5cacfffb
Warn users if the minimum version of Docker Desktop is not met
This only happens on Windows and macOS.

Fixes #693
2025-01-21 10:21:24 +01:00
13 changed files with 666 additions and 403 deletions

View file

@ -8,12 +8,13 @@ Here is a list of tasks that should be done before issuing the release:
- [ ] Create a new issue named **QA and Release for version \<VERSION\>**, to track the general progress.
You can generate its content with the the `poetry run ./dev_scripts/generate-release-tasks.py` command.
- [ ] [Add new Linux platforms and remove obsolete ones](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#add-new-platforms-and-remove-obsolete-ones)
- [ ] [Add new Linux platforms and remove obsolete ones](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#add-new-linux-platforms-and-remove-obsolete-ones)
- [ ] Bump the Python dependencies using `poetry lock`
- [ ] Update `version` in `pyproject.toml`
- [ ] Update `share/version.txt`
- [ ] Update the "Version" field in `install/linux/dangerzone.spec`
- [ ] Bump the Debian version by adding a new changelog entry in `debian/changelog`
- [ ] [Bump the minimum Docker Desktop versions](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#bump-the-minimum-docker-desktop-version) in `isolation_provider/container.py`
- [ ] Update screenshot in `README.md`, if necessary
- [ ] CHANGELOG.md should be updated to include a list of all major changes since the last release
- [ ] A draft release should be created. Copy the release notes text from the template at [`docs/templates/release-notes`](https://github.com/freedomofpress/dangerzone/tree/main/docs/templates/)
@ -46,6 +47,12 @@ In case of the removal of a version:
* Consult the previous paragraph, but also `grep` your way around.
2. Add a notice in our `CHANGELOG.md` about the version removal.
## Bump the minimum Docker Desktop version
We embed the minimum docker desktop versions inside Dangerzone, as an incentive for our macOS and Windows users to upgrade to the latests version.
You can find the latest version at the time of the release by looking at [their release notes](https://docs.docker.com/desktop/release-notes/)
## Large Document Testing
Parallel to the QA process, the release candidate should be put through the large document tests in a dedicated machine to run overnight.

View file

@ -302,7 +302,7 @@ def display_banner() -> None:
+ Back.BLACK
+ Fore.LIGHTWHITE_EX
+ Style.BRIGHT
+ f"{' '*left_spaces}Dangerzone v{get_version()}{' '*right_spaces}"
+ f"{' ' * left_spaces}Dangerzone v{get_version()}{' ' * right_spaces}"
+ Fore.YELLOW
+ Style.DIM
+ ""

View file

@ -124,6 +124,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.setWindowTitle("Dangerzone")
self.setWindowIcon(self.dangerzone.get_window_icon())
self.alert: Optional[Alert] = None
self.setMinimumWidth(600)
if platform.system() == "Darwin":
@ -226,6 +227,13 @@ class MainWindow(QtWidgets.QMainWindow):
# This allows us to make QSS rules conditional on the OS color mode.
self.setProperty("OSColorMode", self.dangerzone.app.os_color_mode.value)
if hasattr(self.dangerzone.isolation_provider, "check_docker_desktop_version"):
is_version_valid, version = (
self.dangerzone.isolation_provider.check_docker_desktop_version()
)
if not is_version_valid:
self.handle_docker_desktop_version_check(is_version_valid, version)
self.show()
def show_update_success(self) -> None:
@ -279,6 +287,46 @@ class MainWindow(QtWidgets.QMainWindow):
self.dangerzone.settings.set("updater_check", check)
self.dangerzone.settings.save()
def handle_docker_desktop_version_check(
self, is_version_valid: bool, version: str
) -> None:
hamburger_menu = self.hamburger_button.menu()
sep = hamburger_menu.insertSeparator(hamburger_menu.actions()[0])
upgrade_action = QAction("Docker Desktop should be upgraded", hamburger_menu)
upgrade_action.setIcon(
QtGui.QIcon(
load_svg_image(
"hamburger_menu_update_dot_error.svg", width=64, height=64
)
)
)
message = """
<p>A new version of Docker Desktop is available. Please upgrade your system.</p>
<p>Visit the <a href="https://www.docker.com/products/docker-desktop">Docker Desktop website</a> to download the latest version.</p>
<em>Keeping Docker Desktop up to date allows you to have more confidence that your documents are processed safely.</em>
"""
self.alert = Alert(
self.dangerzone,
title="Upgrade Docker Desktop",
message=message,
ok_text="Ok",
has_cancel=False,
)
def _launch_alert() -> None:
if self.alert:
self.alert.launch()
upgrade_action.triggered.connect(_launch_alert)
hamburger_menu.insertAction(sep, upgrade_action)
self.hamburger_button.setIcon(
QtGui.QIcon(
load_svg_image("hamburger_menu_update_error.svg", width=64, height=64)
)
)
def handle_updates(self, report: UpdateReport) -> None:
"""Handle update reports from the update checker thread.
@ -365,7 +413,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.content_widget.show()
def closeEvent(self, e: QtGui.QCloseEvent) -> None:
alert_widget = Alert(
self.alert = Alert(
self.dangerzone,
message="Some documents are still being converted.\n Are you sure you want to quit?",
ok_text="Abort conversions",
@ -379,7 +427,7 @@ class MainWindow(QtWidgets.QMainWindow):
else:
self.dangerzone.app.exit(0)
else:
accept_exit = alert_widget.launch()
accept_exit = self.alert.launch()
if not accept_exit:
e.ignore()
return
@ -623,7 +671,7 @@ class ContentWidget(QtWidgets.QWidget):
def documents_selected(self, docs: List[Document]) -> None:
if self.conversion_started:
Alert(
self.alert = Alert(
self.dangerzone,
message="Dangerzone does not support adding documents after the conversion has started.",
has_cancel=False,
@ -633,7 +681,7 @@ class ContentWidget(QtWidgets.QWidget):
# Ensure all files in batch are in the same directory
dirnames = {os.path.dirname(doc.input_filename) for doc in docs}
if len(dirnames) > 1:
Alert(
self.alert = Alert(
self.dangerzone,
message="Dangerzone does not support adding documents from multiple locations.\n\n The newly added documents were ignored.",
has_cancel=False,
@ -802,14 +850,14 @@ class DocSelectionDropFrame(QtWidgets.QFrame):
text = f"{num_unsupported_docs} files are not supported."
ok_text = "Continue without these files"
alert_widget = Alert(
self.alert = Alert(
self.dangerzone,
message=f"{text}\nThe supported extensions are: "
+ ", ".join(get_supported_extensions()),
ok_text=ok_text,
)
return alert_widget.exec_()
return self.alert.exec_()
class SettingsWidget(QtWidgets.QWidget):

View file

@ -335,9 +335,9 @@ class IsolationProvider(ABC):
stderr_thread = self.start_stderr_thread(p, stderr)
if platform.system() != "Windows":
assert os.getpgid(p.pid) != os.getpgid(
os.getpid()
), "Parent shares same PGID with child"
assert os.getpgid(p.pid) != os.getpgid(os.getpid()), (
"Parent shares same PGID with child"
)
try:
yield p

View file

@ -3,7 +3,7 @@ import os
import platform
import shlex
import subprocess
from typing import List
from typing import List, Tuple
from .. import container_utils, errors
from ..document import Document
@ -11,7 +11,10 @@ from ..util import get_resource_path, get_subprocess_startupinfo
from .base import IsolationProvider, terminate_process_group
TIMEOUT_KILL = 5 # Timeout in seconds until the kill command returns.
MINIMUM_DOCKER_DESKTOP = {
"Darwin": "4.36.0",
"Windows": "4.36.0",
}
# Define startupinfo for subprocesses
if platform.system() == "Windows":
@ -121,6 +124,7 @@ class Container(IsolationProvider):
def is_available() -> bool:
container_runtime = container_utils.get_runtime()
runtime_name = container_utils.get_runtime_name()
# Can we run `docker/podman image ls` without an error
with subprocess.Popen(
[container_runtime, "image", "ls"],
@ -135,6 +139,28 @@ class Container(IsolationProvider):
)
return True
def check_docker_desktop_version(self) -> Tuple[bool, str]:
# On windows and darwin, check that the minimum version is met
version = ""
if platform.system() != "Linux":
with subprocess.Popen(
["docker", "version", "--format", "{{.Server.Platform.Name}}"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
startupinfo=get_subprocess_startupinfo(),
) as p:
stdout, stderr = p.communicate()
if p.returncode != 0:
# When an error occurs, consider that the check went
# through, as we're checking for installation compatibiliy
# somewhere else already
return True, version
# The output is like "Docker Desktop 4.35.1 (173168)"
version = stdout.decode().replace("Docker Desktop", "").split()[0]
if version < MINIMUM_DOCKER_DESKTOP[platform.system()]:
return False, version
return True, version
def doc_to_pixels_container_name(self, document: Document) -> str:
"""Unique container name for the doc-to-pixels phase."""
return f"dangerzone-doc-to-pixels-{document.id}"

View file

@ -5,11 +5,14 @@ import sys
import traceback
import unicodedata
import appdirs
try:
import platformdirs
except ImportError:
import appdirs as platformdirs
def get_config_dir() -> str:
return appdirs.user_config_dir("dangerzone")
return platformdirs.user_config_dir("dangerzone")
def get_resource_path(filename: str) -> str:

2
debian/control vendored
View file

@ -9,7 +9,7 @@ Rules-Requires-Root: no
Package: dangerzone
Architecture: any
Depends: ${misc:Depends}, ${python3:Depends}, podman, python3, python3-pyside2.qtcore, python3-pyside2.qtgui, python3-pyside2.qtwidgets, python3-pyside2.qtsvg, python3-appdirs, python3-click, python3-xdg, python3-colorama, python3-requests, python3-markdown, python3-packaging, tesseract-ocr-all
Depends: ${misc:Depends}, podman, python3, python3-pyside2.qtcore, python3-pyside2.qtgui, python3-pyside2.qtwidgets, python3-pyside2.qtsvg, python3-platformdirs | python3-appdirs, python3-click, python3-xdg, python3-colorama, python3-requests, python3-markdown, python3-packaging, tesseract-ocr-all
Description: Take potentially dangerous PDFs, office documents, or images
Dangerzone is an open source desktop application that takes potentially dangerous PDFs, office documents, or images and converts them to safe PDFs. It uses disposable VMs on Qubes OS, or container technology in other OSes, to convert the documents within a secure sandbox.
.

View file

@ -226,6 +226,11 @@ convert the documents within a secure sandbox.
sed -i 's/<3.13/<3.14/' pyproject.toml
%endif
# Bypass the version pin for Fedora as the 6.8.1.1 package is causing trouble
# A 6.8.1.1 package was only released with a wheel for macOS, but was picked by
# Fedora packagers. We cannot use "*" when PyPI is involved as it will fail to download the latest version.
# For Fedora, we can pick any of the released versions.
sed -i '/shiboken6 = \[/,/\]/c\shiboken6 = "*"' pyproject.toml
%generate_buildrequires
%pyproject_buildrequires -R

792
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,7 @@ include = [
[tool.poetry.dependencies]
python = ">=3.9,<3.13"
click = "*"
appdirs = "*"
platformdirs = "*"
PySide6 = "^6.7.1"
PyMuPDF = "^1.23.3" # The version in Fedora 39
colorama = "*"
@ -23,6 +23,13 @@ pyxdg = {version = "*", platform = "linux"}
requests = "*"
markdown = "*"
packaging = "*"
# shiboken6 released a 6.8.1.1 version only for macOS
# and it's getting picked by poetry, so pin it instead.
shiboken6 = [
{version = "*", platform = "darwin"},
{version = "<6.8.1.1", platform = "linux"},
{version = "<6.8.1.1", platform = "win32"},
]
[tool.poetry.scripts]
dangerzone = 'dangerzone:main'

View file

@ -587,3 +587,57 @@ def test_installation_failure_return_false(qtbot: QtBot, mocker: MockerFixture)
assert "the following error occured" in widget.label.text()
assert "The image cannot be found" in widget.traceback.toPlainText()
def test_up_to_date_docker_desktop_does_nothing(
qtbot: QtBot, mocker: MockerFixture
) -> None:
# Setup install to return False
mock_app = mocker.MagicMock()
dummy = mocker.MagicMock(spec=Container)
dummy.check_docker_desktop_version.return_value = (True, "1.0.0")
dz = DangerzoneGui(mock_app, dummy)
window = MainWindow(dz)
qtbot.addWidget(window)
menu_actions = window.hamburger_button.menu().actions()
assert "Docker Desktop should be upgraded" not in [
a.toolTip() for a in menu_actions
]
def test_outdated_docker_desktop_displays_warning(
qtbot: QtBot, mocker: MockerFixture
) -> None:
# Setup install to return False
mock_app = mocker.MagicMock()
dummy = mocker.MagicMock(spec=Container)
dummy.check_docker_desktop_version.return_value = (False, "1.0.0")
dz = DangerzoneGui(mock_app, dummy)
load_svg_spy = mocker.spy(main_window_module, "load_svg_image")
window = MainWindow(dz)
qtbot.addWidget(window)
menu_actions = window.hamburger_button.menu().actions()
assert menu_actions[0].toolTip() == "Docker Desktop should be upgraded"
# Check that the hamburger icon has changed with the expected SVG image.
assert load_svg_spy.call_count == 4
assert (
load_svg_spy.call_args_list[2].args[0] == "hamburger_menu_update_dot_error.svg"
)
alert_spy = mocker.spy(window.alert, "launch")
# Clicking the menu item should open a warning message
def _check_alert_displayed() -> None:
alert_spy.assert_any_call()
if window.alert:
window.alert.close()
QtCore.QTimer.singleShot(0, _check_alert_displayed)
menu_actions[0].trigger()

View file

@ -1,4 +1,5 @@
import os
import platform
import pytest
from pytest_mock import MockerFixture
@ -108,6 +109,92 @@ class TestContainer(IsolationProviderTest):
with pytest.raises(errors.ImageNotPresentException):
provider.install()
@pytest.mark.skipif(
platform.system() not in ("Windows", "Darwin"),
reason="macOS and Windows specific",
)
def test_old_docker_desktop_version_is_detected(
self, mocker: MockerFixture, provider: Container, fp: FakeProcess
) -> None:
fp.register_subprocess(
[
"docker",
"version",
"--format",
"{{.Server.Platform.Name}}",
],
stdout="Docker Desktop 1.0.0 (173100)",
)
mocker.patch(
"dangerzone.isolation_provider.container.MINIMUM_DOCKER_DESKTOP",
{"Darwin": "1.0.1", "Windows": "1.0.1"},
)
assert (False, "1.0.0") == provider.check_docker_desktop_version()
@pytest.mark.skipif(
platform.system() not in ("Windows", "Darwin"),
reason="macOS and Windows specific",
)
def test_up_to_date_docker_desktop_version_is_detected(
self, mocker: MockerFixture, provider: Container, fp: FakeProcess
) -> None:
fp.register_subprocess(
[
"docker",
"version",
"--format",
"{{.Server.Platform.Name}}",
],
stdout="Docker Desktop 1.0.1 (173100)",
)
# Require version 1.0.1
mocker.patch(
"dangerzone.isolation_provider.container.MINIMUM_DOCKER_DESKTOP",
{"Darwin": "1.0.1", "Windows": "1.0.1"},
)
assert (True, "1.0.1") == provider.check_docker_desktop_version()
fp.register_subprocess(
[
"docker",
"version",
"--format",
"{{.Server.Platform.Name}}",
],
stdout="Docker Desktop 2.0.0 (173100)",
)
assert (True, "2.0.0") == provider.check_docker_desktop_version()
@pytest.mark.skipif(
platform.system() not in ("Windows", "Darwin"),
reason="macOS and Windows specific",
)
def test_docker_desktop_version_failure_returns_true(
self, mocker: MockerFixture, provider: Container, fp: FakeProcess
) -> None:
fp.register_subprocess(
[
"docker",
"version",
"--format",
"{{.Server.Platform.Name}}",
],
stderr="Oopsie",
returncode=1,
)
assert provider.check_docker_desktop_version() == (True, "")
@pytest.mark.skipif(
platform.system() != "Linux",
reason="Linux specific",
)
def test_linux_skips_desktop_version_check_returns_true(
self, mocker: MockerFixture, provider: Container
) -> None:
assert (True, "") == provider.check_docker_desktop_version()
class TestContainerTermination(IsolationProviderTermination):
pass

View file

@ -12,9 +12,9 @@ VERSION_FILE_NAME = "version.txt"
def test_get_resource_path() -> None:
share_dir = Path("share").resolve()
resource_path = Path(util.get_resource_path(VERSION_FILE_NAME)).parent
assert share_dir.samefile(
resource_path
), f"{share_dir} is not the same file as {resource_path}"
assert share_dir.samefile(resource_path), (
f"{share_dir} is not the same file as {resource_path}"
)
@pytest.mark.skipif(platform.system() != "Windows", reason="Windows-specific")