diff --git a/LogarithmPlotter/logarithmplotter.py b/LogarithmPlotter/logarithmplotter.py index d528c02..bc4d895 100644 --- a/LogarithmPlotter/logarithmplotter.py +++ b/LogarithmPlotter/logarithmplotter.py @@ -36,7 +36,8 @@ tempdir = TemporaryDirectory() tmpfile = path.join(tempdir.name, 'graph.png') pwd = getcwd() -chdir(path.dirname(path.realpath(__file__))) +logarithmplotter_path = path.dirname(path.realpath(__file__)) +chdir(logarithmplotter_path) if path.realpath(path.join(getcwd(), "..")) not in sys_path: sys_path.append(path.realpath(path.join(getcwd(), ".."))) @@ -91,7 +92,7 @@ def get_platform_qt_style(os) -> str: def register_icon_directories() -> None: icon_fallbacks = QIcon.fallbackSearchPaths() - base_icon_path = path.join(getcwd(), "qml", "eu", "ad5001", "LogarithmPlotter", "icons") + base_icon_path = path.join(logarithmplotter_path, "qml", "eu", "ad5001", "LogarithmPlotter", "icons") paths = [["common"], ["objects"], ["history"], ["settings"], ["settings", "custom"]] for p in paths: icon_fallbacks.append(path.realpath(path.join(base_icon_path, *p))) @@ -106,7 +107,7 @@ def create_qapp() -> QApplication: app.setDesktopFileName("eu.ad5001.LogarithmPlotter") app.setOrganizationName("Ad5001") app.styleHints().setShowShortcutsInContextMenus(True) - app.setWindowIcon(QIcon(path.realpath(path.join(getcwd(), "logarithmplotter.svg")))) + app.setWindowIcon(QIcon(path.realpath(path.join(logarithmplotter_path, "logarithmplotter.svg")))) return app @@ -115,10 +116,12 @@ def install_translation(app: QApplication) -> QTranslator: translator = QTranslator() # Check if lang is forced. forcedlang = [p for p in argv if p[:7] == "--lang="] + i18n_path = path.realpath(path.join(logarithmplotter_path, "i18n")) locale = QLocale(forcedlang[0][7:]) if len(forcedlang) > 0 else QLocale() - if not translator.load(locale, "lp", "_", path.realpath(path.join(getcwd(), "i18n"))): + if not translator.load(locale, "lp", "_", i18n_path): # Load default translation - translator.load(QLocale("en"), "lp", "_", path.realpath(path.join(getcwd(), "i18n"))) + print("Loading default language en...") + translator.load(QLocale("en"), "lp", "_", i18n_path) app.installTranslator(translator) return translator @@ -133,8 +136,9 @@ def create_engine(helper: Helper, latex: Latex, dep_time: float) -> tuple[QQmlAp engine.rootContext().setContextProperty("TestBuild", "--test-build" in argv) engine.rootContext().setContextProperty("StartTime", dep_time) - engine.addImportPath(path.realpath(path.join(getcwd(), "qml"))) - engine.load(path.realpath(path.join(getcwd(), "qml", "eu", "ad5001", "LogarithmPlotter", "LogarithmPlotter.qml"))) + qml_path = path.realpath(path.join(logarithmplotter_path, "qml")) + engine.addImportPath(qml_path) + engine.load(path.join(qml_path, "eu", "ad5001", "LogarithmPlotter", "LogarithmPlotter.qml")) return engine, js_globals diff --git a/LogarithmPlotter/util/helper.py b/LogarithmPlotter/util/helper.py index c7c92d8..d2fe631 100644 --- a/LogarithmPlotter/util/helper.py +++ b/LogarithmPlotter/util/helper.py @@ -24,16 +24,29 @@ from PySide6 import __version__ as PySide6_version from os import chdir, path from json import loads -from sys import version as sys_version +from sys import version as sys_version, argv from urllib.request import urlopen from urllib.error import HTTPError, URLError from LogarithmPlotter import __VERSION__ from LogarithmPlotter.util import config +SHOW_GUI_MESSAGES = "--test-build" not in argv +CHANGELOG_VERSION = __VERSION__ + class InvalidFileException(Exception): pass +def show_message(msg: str) -> None: + """ + Shows a GUI message if GUI messages are enabled + """ + if SHOW_GUI_MESSAGES: + QMessageBox.warning(None, "LogarithmPlotter", msg, QMessageBox.OK) + else: + raise InvalidFileException(msg) + + class ChangelogFetcher(QRunnable): def __init__(self, helper): @@ -44,7 +57,7 @@ class ChangelogFetcher(QRunnable): msg_text = "Unknown changelog error." try: # Fetching version - r = urlopen("https://api.ad5001.eu/changelog/logarithmplotter/?version=" + __VERSION__) + r = urlopen("https://api.ad5001.eu/changelog/logarithmplotter/?version=" + CHANGELOG_VERSION) lines = r.readlines() r.close() msg_text = "".join(map(lambda x: x.decode('utf-8'), lines)).strip() @@ -53,21 +66,16 @@ class ChangelogFetcher(QRunnable): str(e.code)) except URLError as e: msg_text = QCoreApplication.translate("changelog", "Could not fetch update: {}.").format(str(e.reason)) - self.helper.gotChangelog.emit(msg_text) + self.helper.changelogFetched.emit(msg_text) class Helper(QObject): changelogFetched = Signal(str) - gotChangelog = Signal(str) def __init__(self, cwd: str, tmpfile: str): QObject.__init__(self) self.cwd = cwd self.tmpfile = tmpfile - self.gotChangelog.connect(self.fetched) - - def fetched(self, changelog: str): - self.changelogFetched.emit(changelog) @Slot(str, str) def write(self, filename, filedata): @@ -93,20 +101,19 @@ class Helper(QObject): if data[:5] == "LPFv1": # V1 version of the file data = data[5:] - elif data[0] == "{" and "type" in loads(data) and loads(data)["type"] == "logplotv1": - pass elif data[:3] == "LPF": # More recent version of LogarithmPlotter file, but incompatible with the current format - msg = QCoreApplication.translate("This file was created by a more recent version of LogarithmPlotter and cannot be backloaded in LogarithmPlotter v{}.\nPlease update LogarithmPlotter to open this file.") + msg = QCoreApplication.translate('main', + "This file was created by a more recent version of LogarithmPlotter and cannot be backloaded in LogarithmPlotter v{}.\nPlease update LogarithmPlotter to open this file.") raise InvalidFileException(msg.format(__VERSION__)) else: - raise Exception("Invalid LogarithmPlotter file.") - except Exception as e: # If file can't be loaded + raise InvalidFileException("Invalid LogarithmPlotter file.") + except InvalidFileException as e: # If file can't be loaded msg = QCoreApplication.translate('main', 'Could not open file "{}":\n{}') - QMessageBox.warning(None, 'LogarithmPlotter', msg.format(filename, e), QMessageBox.Ok) # Cannot parse file + show_message(msg.format(filename, e)) # Cannot parse file else: msg = QCoreApplication.translate('main', 'Could not open file: "{}"\nFile does not exist.') - QMessageBox.warning(None, 'LogarithmPlotter', msg.format(filename), QMessageBox.Ok) # Cannot parse file + show_message(msg.format(filename)) # Cannot parse file try: chdir(path.dirname(path.realpath(__file__))) except NotADirectoryError as e: @@ -130,31 +137,27 @@ class Helper(QObject): @Slot(str, result=str) def getSetting(self, namespace): - return config.getSetting(namespace) + return str(config.getSetting(namespace)) @Slot(str, result=float) def getSettingInt(self, namespace): - return config.getSetting(namespace) + return float(config.getSetting(namespace)) @Slot(str, result=bool) def getSettingBool(self, namespace): - return config.getSetting(namespace) + return bool(config.getSetting(namespace)) @Slot(str, str) def setSetting(self, namespace, value): - return config.setSetting(namespace, value) + return config.setSetting(namespace, str(value)) @Slot(str, bool) def setSettingBool(self, namespace, value): - return config.setSetting(namespace, value) + return config.setSetting(namespace, bool(value)) @Slot(str, float) def setSettingInt(self, namespace, value): - return config.setSetting(namespace, value) - - @Slot(str) - def setLanguage(self, new_lang): - config.setSetting("language", new_lang) + return config.setSetting(namespace, float(value)) @Slot(result=str) def getDebugInfos(self): @@ -167,7 +170,6 @@ class Helper(QObject): @Slot() def fetchChangelog(self): changelog_cache_path = path.join(path.dirname(path.realpath(__file__)), "CHANGELOG.md") - print(changelog_cache_path) if path.exists(changelog_cache_path): # We have a cached version of the changelog, for env that don't have access to the internet. f = open(changelog_cache_path); diff --git a/LogarithmPlotter/util/update.py b/LogarithmPlotter/util/update.py index ed2b5ba..32017d5 100644 --- a/LogarithmPlotter/util/update.py +++ b/LogarithmPlotter/util/update.py @@ -89,3 +89,4 @@ def check_for_updates(current_version, window): runnable = UpdateCheckerRunnable(current_version, update_info) QThreadPool.globalInstance().start(runnable) + return update_info diff --git a/tests/python/test_helper.py b/tests/python/test_helper.py index c4f6ea7..412eac3 100644 --- a/tests/python/test_helper.py +++ b/tests/python/test_helper.py @@ -17,18 +17,166 @@ """ import pytest -from os import getcwd +from os import getcwd, remove from os.path import join from tempfile import TemporaryDirectory -from LogarithmPlotter.util import config +from json import loads +from shutil import copy2 + +from PySide6.QtCore import QObject, Signal, QThreadPool +from PySide6.QtGui import QImage +from PySide6.QtWidgets import QApplication + +from LogarithmPlotter import __VERSION__ as version +from LogarithmPlotter.util import config, helper +from LogarithmPlotter.util.helper import ChangelogFetcher, Helper, InvalidFileException pwd = getcwd() - +helper.SHOW_GUI_MESSAGES = False @pytest.fixture() def temporary(): directory = TemporaryDirectory() config.CONFIG_PATH = join(directory.name, "config.json") tmpfile = join(directory.name, "graph.png") - yield tmpfile + yield tmpfile, directory directory.cleanup() + + +class MockHelperSignals(QObject): + changelogFetched = Signal(str) + + def __init__(self, expect_404): + QObject.__init__(self) + self.expect_404 = expect_404 + self.changelogFetched.connect(self.changelog_fetched) + self.changelog = None + + def changelog_fetched(self, changelog): + self.changelog = changelog + + +class TestChangelog: + + def test_exists(self, qtbot): + helper.CHANGELOG_VERSION = '0.5.0' + mock_helper = MockHelperSignals(False) + fetcher = ChangelogFetcher(mock_helper) + fetcher.run() # Does not raise an exception + qtbot.waitSignal(mock_helper.changelogFetched, timeout=10000) + assert type(mock_helper.changelog) == str + assert '404' not in mock_helper.changelog + + def tests_no_exist(self, qtbot): + mock_helper = MockHelperSignals(True) + helper.CHANGELOG_VERSION = '1.0.0' + fetcher = ChangelogFetcher(mock_helper) + fetcher.run() + qtbot.waitSignal(mock_helper.changelogFetched, timeout=10000) + assert type(mock_helper.changelog) == str + assert '404' in mock_helper.changelog + + +class TestHelper: + def test_read(self, temporary): + # Test file reading and information loading. + tmpfile, directory = temporary + obj = Helper(pwd, tmpfile) + data = obj.load("ci/test1.lpf") + assert type(data) == str + data = loads(data) + assert data['type'] == "logplotv1" + # Checking data['types'] of valid file. + # See https://git.ad5001.eu/Ad5001/LogarithmPlotter/wiki/LogarithmPlotter-file-format-v1.0 + assert type(data['width']) == int + assert type(data['height']) == int + assert type(data['xzoom']) in (int, float) + assert type(data['yzoom']) in (int, float) + assert type(data['xmin']) in (int, float) + assert type(data['ymax']) in (int, float) + assert type(data['xaxisstep']) == str + assert type(data['yaxisstep']) == str + assert type(data['xaxislabel']) == str + assert type(data['yaxislabel']) == str + assert type(data['logscalex']) == bool + assert type(data['linewidth']) in (int, float) + assert type(data['showxgrad']) == bool + assert type(data['showygrad']) == bool + assert type(data['textsize']) in (int, float) + assert type(data['history']) == list and len(data['history']) == 2 + assert type(data['history'][0]) == list + assert type(data['history'][1]) == list + for action_list in data['history']: + for action in action_list: + assert type(action[0]) == str + assert type(action[1]) == list + assert type(data['objects']) == dict + for obj_type, objects in data['objects'].items(): + assert type(obj_type) == str + assert type(objects) == list + for obj in objects: + assert type(obj) == list + + def test_read_newer(self, temporary): + tmpfile, directory = temporary + obj = Helper(pwd, tmpfile) + newer_file_path = join(directory.name, "newer.lpf") + with open(newer_file_path, "w") as f: + f.write("LPFv2[other invalid data]") + with pytest.raises(InvalidFileException): + obj.load(newer_file_path) + + def test_read_invalid_file(self, temporary): + tmpfile, directory = temporary + obj = Helper(pwd, tmpfile) + with pytest.raises(InvalidFileException): + obj.load("./inexistant.lpf") + with pytest.raises(InvalidFileException): + obj.load("./pyproject.toml") + + def test_write(self, temporary): + tmpfile, directory = temporary + obj = Helper(pwd, tmpfile) + target = join(directory.name, "target.lpf") + data = "example_data" + obj.write(target, data) + with open(target, "r") as f: + read_data = f.read() + # Ensure data has been written. + assert read_data == "LPFv1" + data + + def test_tmp_graphic(self, temporary): + tmpfile, directory = temporary + obj = Helper(pwd, tmpfile) + assert obj.gettmpfile() == tmpfile + obj.copyImageToClipboard() + clipboard = QApplication.clipboard() + assert type(clipboard.image()) == QImage + + def test_strings(self, temporary): + tmpfile, directory = temporary + obj = Helper(pwd, tmpfile) + assert obj.getVersion() == version + assert type(obj.getDebugInfos()) == str + assert type(obj.getSetting("check_for_updates")) == str + assert type(obj.getSettingInt("check_for_updates")) == float + assert type(obj.getSettingBool("check_for_updates")) == bool + + def test_set_config(self, temporary): + tmpfile, directory = temporary + obj = Helper(pwd, tmpfile) + obj.setSetting("last_install_greet", obj.getSetting("last_install_greet")) + obj.setSettingBool("check_for_updates", obj.getSettingBool("check_for_updates")) + obj.setSettingInt("default_graph.xzoom", obj.getSettingInt("default_graph.xzoom")) + + def test_fetch_changelog(self, temporary, qtbot): + tmpfile, directory = temporary + obj = Helper(pwd, tmpfile) + copy2("../../CHANGELOG.md", "../../LogarithmPlotter/util/CHANGELOG.md") + obj.fetchChangelog() + assert QThreadPool.globalInstance().activeThreadCount() == 0 + qtbot.waitSignal(obj.changelogFetched, timeout=10000) + remove("../../LogarithmPlotter/util/CHANGELOG.md") + obj.fetchChangelog() + assert QThreadPool.globalInstance().activeThreadCount() > 0 + qtbot.waitSignal(obj.changelogFetched, timeout=10000) \ No newline at end of file diff --git a/tests/python/test_native.py b/tests/python/test_native.py index edaf88e..6fc2d65 100644 --- a/tests/python/test_native.py +++ b/tests/python/test_native.py @@ -36,7 +36,7 @@ class MockIO: class MockFileOpenEvent(QEvent): def __init__(self, file): - super(QEvent.FileOpen) + QEvent.__init__(self, QEvent.FileOpen) self._file = file def file(self): diff --git a/tests/python/test_update.py b/tests/python/test_update.py index 385d12a..9f65597 100644 --- a/tests/python/test_update.py +++ b/tests/python/test_update.py @@ -33,7 +33,7 @@ def check_update_callback_type(show_alert, msg_text, update_available): assert type(msg_text) == str assert type(update_available) == bool -def test_update(): +def test_update(qtbot): def check_older(show_alert, msg_text, update_available): check_update_callback_type(show_alert, msg_text, update_available) assert update_available @@ -50,14 +50,18 @@ def test_update(): update_info_newer.got_update_info.connect(check_newer) runnable = UpdateCheckerRunnable('1.0.0', update_info_newer) runnable.run() + qtbot.waitSignal(update_info_newer.got_update_info, timeout=10000) runnable = UpdateCheckerRunnable('0.1.0', update_info_older) runnable.run() + qtbot.waitSignal(update_info_older.got_update_info, timeout=10000) runnable = UpdateCheckerRunnable('0.5.0+dev0+git20240101', update_info_older) runnable.run() + qtbot.waitSignal(update_info_older.got_update_info, timeout=10000) -def test_update_checker(): - check_for_updates('0.6.0', MockWindow()) +def test_update_checker(qtbot): + update_info = check_for_updates('0.6.0', MockWindow()) assert QThreadPool.globalInstance().activeThreadCount() == 1 + qtbot.waitSignal(update_info.got_update_info, timeout=10000) argv.append("--no-check-for-updates") - check_for_updates('0.6.0', MockWindow()) + update_info = check_for_updates('0.6.0', MockWindow()) assert QThreadPool.globalInstance().activeThreadCount() < 2 # No new update checks where added