From 7e4b70afb8f17a7b73eb2abac7b8103447ad43dd Mon Sep 17 00:00:00 2001 From: Erik Moeller Date: Fri, 4 Sep 2020 16:37:43 -0700 Subject: [PATCH] Add support for PyQt5 to updater, notifier Also adds PyQt5 to shared requirements file and renames it to dev-requirements.txt for consistency. --- .circleci/config.yml | 2 +- launcher/Makefile | 2 +- launcher/README.md | 24 ++++ ...st-requirements.in => dev-requirements.in} | 1 + ...-requirements.txt => dev-requirements.txt} | 27 ++++- launcher/sdw-launcher.py | 10 +- launcher/sdw-notify.py | 10 +- launcher/sdw_updater_gui/UpdaterApp.py | 16 ++- launcher/sdw_updater_gui/UpdaterAppUiQt5.py | 114 ++++++++++++++++++ launcher/sdw_util/Util.py | 55 +++++++++ launcher/tests/fixtures/bad-os-release-file | 4 + launcher/tests/fixtures/os-release-qubes-4.0 | 7 ++ launcher/tests/fixtures/os-release-qubes-4.1 | 7 ++ launcher/tests/fixtures/os-release-ubuntu | 12 ++ launcher/tests/test_util.py | 95 +++++++++++++++ 15 files changed, 374 insertions(+), 12 deletions(-) create mode 100644 launcher/README.md rename launcher/{test-requirements.in => dev-requirements.in} (76%) rename launcher/{test-requirements.txt => dev-requirements.txt} (87%) create mode 100644 launcher/sdw_updater_gui/UpdaterAppUiQt5.py create mode 100644 launcher/tests/fixtures/bad-os-release-file create mode 100644 launcher/tests/fixtures/os-release-qubes-4.0 create mode 100644 launcher/tests/fixtures/os-release-qubes-4.1 create mode 100644 launcher/tests/fixtures/os-release-ubuntu diff --git a/.circleci/config.yml b/.circleci/config.yml index 5286bd1b0..6948e6849 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -32,7 +32,7 @@ jobs: set -e virtualenv .venv source .venv/bin/activate - pip install --require-hashes -r test-requirements.txt + pip install --require-hashes -r dev-requirements.txt sudo apt install lsof make test && make bandit diff --git a/launcher/Makefile b/launcher/Makefile index a37fc5025..70b62591c 100644 --- a/launcher/Makefile +++ b/launcher/Makefile @@ -1,6 +1,6 @@ .PHONY: update-pip-requirements update-pip-requirements: ## Updates all Python requirements files via pip-compile. - pip-compile --allow-unsafe --generate-hashes --output-file=test-requirements.txt test-requirements.in + pip-compile --allow-unsafe --generate-hashes --output-file=dev-requirements.txt dev-requirements.in .PHONY: bandit bandit: diff --git a/launcher/README.md b/launcher/README.md new file mode 100644 index 000000000..3a4f3adbb --- /dev/null +++ b/launcher/README.md @@ -0,0 +1,24 @@ +The preflight updater GUI currently supports both PyQt4 and PyQt5. To +enforce the use of PyQt5, set the environment variable SDW_UPDATER_QT to 5. + +## Why support PyQt4 and PyQt5? + +Qubes 4.0.3 uses an end-of-life Fedora template in dom0 (fedora-25). See +rationale here: + +https://www.qubes-os.org/doc/supported-versions/#note-on-dom0-and-eol + +fedora-25 only includes PyQt4, which is why we have to support it for now. +The next version of Qubes, Qubes 4.1, will include PyQt5 in dom0. + +## Installing PyQt4 + +PyQt4 is no longer maintained, and is best installed through system +packages, e.g., https://packages.debian.org/buster/python3-pyqt4 + +## Installing PyQt5 + +The recommended version of PyQt5 is included in the developer requirements +for this project, which you can install via: + +pip install --require-hashes -r dev-requirements.txt diff --git a/launcher/test-requirements.in b/launcher/dev-requirements.in similarity index 76% rename from launcher/test-requirements.in rename to launcher/dev-requirements.in index 5b03df368..43d9bf745 100644 --- a/launcher/test-requirements.in +++ b/launcher/dev-requirements.in @@ -2,5 +2,6 @@ bandit black pip-tools pip +PyQt5==5.11.3 pytest pytest-cov diff --git a/launcher/test-requirements.txt b/launcher/dev-requirements.txt similarity index 87% rename from launcher/test-requirements.txt rename to launcher/dev-requirements.txt index 1b05f8530..6e00a8f91 100644 --- a/launcher/test-requirements.txt +++ b/launcher/dev-requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --allow-unsafe --generate-hashes --output-file=test-requirements.txt test-requirements.in +# pip-compile --allow-unsafe --generate-hashes --output-file=dev-requirements.txt dev-requirements.in # appdirs==1.4.3 \ --hash=sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92 \ @@ -188,6 +188,31 @@ zipp==0.6.0 \ --hash=sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335 \ # via importlib-metadata +pyqt5-sip==4.19.19 \ + --hash=sha256:304acf771b6033cb4bafc415939d227c91265d30664ed643b298d7e95f509f81 \ + --hash=sha256:39d2677f4de46ed4d7aa3b612f31c74c881975efe51c6a23fbb1d9382e4cc850 \ + --hash=sha256:54b99a3057e8f01b90d49cca9ca566b1ea23d8920038760f44e75b90c62b9d5f \ + --hash=sha256:59f5332f86f3ccd3ac94674fe91eae6e8aca26da7c6588917cabd0fe22af106d \ + --hash=sha256:72be07a21b0f379987c4ec59bc86834a9719a2f9cfb49606a4d4e34dae9aa549 \ + --hash=sha256:7b3b8c015e545fa30e42205fc1115b7c6afcb6acec790ce3f330a06323730523 \ + --hash=sha256:7fbb6389c20aff4c3257e89bb1787effffcaf05c32d937c00094ae45846bffd5 \ + --hash=sha256:828d9911acc483672a2bae1cc1bf79f591eb3338faad1f2c798aa2f45459a318 \ + --hash=sha256:a9460dac973deccc6ff2d90f18fd105cbaada147f84e5917ed79374dcb237758 \ + --hash=sha256:aade50f9a1b9d20f6aabe88e8999b10db57218f5c31950160f3f7957dd64e07c \ + --hash=sha256:ac9e5b282d1f0771a8310ed974afe1961ec31e9ae787d052c0e504ea46ae323a \ + --hash=sha256:ba41bd21b89c6713f7077b5f7d4a1c452989190aad5704e215560a266a1ecbab \ + --hash=sha256:c309dbbd6c155e961bfd6893496afa5cd184cce6f7dffd87ea68ee048b6f97e1 \ + --hash=sha256:cfc21b1f80d4655ffa776c505a2576b4d148bbc52bb3e33fedbf6cfbdbc09d1b \ + --hash=sha256:d7b26e0b6d81bf14c1239e6a891ac1303a7e882512d990ec330369c7269226d7 \ + --hash=sha256:f8b7a3e05235ce58a38bf317f71a5cb4ab45d3b34dc57421dd8cea48e0e4023e \ + # via pyqt5 +pyqt5==5.11.3 \ + --hash=sha256:517e4339135c4874b799af0d484bc2e8c27b54850113a68eec40a0b56534f450 \ + --hash=sha256:ac1eb5a114b6e7788e8be378be41c5e54b17d5158994504e85e43b5fca006a39 \ + --hash=sha256:d2309296a5a79d0a1c0e6c387c30f0398b65523a6dcc8a19cc172e46b949e00d \ + --hash=sha256:e85936bae1581bcb908847d2038e5b34237a5e6acc03130099a78930770e7ead \ + # via -r qt5-requirements.in + # The following packages are considered to be unsafe in a requirements file: pip==19.3.1 \ --hash=sha256:21207d76c1031e517668898a6b46a9fb1501c7a4710ef5dfd6a40ad9e6757ea7 \ diff --git a/launcher/sdw-launcher.py b/launcher/sdw-launcher.py index 2b93d5c69..88376ea7f 100644 --- a/launcher/sdw-launcher.py +++ b/launcher/sdw-launcher.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -from PyQt4 import QtGui from sdw_updater_gui.UpdaterApp import UpdaterApp from sdw_util import Util from sdw_updater_gui import Updater @@ -9,6 +8,13 @@ import sys import argparse +if Util.get_qt_version() == 5: + print("Using Qt5 (experimental)") + from PyQt5.QtWidgets import QApplication +else: + from PyQt4.QtGui import QApplication + + DEFAULT_INTERVAL = 28800 # 8hr default for update interval @@ -23,7 +29,7 @@ def launch_updater(): Start the updater GUI """ - app = QtGui.QApplication(sys.argv) + app = QApplication(sys.argv) form = UpdaterApp() form.show() sys.exit(app.exec_()) diff --git a/launcher/sdw-notify.py b/launcher/sdw-notify.py index a939ff2e8..2e0e77009 100755 --- a/launcher/sdw-notify.py +++ b/launcher/sdw-notify.py @@ -10,8 +10,12 @@ from sdw_notify import Notify from sdw_updater_gui import Updater from sdw_util import Util -from PyQt4 import QtGui -from PyQt4.QtGui import QMessageBox + +if Util.get_qt_version() == 5: + print("Using Qt5 (experimental)") + from PyQt5.QtWidgets import QApplication, QMessageBox +else: + from PyQt4.QtGui import QApplication, QMessageBox def main(): @@ -50,7 +54,7 @@ def show_update_warning(): Show a graphical warning reminding the user to check for security updates using the preflight updater. """ - app = QtGui.QApplication([]) # noqa: F841 + app = QApplication([]) # noqa: F841 QMessageBox.warning( None, diff --git a/launcher/sdw_updater_gui/UpdaterApp.py b/launcher/sdw_updater_gui/UpdaterApp.py index c1d63f07c..c0348e0c7 100644 --- a/launcher/sdw_updater_gui/UpdaterApp.py +++ b/launcher/sdw_updater_gui/UpdaterApp.py @@ -1,13 +1,21 @@ -from PyQt4 import QtGui -from PyQt4.QtCore import QThread, pyqtSignal, pyqtSlot -from sdw_updater_gui.UpdaterAppUi import Ui_UpdaterDialog from sdw_updater_gui import strings from sdw_updater_gui import Updater from sdw_updater_gui.Updater import UpdateStatus +from sdw_util import Util import logging import subprocess import sys +if Util.get_qt_version() == 5: + from PyQt5.QtWidgets import QDialog + from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot + from sdw_updater_gui.UpdaterAppUiQt5 import Ui_UpdaterDialog +else: + from PyQt4.QtGui import QDialog + from PyQt4.QtCore import QThread, pyqtSignal, pyqtSlot + from sdw_updater_gui.UpdaterAppUi import Ui_UpdaterDialog + + logger = logging.getLogger(__name__) @@ -24,7 +32,7 @@ def launch_securedrop_client(): sys.exit(0) -class UpdaterApp(QtGui.QDialog, Ui_UpdaterDialog): +class UpdaterApp(QDialog, Ui_UpdaterDialog): def __init__(self, parent=None): super(UpdaterApp, self).__init__(parent) diff --git a/launcher/sdw_updater_gui/UpdaterAppUiQt5.py b/launcher/sdw_updater_gui/UpdaterAppUiQt5.py new file mode 100644 index 000000000..120db81af --- /dev/null +++ b/launcher/sdw_updater_gui/UpdaterAppUiQt5.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'sdw_updater.ui' +# +# Created by: PyQt5 UI code generator 5.10.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_UpdaterDialog(object): + def setupUi(self, UpdaterDialog): + UpdaterDialog.setObjectName("UpdaterDialog") + UpdaterDialog.resize(520, 300) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred + ) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(UpdaterDialog.sizePolicy().hasHeightForWidth()) + UpdaterDialog.setSizePolicy(sizePolicy) + UpdaterDialog.setMaximumSize(QtCore.QSize(600, 420)) + self.gridLayout_2 = QtWidgets.QGridLayout(UpdaterDialog) + self.gridLayout_2.setObjectName("gridLayout_2") + self.gridLayout = QtWidgets.QGridLayout() + self.gridLayout.setContentsMargins(-1, 15, -1, 15) + self.gridLayout.setHorizontalSpacing(3) + self.gridLayout.setObjectName("gridLayout") + spacerItem = QtWidgets.QSpacerItem( + 20, 10, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed + ) + self.gridLayout.addItem(spacerItem, 1, 1, 1, 5) + self.clientOpenButton = QtWidgets.QPushButton(UpdaterDialog) + self.clientOpenButton.setStyleSheet("") + self.clientOpenButton.setAutoDefault(True) + self.clientOpenButton.setObjectName("clientOpenButton") + self.gridLayout.addWidget(self.clientOpenButton, 7, 4, 1, 1) + self.proposedActionDescription = QtWidgets.QLabel(UpdaterDialog) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum + ) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth( + self.proposedActionDescription.sizePolicy().hasHeightForWidth() + ) + self.proposedActionDescription.setSizePolicy(sizePolicy) + self.proposedActionDescription.setAlignment( + QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop + ) + self.proposedActionDescription.setWordWrap(True) + self.proposedActionDescription.setObjectName("proposedActionDescription") + self.gridLayout.addWidget(self.proposedActionDescription, 4, 1, 1, 5) + spacerItem1 = QtWidgets.QSpacerItem( + 40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum + ) + self.gridLayout.addItem(spacerItem1, 7, 1, 1, 1) + self.rebootButton = QtWidgets.QPushButton(UpdaterDialog) + self.rebootButton.setStyleSheet("") + self.rebootButton.setAutoDefault(True) + self.rebootButton.setObjectName("rebootButton") + self.gridLayout.addWidget(self.rebootButton, 7, 3, 1, 1) + self.applyUpdatesButton = QtWidgets.QPushButton(UpdaterDialog) + self.applyUpdatesButton.setStyleSheet("") + self.applyUpdatesButton.setAutoDefault(True) + self.applyUpdatesButton.setDefault(False) + self.applyUpdatesButton.setObjectName("applyUpdatesButton") + self.gridLayout.addWidget(self.applyUpdatesButton, 7, 2, 1, 1) + self.cancelButton = QtWidgets.QPushButton(UpdaterDialog) + self.cancelButton.setStyleSheet("") + self.cancelButton.setAutoDefault(True) + self.cancelButton.setObjectName("cancelButton") + self.gridLayout.addWidget(self.cancelButton, 7, 5, 1, 1) + self.progressBar = QtWidgets.QProgressBar(UpdaterDialog) + self.progressBar.setProperty("value", 0) + self.progressBar.setObjectName("progressBar") + self.gridLayout.addWidget(self.progressBar, 2, 1, 1, 5) + self.headline = QtWidgets.QLabel(UpdaterDialog) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed + ) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.headline.sizePolicy().hasHeightForWidth()) + self.headline.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setPointSize(18) + font.setBold(True) + font.setItalic(False) + font.setWeight(75) + self.headline.setFont(font) + self.headline.setObjectName("headline") + self.gridLayout.addWidget(self.headline, 0, 1, 1, 5) + spacerItem2 = QtWidgets.QSpacerItem( + 20, 10, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed + ) + self.gridLayout.addItem(spacerItem2, 3, 1, 1, 5) + self.gridLayout_2.addLayout(self.gridLayout, 0, 0, 1, 1) + + self.retranslateUi(UpdaterDialog) + QtCore.QMetaObject.connectSlotsByName(UpdaterDialog) + + def retranslateUi(self, UpdaterDialog): + _translate = QtCore.QCoreApplication.translate + UpdaterDialog.setWindowTitle( + _translate("UpdaterDialog", "SecureDrop Workstation preflight updater") + ) + self.clientOpenButton.setText(_translate("UpdaterDialog", "Continue")) + self.proposedActionDescription.setText(_translate("UpdaterDialog", "Description goes here")) + self.rebootButton.setText(_translate("UpdaterDialog", "Reboot")) + self.applyUpdatesButton.setText(_translate("UpdaterDialog", "Start Updates")) + self.cancelButton.setText(_translate("UpdaterDialog", "Cancel")) + self.headline.setText(_translate("UpdaterDialog", "Headline goes here")) diff --git a/launcher/sdw_util/Util.py b/launcher/sdw_util/Util.py index 07c857b99..fe05a8266 100644 --- a/launcher/sdw_util/Util.py +++ b/launcher/sdw_util/Util.py @@ -18,6 +18,9 @@ # Folder where logs are stored LOG_DIRECTORY = os.path.join(BASE_DIRECTORY, "logs") +# File that contains Qubes version information (overridden by tests) +OS_RELEASE_FILE = "/etc/os-release" + # Shared error string LOCK_ERROR = "Error obtaining lock on '{}'. Process may already be running." @@ -108,3 +111,55 @@ def is_conflicting_process_running(list): sdlog.error("Conflicting process '{}' is currently running.".format(name)) return True return False + + +def get_qubes_version(): + """ + Helper function for checking the Qubes version. Returns None if not on Qubes. + """ + is_qubes = False + version = None + try: + with open(OS_RELEASE_FILE) as f: + for line in f: + try: + key, value = line.rstrip().split("=") + except ValueError: + continue + + if key == "NAME" and "qubes" in value.lower(): + is_qubes = True + if key == "VERSION": + version = value + except FileNotFoundError: + return None + + if not is_qubes: + return None + + return version + + +def get_qt_version(): + """ + Determine the version of Qt appropriate for the environment we're in. + """ + qubes_version = get_qubes_version() + + # For now we must support both Qt4 and Qt5. We default to Qt4, because + # that's used in Qubes 4.0, the current stable version. + if qubes_version is not None and "4.1" in qubes_version: + default_version = 5 + else: + default_version = 4 + + version_str = os.getenv("SDW_UPDATER_QT", default_version) + try: + version = int(version_str) + except ValueError: + version = None + + if version in [4, 5]: + return version + else: + raise ValueError("Qt version not supported: {}".format(version_str)) diff --git a/launcher/tests/fixtures/bad-os-release-file b/launcher/tests/fixtures/bad-os-release-file new file mode 100644 index 000000000..eca68dfc5 --- /dev/null +++ b/launcher/tests/fixtures/bad-os-release-file @@ -0,0 +1,4 @@ +# No line +VERSION= +[we're doing toml now] +RELEASES = [ ["gamma", "delta"], [1, 2] ] diff --git a/launcher/tests/fixtures/os-release-qubes-4.0 b/launcher/tests/fixtures/os-release-qubes-4.0 new file mode 100644 index 000000000..567f34649 --- /dev/null +++ b/launcher/tests/fixtures/os-release-qubes-4.0 @@ -0,0 +1,7 @@ +NAME=Qubes +VERSION="4.0 (R4.0)" +ID=qubes +VERSION_ID=4.0 +PRETTY_NAME="Qubes 4.0 (R4.0)" +ANSI_COLOR="0;31" +CPE_NAME="cpe:/o:ITL:qubes:4.0" diff --git a/launcher/tests/fixtures/os-release-qubes-4.1 b/launcher/tests/fixtures/os-release-qubes-4.1 new file mode 100644 index 000000000..0c934b196 --- /dev/null +++ b/launcher/tests/fixtures/os-release-qubes-4.1 @@ -0,0 +1,7 @@ +NAME=Qubes +VERSION="4.1 (R4.1)" +ID=qubes +VERSION_ID=4.0 +PRETTY_NAME="Qubes 4.1 (R4.1)" +ANSI_COLOR="0;31" +CPE_NAME="cpe:/o:ITL:qubes:4.1" diff --git a/launcher/tests/fixtures/os-release-ubuntu b/launcher/tests/fixtures/os-release-ubuntu new file mode 100644 index 000000000..f1839e72b --- /dev/null +++ b/launcher/tests/fixtures/os-release-ubuntu @@ -0,0 +1,12 @@ +NAME="Ubuntu" +VERSION="18.04.5 LTS (Bionic Beaver)" +ID=ubuntu +ID_LIKE=debian +PRETTY_NAME="Ubuntu 18.04.5 LTS" +VERSION_ID="18.04" +HOME_URL="https://www.ubuntu.com/" +SUPPORT_URL="https://help.ubuntu.com/" +BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" +PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" +VERSION_CODENAME=bionic +UBUNTU_CODENAME=bionic diff --git a/launcher/tests/test_util.py b/launcher/tests/test_util.py index 722486b5f..c9660bb0e 100644 --- a/launcher/tests/test_util.py +++ b/launcher/tests/test_util.py @@ -15,6 +15,9 @@ CONFLICTING_PROCESS_REGEX = r"Conflicting process .* is currently running." +# Fixtures (sample files) for certain tests +FIXTURES_PATH = os.path.join(os.path.dirname(__file__), "fixtures") + relpath_util = "../sdw_util/Util.py" path_to_util = os.path.join(os.path.dirname(os.path.abspath(__file__)), relpath_util) util = SourceFileLoader("Util", path_to_util).load_module() @@ -193,3 +196,95 @@ def test_for_conflicting_process( else: assert running_process is False assert not mocked_error.called + + +@pytest.mark.parametrize( + "os_release_fixture,version_contains", + [ + ("os-release-qubes-4.0", "4.0"), + ("os-release-qubes-4.1", "4.1"), + ("os-release-ubuntu", None), + ("no-such-file", None), + ], +) +@mock.patch("Util.sdlog.error") +@mock.patch("Util.sdlog.warning") +@mock.patch("Util.sdlog.info") +@mock.patch("Util.OS_RELEASE_FILE", os.path.join(FIXTURES_PATH, "os-release-qubes-4.0")) +def test_detect_qubes( + mocked_info, mocked_warning, mocked_error, os_release_fixture, version_contains +): + """ + Test whether we can successfully detect whether we're on Qubes and, if so, + what version of Qubes, by parsing /etc/os-release in the expected format. + """ + with mock.patch("Util.OS_RELEASE_FILE", os.path.join(FIXTURES_PATH, os_release_fixture)): + qubes_version = util.get_qubes_version() + if version_contains is not None: + assert qubes_version is not None + assert version_contains in qubes_version + else: + assert qubes_version is None + + +@pytest.mark.parametrize( + "env_override,expected_qt_override_result", [(None, None), ("4", 4), ("5", 5)] +) +@pytest.mark.parametrize( + "os_release_fixture,expected_qt_version", + [ + ("os-release-qubes-4.0", 4), + ("os-release-qubes-4.1", 5), + ("os-release-ubuntu", 4), + ("no-such-file", 4), + ("bad-os-release-file", 4), + ], +) +@mock.patch("Util.sdlog.error") +@mock.patch("Util.sdlog.warning") +@mock.patch("Util.sdlog.info") +@mock.patch("Util.OS_RELEASE_FILE", os.path.join(FIXTURES_PATH, "os-release-qubes-4.0")) +def test_pick_qt( + mocked_info, + mocked_warning, + mocked_error, + os_release_fixture, + expected_qt_version, + env_override, + expected_qt_override_result, +): + """ + Test whether we're using the expected Qt version based on the operating system + and the environment variable, which should take precedence if defined. + """ + if env_override is None: + mocked_env = {} + else: + mocked_env = {"SDW_UPDATER_QT": env_override} + + with mock.patch( + "Util.OS_RELEASE_FILE", os.path.join(FIXTURES_PATH, os_release_fixture) + ), mock.patch.dict("os.environ", mocked_env): + qt_version = util.get_qt_version() + if expected_qt_override_result is not None: + assert qt_version == expected_qt_override_result + else: + assert qt_version == expected_qt_version + + +@pytest.mark.parametrize("env_override", ["3", "3000", "GTK"]) +@mock.patch("Util.sdlog.error") +@mock.patch("Util.sdlog.warning") +@mock.patch("Util.sdlog.info") +def test_pick_bad_qt( + mocked_info, mocked_warning, mocked_error, env_override, +): + """ + Test whether we're getting the expected error when specifying an invalid + version via environment override + """ + mocked_env = {"SDW_UPDATER_QT": env_override} + with mock.patch.dict("os.environ", mocked_env), mock.patch( + "Util.OS_RELEASE_FILE", os.path.join(FIXTURES_PATH, "os-release-qubes-4.0") + ), pytest.raises(ValueError): + util.get_qt_version()