This commit is contained in:
Alexis Métaireau 2025-03-24 18:13:29 +01:00 committed by GitHub
commit fe538d4bd2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 150 additions and 134 deletions

View file

@ -1,10 +1,12 @@
import logging import logging
import os
import platform import platform
import shutil import shutil
import subprocess import subprocess
from typing import List, Tuple from typing import List, Tuple
from . import errors from . import errors
from .settings import Settings
from .util import get_resource_path, get_subprocess_startupinfo from .util import get_resource_path, get_subprocess_startupinfo
CONTAINER_NAME = "dangerzone.rocks/dangerzone" CONTAINER_NAME = "dangerzone.rocks/dangerzone"
@ -13,14 +15,27 @@ log = logging.getLogger(__name__)
def get_runtime_name() -> str: def get_runtime_name() -> str:
if platform.system() == "Linux": settings = Settings()
runtime_name = "podman" try:
else: runtime_name = settings.get("container_runtime")
# Windows, Darwin, and unknown use docker for now, dangerzone-vm eventually except KeyError:
runtime_name = "docker" return "podman" if platform.system() == "Linux" else "docker"
return runtime_name return runtime_name
def get_runtime() -> str:
container_tech = get_runtime_name()
runtime = shutil.which(container_tech)
if runtime is None:
# Fallback to the container runtime path from the settings
settings = Settings()
runtime_path = settings.get("container_runtime_path")
if os.path.exists(runtime_path):
return runtime_path
raise errors.NoContainerTechException(container_tech)
return runtime
def get_runtime_version() -> Tuple[int, int]: def get_runtime_version() -> Tuple[int, int]:
"""Get the major/minor parts of the Docker/Podman version. """Get the major/minor parts of the Docker/Podman version.
@ -31,13 +46,14 @@ def get_runtime_version() -> Tuple[int, int]:
semver parser is an overkill. semver parser is an overkill.
""" """
# Get the Docker/Podman version, using a Go template. # Get the Docker/Podman version, using a Go template.
runtime = get_runtime_name() runtime_name = get_runtime_name()
if runtime == "podman":
if runtime_name == "podman":
query = "{{.Client.Version}}" query = "{{.Client.Version}}"
else: else:
query = "{{.Server.Version}}" query = "{{.Server.Version}}"
cmd = [runtime, "version", "-f", query] cmd = [get_runtime(), "version", "-f", query]
try: try:
version = subprocess.run( version = subprocess.run(
cmd, cmd,
@ -46,7 +62,7 @@ def get_runtime_version() -> Tuple[int, int]:
check=True, check=True,
).stdout.decode() ).stdout.decode()
except Exception as e: except Exception as e:
msg = f"Could not get the version of the {runtime.capitalize()} tool: {e}" msg = f"Could not get the version of the {runtime_name.capitalize()} tool: {e}"
raise RuntimeError(msg) from e raise RuntimeError(msg) from e
# Parse this version and return the major/minor parts, since we don't need the # Parse this version and return the major/minor parts, since we don't need the
@ -56,20 +72,12 @@ def get_runtime_version() -> Tuple[int, int]:
return (int(major), int(minor)) return (int(major), int(minor))
except Exception as e: except Exception as e:
msg = ( msg = (
f"Could not parse the version of the {runtime.capitalize()} tool" f"Could not parse the version of the {runtime_name.capitalize()} tool"
f" (found: '{version}') due to the following error: {e}" f" (found: '{version}') due to the following error: {e}"
) )
raise RuntimeError(msg) raise RuntimeError(msg)
def get_runtime() -> str:
container_tech = get_runtime_name()
runtime = shutil.which(container_tech)
if runtime is None:
raise errors.NoContainerTechException(container_tech)
return runtime
def list_image_tags() -> List[str]: def list_image_tags() -> List[str]:
"""Get the tags of all loaded Dangerzone images. """Get the tags of all loaded Dangerzone images.
@ -121,7 +129,7 @@ def delete_image_tag(tag: str) -> None:
def get_expected_tag() -> str: def get_expected_tag() -> str:
"""Get the tag of the Dangerzone image tarball from the image-id.txt file.""" """Get the tag of the Dangerzone image tarball from the image-id.txt file."""
with open(get_resource_path("image-id.txt")) as f: with get_resource_path("image-id.txt").open() as f:
return f.read().strip() return f.read().strip()
@ -130,7 +138,7 @@ def load_image_tarball() -> None:
tarball_path = get_resource_path("container.tar") tarball_path = get_resource_path("container.tar")
try: try:
res = subprocess.run( res = subprocess.run(
[get_runtime(), "load", "-i", tarball_path], [get_runtime(), "load", "-i", str(tarball_path)],
startupinfo=get_subprocess_startupinfo(), startupinfo=get_subprocess_startupinfo(),
capture_output=True, capture_output=True,
check=True, check=True,

View file

@ -51,7 +51,7 @@ class Application(QtWidgets.QApplication):
def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
super(Application, self).__init__(*args, **kwargs) super(Application, self).__init__(*args, **kwargs)
self.setQuitOnLastWindowClosed(False) self.setQuitOnLastWindowClosed(False)
with open(get_resource_path("dangerzone.css"), "r") as f: with get_resource_path("dangerzone.css").open("r") as f:
style = f.read() style = f.read()
self.setStyleSheet(style) self.setStyleSheet(style)

View file

@ -63,7 +63,7 @@ class DangerzoneGui(DangerzoneCore):
path = get_resource_path("dangerzone.ico") path = get_resource_path("dangerzone.ico")
else: else:
path = get_resource_path("icon.png") path = get_resource_path("icon.png")
return QtGui.QIcon(path) return QtGui.QIcon(str(path))
def open_pdf_viewer(self, filename: str) -> None: def open_pdf_viewer(self, filename: str) -> None:
if platform.system() == "Darwin": if platform.system() == "Darwin":
@ -252,7 +252,7 @@ class Alert(Dialog):
def create_layout(self) -> QtWidgets.QBoxLayout: def create_layout(self) -> QtWidgets.QBoxLayout:
logo = QtWidgets.QLabel() logo = QtWidgets.QLabel()
logo.setPixmap( logo.setPixmap(
QtGui.QPixmap.fromImage(QtGui.QImage(get_resource_path("icon.png"))) QtGui.QPixmap.fromImage(QtGui.QImage(str(get_resource_path("icon.png"))))
) )
label = QtWidgets.QLabel() label = QtWidgets.QLabel()

View file

@ -62,7 +62,7 @@ def load_svg_image(filename: str, width: int, height: int) -> QtGui.QPixmap:
This answer is basically taken from: https://stackoverflow.com/a/25689790 This answer is basically taken from: https://stackoverflow.com/a/25689790
""" """
path = get_resource_path(filename) path = get_resource_path(filename)
svg_renderer = QtSvg.QSvgRenderer(path) svg_renderer = QtSvg.QSvgRenderer(str(path))
image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32) image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32)
# Set the ARGB to 0 to prevent rendering artifacts # Set the ARGB to 0 to prevent rendering artifacts
image.fill(0x00000000) image.fill(0x00000000)
@ -130,9 +130,8 @@ class MainWindow(QtWidgets.QMainWindow):
# Header # Header
logo = QtWidgets.QLabel() logo = QtWidgets.QLabel()
logo.setPixmap( icon_path = str(get_resource_path("icon.png"))
QtGui.QPixmap.fromImage(QtGui.QImage(get_resource_path("icon.png"))) logo.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(icon_path)))
)
header_label = QtWidgets.QLabel("Dangerzone") header_label = QtWidgets.QLabel("Dangerzone")
header_label.setFont(self.dangerzone.fixed_font) header_label.setFont(self.dangerzone.fixed_font)
header_label.setStyleSheet("QLabel { font-weight: bold; font-size: 50px; }") header_label.setStyleSheet("QLabel { font-weight: bold; font-size: 50px; }")
@ -575,8 +574,15 @@ class WaitingWidgetContainer(WaitingWidget):
self.finished.emit() self.finished.emit()
def state_change(self, state: str, error: Optional[str] = None) -> None: def state_change(self, state: str, error: Optional[str] = None) -> None:
custom_runtime = self.dangerzone.settings.custom_runtime_specified()
if state == "not_installed": if state == "not_installed":
if platform.system() == "Linux": if custom_runtime:
self.show_error(
"<strong>We could not find the container runtime defined in your settings</strong><br><br>"
"Please check your settings, install it if needed, and retry."
)
elif platform.system() == "Linux":
self.show_error( self.show_error(
"<strong>Dangerzone requires Podman</strong><br><br>" "<strong>Dangerzone requires Podman</strong><br><br>"
"Install it and retry." "Install it and retry."
@ -589,7 +595,12 @@ class WaitingWidgetContainer(WaitingWidget):
) )
elif state == "not_running": elif state == "not_running":
if platform.system() == "Linux": if custom_runtime:
self.show_error(
"<strong>We were unable to start the container runtime defined in your settings</strong><br><br>"
"Please check your settings, install it if needed, and retry."
)
elif platform.system() == "Linux":
# "not_running" here means that the `podman image ls` command failed. # "not_running" here means that the `podman image ls` command failed.
message = ( message = (
"<strong>Dangerzone requires Podman</strong><br><br>" "<strong>Dangerzone requires Podman</strong><br><br>"
@ -1222,7 +1233,7 @@ class DocumentsListWidget(QtWidgets.QListWidget):
if not self.thread_pool_initized: if not self.thread_pool_initized:
max_jobs = self.dangerzone.isolation_provider.get_max_parallel_conversions() max_jobs = self.dangerzone.isolation_provider.get_max_parallel_conversions()
# Call freeze_support() to avoid passing unknown options to the subprocess. # Call freeze_support() to avoid passing unknown options to the subprocess.
# See https://github.com/freedomofpress/dangerzone/issues/873 # See https://github.com/freedomofpress/Adangerzone/issues/873
freeze_support() freeze_support()
self.thread_pool = ThreadPool(max_jobs) self.thread_pool = ThreadPool(max_jobs)
@ -1306,7 +1317,7 @@ class DocumentWidget(QtWidgets.QWidget):
def load_status_image(self, filename: str) -> QtGui.QPixmap: def load_status_image(self, filename: str) -> QtGui.QPixmap:
path = get_resource_path(filename) path = get_resource_path(filename)
img = QtGui.QImage(path) img = QtGui.QImage(str(path))
image = QtGui.QPixmap.fromImage(img) image = QtGui.QPixmap.fromImage(img)
return image.scaled(QtCore.QSize(15, 15)) return image.scaled(QtCore.QSize(15, 15))

View file

@ -64,7 +64,7 @@ class Container(IsolationProvider):
# #
# [1] https://github.com/freedomofpress/dangerzone/issues/846 # [1] https://github.com/freedomofpress/dangerzone/issues/846
# [2] https://github.com/containers/common/blob/d3283f8401eeeb21f3c59a425b5461f069e199a7/pkg/seccomp/seccomp.json # [2] https://github.com/containers/common/blob/d3283f8401eeeb21f3c59a425b5461f069e199a7/pkg/seccomp/seccomp.json
seccomp_json_path = get_resource_path("seccomp.gvisor.json") seccomp_json_path = str(get_resource_path("seccomp.gvisor.json"))
security_args += ["--security-opt", f"seccomp={seccomp_json_path}"] security_args += ["--security-opt", f"seccomp={seccomp_json_path}"]
security_args += ["--cap-drop", "all"] security_args += ["--cap-drop", "all"]
@ -143,7 +143,10 @@ class Container(IsolationProvider):
def check_docker_desktop_version(self) -> Tuple[bool, str]: def check_docker_desktop_version(self) -> Tuple[bool, str]:
# On windows and darwin, check that the minimum version is met # On windows and darwin, check that the minimum version is met
version = "" version = ""
if platform.system() != "Linux": runtime_is_docker = container_utils.get_runtime_name() == "docker"
platform_is_not_linux = platform.system() != "Linux"
if runtime_is_docker and platform_is_not_linux:
with subprocess.Popen( with subprocess.Popen(
["docker", "version", "--format", "{{.Server.Platform.Name}}"], ["docker", "version", "--format", "{{.Server.Platform.Name}}"],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,

View file

@ -130,7 +130,6 @@ def is_qubes_native_conversion() -> bool:
# This disambiguates if it is running a Qubes targetted build or not # This disambiguates if it is running a Qubes targetted build or not
# (Qubes-specific builds don't ship the container image) # (Qubes-specific builds don't ship the container image)
container_image_path = get_resource_path("container.tar") return not get_resource_path("container.tar").exists()
return not os.path.exists(container_image_path)
else: else:
return False return False

View file

@ -23,16 +23,13 @@ class DangerzoneCore(object):
# Initialize terminal colors # Initialize terminal colors
colorama.init(autoreset=True) colorama.init(autoreset=True)
# App data folder
self.appdata_path = util.get_config_dir()
# Languages supported by tesseract # Languages supported by tesseract
with open(get_resource_path("ocr-languages.json"), "r") as f: with get_resource_path("ocr-languages.json").open("r") as f:
unsorted_ocr_languages = json.load(f) unsorted_ocr_languages = json.load(f)
self.ocr_languages = dict(sorted(unsorted_ocr_languages.items())) self.ocr_languages = dict(sorted(unsorted_ocr_languages.items()))
# Load settings # Load settings
self.settings = Settings(self) self.settings = Settings()
self.documents: List[Document] = [] self.documents: List[Document] = []
self.isolation_provider = isolation_provider self.isolation_provider = isolation_provider

View file

@ -6,24 +6,18 @@ from typing import TYPE_CHECKING, Any, Dict
from packaging import version from packaging import version
from .document import SAFE_EXTENSION from .document import SAFE_EXTENSION
from .util import get_version from .util import get_config_dir, get_version
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
if TYPE_CHECKING:
from .logic import DangerzoneCore
SETTINGS_FILENAME: str = "settings.json" SETTINGS_FILENAME: str = "settings.json"
class Settings: class Settings:
settings: Dict[str, Any] settings: Dict[str, Any]
def __init__(self, dangerzone: "DangerzoneCore") -> None: def __init__(self) -> None:
self.dangerzone = dangerzone self.settings_filename = get_config_dir() / SETTINGS_FILENAME
self.settings_filename = os.path.join(
self.dangerzone.appdata_path, SETTINGS_FILENAME
)
self.default_settings: Dict[str, Any] = self.generate_default_settings() self.default_settings: Dict[str, Any] = self.generate_default_settings()
self.load() self.load()
@ -45,6 +39,9 @@ class Settings:
"updater_errors": 0, "updater_errors": 0,
} }
def custom_runtime_specified(self) -> bool:
return "container_runtime" in self.settings
def get(self, key: str) -> Any: def get(self, key: str) -> Any:
return self.settings[key] return self.settings[key]
@ -91,6 +88,6 @@ class Settings:
self.save() self.save()
def save(self) -> None: def save(self) -> None:
os.makedirs(self.dangerzone.appdata_path, exist_ok=True) self.settings_filename.parent.mkdir(parents=True, exist_ok=True)
with open(self.settings_filename, "w") as settings_file: with self.settings_filename.open("w") as settings_file:
json.dump(self.settings, settings_file, indent=4) json.dump(self.settings, settings_file, indent=4)

View file

@ -1,9 +1,9 @@
import pathlib
import platform import platform
import subprocess import subprocess
import sys import sys
import traceback import traceback
import unicodedata import unicodedata
from pathlib import Path
try: try:
import platformdirs import platformdirs
@ -11,40 +11,39 @@ except ImportError:
import appdirs as platformdirs import appdirs as platformdirs
def get_config_dir() -> str: def get_config_dir() -> Path:
return platformdirs.user_config_dir("dangerzone") return Path(platformdirs.user_config_dir("dangerzone"))
def get_resource_path(filename: str) -> str: def get_resource_path(filename: str) -> Path:
if getattr(sys, "dangerzone_dev", False): if getattr(sys, "dangerzone_dev", False):
# Look for resources directory relative to python file # Look for resources directory relative to python file
project_root = pathlib.Path(__file__).parent.parent project_root = Path(__file__).parent.parent
prefix = project_root / "share" prefix = project_root / "share"
else: else:
if platform.system() == "Darwin": if platform.system() == "Darwin":
bin_path = pathlib.Path(sys.executable) bin_path = Path(sys.executable)
app_path = bin_path.parent.parent app_path = bin_path.parent.parent
prefix = app_path / "Resources" / "share" prefix = app_path / "Resources" / "share"
elif platform.system() == "Linux": elif platform.system() == "Linux":
prefix = pathlib.Path(sys.prefix) / "share" / "dangerzone" prefix = Path(sys.prefix) / "share" / "dangerzone"
elif platform.system() == "Windows": elif platform.system() == "Windows":
exe_path = pathlib.Path(sys.executable) exe_path = Path(sys.executable)
dz_install_path = exe_path.parent dz_install_path = exe_path.parent
prefix = dz_install_path / "share" prefix = dz_install_path / "share"
else: else:
raise NotImplementedError(f"Unsupported system {platform.system()}") raise NotImplementedError(f"Unsupported system {platform.system()}")
resource_path = prefix / filename return prefix / filename
return str(resource_path)
def get_tessdata_dir() -> pathlib.Path: def get_tessdata_dir() -> Path:
if getattr(sys, "dangerzone_dev", False) or platform.system() in ( if getattr(sys, "dangerzone_dev", False) or platform.system() in (
"Windows", "Windows",
"Darwin", "Darwin",
): ):
# Always use the tessdata path from the Dangerzone ./share directory, for # Always use the tessdata path from the Dangerzone ./share directory, for
# development builds, or in Windows/macOS platforms. # development builds, or in Windows/macOS platforms.
return pathlib.Path(get_resource_path("tessdata")) return get_resource_path("tessdata")
# In case of Linux systems, grab the Tesseract data from any of the following # In case of Linux systems, grab the Tesseract data from any of the following
# locations. We have found some of the locations through trial and error, whereas # locations. We have found some of the locations through trial and error, whereas
@ -55,11 +54,11 @@ def get_tessdata_dir() -> pathlib.Path:
# #
# [1] https://tesseract-ocr.github.io/tessdoc/Installation.html # [1] https://tesseract-ocr.github.io/tessdoc/Installation.html
tessdata_dirs = [ tessdata_dirs = [
pathlib.Path("/usr/share/tessdata/"), # on some Debian Path("/usr/share/tessdata/"), # on some Debian
pathlib.Path("/usr/share/tesseract/tessdata/"), # on Fedora Path("/usr/share/tesseract/tessdata/"), # on Fedora
pathlib.Path("/usr/share/tesseract-ocr/tessdata/"), # ? (documented) Path("/usr/share/tesseract-ocr/tessdata/"), # ? (documented)
pathlib.Path("/usr/share/tesseract-ocr/4.00/tessdata/"), # on Debian Bullseye Path("/usr/share/tesseract-ocr/4.00/tessdata/"), # on Debian Bullseye
pathlib.Path("/usr/share/tesseract-ocr/5/tessdata/"), # on Debian Trixie Path("/usr/share/tesseract-ocr/5/tessdata/"), # on Debian Trixie
] ]
for dir in tessdata_dirs: for dir in tessdata_dirs:
@ -71,7 +70,7 @@ def get_tessdata_dir() -> pathlib.Path:
def get_version() -> str: def get_version() -> str:
try: try:
with open(get_resource_path("version.txt")) as f: with get_resource_path("version.txt").open() as f:
version = f.read().strip() version = f.read().strip()
except FileNotFoundError: except FileNotFoundError:
# In dev mode, in Windows, get_resource_path doesn't work properly for the container, but luckily # In dev mode, in Windows, get_resource_path doesn't work properly for the container, but luckily

View file

@ -21,34 +21,25 @@ def get_qt_app() -> Application:
def generate_isolated_updater( def generate_isolated_updater(
tmp_path: Path, tmp_path: Path,
monkeypatch: MonkeyPatch, mocker: MockerFixture,
app_mocker: Optional[MockerFixture] = None, mock_app: bool = False,
) -> UpdaterThread: ) -> UpdaterThread:
"""Generate an Updater class with its own settings.""" """Generate an Updater class with its own settings."""
if app_mocker: app = mocker.MagicMock() if mock_app else get_qt_app()
app = app_mocker.MagicMock()
else:
app = get_qt_app()
dummy = Dummy() dummy = Dummy()
# XXX: We can monkey-patch global state without wrapping it in a context manager, or mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
# worrying that it will leak between tests, for two reasons:
#
# 1. Parallel tests in PyTest take place in different processes.
# 2. The monkeypatch fixture tears down the monkey-patch after each test ends.
monkeypatch.setattr(util, "get_config_dir", lambda: tmp_path)
dangerzone = DangerzoneGui(app, isolation_provider=dummy) dangerzone = DangerzoneGui(app, isolation_provider=dummy)
updater = UpdaterThread(dangerzone) updater = UpdaterThread(dangerzone)
return updater return updater
@pytest.fixture @pytest.fixture
def updater( def updater(tmp_path: Path, mocker: MockerFixture) -> UpdaterThread:
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture return generate_isolated_updater(tmp_path, mocker, mock_app=True)
) -> UpdaterThread:
return generate_isolated_updater(tmp_path, monkeypatch, mocker)
@pytest.fixture @pytest.fixture
def qt_updater(tmp_path: Path, monkeypatch: MonkeyPatch) -> UpdaterThread: def qt_updater(tmp_path: Path, mocker: MockerFixture) -> UpdaterThread:
return generate_isolated_updater(tmp_path, monkeypatch) return generate_isolated_updater(tmp_path, mocker, mock_app=False)

View file

@ -48,9 +48,7 @@ def test_default_updater_settings(updater: UpdaterThread) -> None:
) )
def test_pre_0_4_2_settings( def test_pre_0_4_2_settings(tmp_path: Path, mocker: MockerFixture) -> None:
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture
) -> None:
"""Check settings of installations prior to 0.4.2. """Check settings of installations prior to 0.4.2.
Check that installations that have been upgraded from a version < 0.4.2 to >= 0.4.2 Check that installations that have been upgraded from a version < 0.4.2 to >= 0.4.2
@ -58,7 +56,7 @@ def test_pre_0_4_2_settings(
in their settings.json file. in their settings.json file.
""" """
save_settings(tmp_path, default_settings_0_4_1()) save_settings(tmp_path, default_settings_0_4_1())
updater = generate_isolated_updater(tmp_path, monkeypatch, mocker) updater = generate_isolated_updater(tmp_path, mocker, mock_app=True)
assert ( assert (
updater.dangerzone.settings.get_updater_settings() == default_updater_settings() updater.dangerzone.settings.get_updater_settings() == default_updater_settings()
) )
@ -83,12 +81,10 @@ def test_post_0_4_2_settings(
# version is 0.4.3. # version is 0.4.3.
expected_settings = default_updater_settings() expected_settings = default_updater_settings()
expected_settings["updater_latest_version"] = "0.4.3" expected_settings["updater_latest_version"] = "0.4.3"
monkeypatch.setattr( monkeypatch.setattr(settings, "get_version", lambda: "0.4.3")
settings, "get_version", lambda: expected_settings["updater_latest_version"]
)
# Ensure that the Settings class will correct the latest version field to 0.4.3. # Ensure that the Settings class will correct the latest version field to 0.4.3.
updater = generate_isolated_updater(tmp_path, monkeypatch, mocker) updater = generate_isolated_updater(tmp_path, mocker, mock_app=True)
assert updater.dangerzone.settings.get_updater_settings() == expected_settings assert updater.dangerzone.settings.get_updater_settings() == expected_settings
# Simulate an updater check that found a newer Dangerzone version (e.g., 0.4.4). # Simulate an updater check that found a newer Dangerzone version (e.g., 0.4.4).
@ -118,9 +114,7 @@ def test_linux_no_check(updater: UpdaterThread, monkeypatch: MonkeyPatch) -> Non
assert updater.dangerzone.settings.get_updater_settings() == expected_settings assert updater.dangerzone.settings.get_updater_settings() == expected_settings
def test_user_prompts( def test_user_prompts(updater: UpdaterThread, mocker: MockerFixture) -> None:
updater: UpdaterThread, monkeypatch: MonkeyPatch, mocker: MockerFixture
) -> None:
"""Test prompting users to ask them if they want to enable update checks.""" """Test prompting users to ask them if they want to enable update checks."""
# First run # First run
# #
@ -370,8 +364,6 @@ def test_update_errors(
def test_update_check_prompt( def test_update_check_prompt(
qtbot: QtBot, qtbot: QtBot,
qt_updater: UpdaterThread, qt_updater: UpdaterThread,
monkeypatch: MonkeyPatch,
mocker: MockerFixture,
) -> None: ) -> None:
"""Test that the prompt to enable update checks works properly.""" """Test that the prompt to enable update checks works properly."""
# Force Dangerzone to check immediately for updates # Force Dangerzone to check immediately for updates

View file

@ -74,7 +74,7 @@ class TestContainer(IsolationProviderTest):
container_utils.get_runtime(), container_utils.get_runtime(),
"load", "load",
"-i", "-i",
get_resource_path("container.tar"), get_resource_path("container.tar").absolute(),
], ],
returncode=-1, returncode=-1,
) )
@ -87,7 +87,7 @@ class TestContainer(IsolationProviderTest):
) -> None: ) -> None:
"""When an image keep being not installed, it should return False""" """When an image keep being not installed, it should return False"""
fp.register_subprocess( fp.register_subprocess(
["podman", "version", "-f", "{{.Client.Version}}"], [container_utils.get_runtime(), "version", "-f", "{{.Client.Version}}"],
stdout="4.0.0", stdout="4.0.0",
) )
@ -113,7 +113,7 @@ class TestContainer(IsolationProviderTest):
container_utils.get_runtime(), container_utils.get_runtime(),
"load", "load",
"-i", "-i",
get_resource_path("container.tar"), get_resource_path("container.tar").absolute(),
], ],
) )
with pytest.raises(errors.ImageNotPresentException): with pytest.raises(errors.ImageNotPresentException):

