diff --git a/LogarithmPlotter/__init__.py b/LogarithmPlotter/__init__.py
index b3140ad..2bd4967 100644
--- a/LogarithmPlotter/__init__.py
+++ b/LogarithmPlotter/__init__.py
@@ -39,7 +39,3 @@ if not is_release and which('git') is not None:
# Date cannot be parsed, not git root?
pass
-if __name__ == "__main__":
- from .logarithmplotter import run
-
- run()
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
new file mode 100644
index 0000000..6fc2d65
--- /dev/null
+++ b/tests/python/test_native.py
@@ -0,0 +1,57 @@
+"""
+ * LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
+ * Copyright (C) 2021-2024 Ad5001
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+"""
+
+import pytest
+from os.path import exists
+
+from PySide6.QtCore import QEvent, QObject, QUrl
+from PySide6.QtGui import QActionEvent, QFileOpenEvent
+
+from LogarithmPlotter.util.native import MacOSFileOpenHandler
+
+
+class LoadDiagramCalledSuccessfully(Exception): pass
+
+
+class MockIO:
+ def loadDiagram(self, file_name):
+ assert type(file_name) == str
+ raise LoadDiagramCalledSuccessfully()
+
+
+class MockFileOpenEvent(QEvent):
+ def __init__(self, file):
+ QEvent.__init__(self, QEvent.FileOpen)
+ self._file = file
+
+ def file(self):
+ return self._file
+
+
+def test_native():
+ event_filter = MacOSFileOpenHandler()
+ # Nothing should happen here. The module hasn't been initialized
+ event_filter.eventFilter(None, QFileOpenEvent(QUrl.fromLocalFile("ci/test1.lpf")))
+ with pytest.raises(LoadDiagramCalledSuccessfully):
+ event_filter.init_io(MockIO()) # Now that we've initialized, the loadDiagram function should be called.
+ with pytest.raises(LoadDiagramCalledSuccessfully):
+ # And now it will do so every time an event is loaded.
+ event_filter.eventFilter(None, QFileOpenEvent(QUrl.fromLocalFile("ci/test1.lpf")))
+ # Check what happens when a non file open qevent is launched against it.
+ event_filter.eventFilter(QObject(), QEvent(QEvent.ActionAdded))
+
diff --git a/tests/python/test_update.py b/tests/python/test_update.py
new file mode 100644
index 0000000..9f65597
--- /dev/null
+++ b/tests/python/test_update.py
@@ -0,0 +1,67 @@
+"""
+ * LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
+ * Copyright (C) 2021-2024 Ad5001
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+"""
+from sys import argv
+
+import pytest
+from PySide6.QtCore import QThreadPool
+
+from LogarithmPlotter import __VERSION__ as version
+from LogarithmPlotter.util.update import UpdateInformation, UpdateCheckerRunnable, check_for_updates
+
+
+class MockWindow:
+ def showAlert(self, msg): raise Exception(msg)
+ def showUpdateMenu(self, msg): pass
+
+def check_update_callback_type(show_alert, msg_text, update_available):
+ assert type(show_alert) == bool
+ assert type(msg_text) == str
+ assert type(update_available) == bool
+
+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
+ assert show_alert
+
+ def check_newer(show_alert, msg_text, update_available):
+ check_update_callback_type(show_alert, msg_text, update_available)
+ assert not update_available
+ assert not show_alert
+
+ update_info_older = UpdateInformation()
+ update_info_older.got_update_info.connect(check_older)
+ update_info_newer = UpdateInformation()
+ 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(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")
+ update_info = check_for_updates('0.6.0', MockWindow())
+ assert QThreadPool.globalInstance().activeThreadCount() < 2 # No new update checks where added