View file

@ -0,0 +1,28 @@
from pathlib import Path
from pytest_mock import MockerFixture
from dangerzone.container_utils import get_runtime_name
from dangerzone.settings import Settings
def test_get_runtime_name_from_settings(mocker: MockerFixture, tmp_path: Path) -> None:
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
settings = Settings()
settings.set("container_runtime", "new-kid-on-the-block", autosave=True)
assert get_runtime_name() == "new-kid-on-the-block"
def test_get_runtime_name_linux(mocker: MockerFixture) -> None:
mocker.patch("platform.system", return_value="Linux")
assert get_runtime_name() == "podman"
def test_get_runtime_name_non_linux(mocker: MockerFixture) -> None:
mocker.patch("platform.system", return_value="Windows")
assert get_runtime_name() == "docker"
mocker.patch("platform.system", return_value="Something else")
assert get_runtime_name() == "docker"

View file

@ -1,5 +1,4 @@
import json import json
import os
from pathlib import Path from pathlib import Path
from unittest.mock import PropertyMock from unittest.mock import PropertyMock
@ -22,13 +21,6 @@ def default_settings_0_4_1() -> dict:
} }
@pytest.fixture
def settings(tmp_path: Path, mocker: MockerFixture) -> Settings:
dz_core = mocker.MagicMock()
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
return Settings(dz_core)
def save_settings(tmp_path: Path, settings: dict) -> None: def save_settings(tmp_path: Path, settings: dict) -> None:
"""Mimic the way Settings save a dictionary to a settings.json file.""" """Mimic the way Settings save a dictionary to a settings.json file."""
settings_filename = tmp_path / "settings.json" settings_filename = tmp_path / "settings.json"
@ -36,26 +28,31 @@ def save_settings(tmp_path: Path, settings: dict) -> None:
json.dump(settings, settings_file, indent=4) json.dump(settings, settings_file, indent=4)
def test_no_settings_file_creates_new_one(settings: Settings) -> None: def test_no_settings_file_creates_new_one(
tmp_path: Path,
mocker: MockerFixture,
) -> None:
"""Default settings file is created on first run""" """Default settings file is created on first run"""
assert os.path.isfile(settings.settings_filename) mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
new_settings_dict = json.load(open(settings.settings_filename)) settings = Settings()
assert sorted(new_settings_dict.items()) == sorted(
settings.generate_default_settings().items() assert settings.settings_filename.is_file()
) with settings.settings_filename.open() as settings_file:
new_settings_dict = json.load(settings_file)
assert sorted(new_settings_dict.items()) == sorted(
settings.generate_default_settings().items()
)
def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None: def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None:
# Set some broken settings file # Set some broken settings file
corrupt_settings_dict = "{:}" corrupt_settings_dict = "{:}"
with open(tmp_path / SETTINGS_FILENAME, "w") as settings_file: with (tmp_path / SETTINGS_FILENAME).open("w") as settings_file:
settings_file.write(corrupt_settings_dict) settings_file.write(corrupt_settings_dict)
# Initialize settings mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
dz_core = mocker.MagicMock() settings = Settings()
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path) assert settings.settings_filename.is_file()
settings = Settings(dz_core)
assert os.path.isfile(settings.settings_filename)
# Check if settings file was reset to the default # Check if settings file was reset to the default
new_settings_dict = json.load(open(settings.settings_filename)) new_settings_dict = json.load(open(settings.settings_filename))
@ -66,10 +63,7 @@ def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None:
def test_new_default_setting(tmp_path: Path, mocker: MockerFixture) -> None: def test_new_default_setting(tmp_path: Path, mocker: MockerFixture) -> None:
# Initialize settings settings = Settings()
dz_core = mocker.MagicMock()
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
settings = Settings(dz_core)
settings.save() settings.save()
# Ensure new default setting is imported into settings # Ensure new default setting is imported into settings
@ -78,15 +72,12 @@ def test_new_default_setting(tmp_path: Path, mocker: MockerFixture) -> None:
return_value={"mock_setting": 1}, return_value={"mock_setting": 1},
) )
settings2 = Settings(dz_core) settings2 = Settings()
assert settings2.get("mock_setting") == 1 assert settings2.get("mock_setting") == 1
def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None: def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None:
# Initialize settings settings = Settings()
dz_core = mocker.MagicMock()
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
settings = Settings(dz_core)
# Add new setting # Add new setting
settings.set("new_setting_autosaved", 20, autosave=True) settings.set("new_setting_autosaved", 20, autosave=True)
@ -95,7 +86,7 @@ def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None:
) # XXX has to be afterwards; otherwise this will be saved ) # XXX has to be afterwards; otherwise this will be saved
# Simulate new app startup (settings recreation) # Simulate new app startup (settings recreation)
settings2 = Settings(dz_core) settings2 = Settings()
# Check if new setting persisted # Check if new setting persisted
assert 20 == settings2.get("new_setting_autosaved") assert 20 == settings2.get("new_setting_autosaved")

View file

@ -11,7 +11,7 @@ VERSION_FILE_NAME = "version.txt"
def test_get_resource_path() -> None: def test_get_resource_path() -> None:
share_dir = Path("share").resolve() share_dir = Path("share").resolve()
resource_path = Path(util.get_resource_path(VERSION_FILE_NAME)).parent resource_path = util.get_resource_path(VERSION_FILE_NAME).parent
assert share_dir.samefile(resource_path), ( assert share_dir.samefile(resource_path), (
f"{share_dir} is not the same file as {resource_path}" f"{share_dir} is not the same file as {resource_path}"
) )