From 4ac7de48b05527a07d0ab1e25d2f1662120f0785 Mon Sep 17 00:00:00 2001
From: Ad5001 <mail@ad5001.eu>
Date: Wed, 18 Sep 2024 22:51:23 +0200
Subject: [PATCH 1/2] Latex tests

---
 LogarithmPlotter/util/latex.py | 76 +++++++++++++++++++---------------
 ci/drone.yml                   |  1 +
 poetry.lock                    | 21 +++++++++-
 pyproject.toml                 |  5 +--
 scripts/run-tests.sh           |  7 ++++
 tests/python/test_latex.py     | 68 ++++++++++++++++++++++++++++++
 6 files changed, 139 insertions(+), 39 deletions(-)
 create mode 100644 scripts/run-tests.sh
 create mode 100644 tests/python/test_latex.py

diff --git a/LogarithmPlotter/util/latex.py b/LogarithmPlotter/util/latex.py
index 6b31359..2c650ce 100644
--- a/LogarithmPlotter/util/latex.py
+++ b/LogarithmPlotter/util/latex.py
@@ -18,13 +18,12 @@
 
 from PySide6.QtCore import QObject, Slot, Property, QCoreApplication
 from PySide6.QtGui import QImage, QColor
-from PySide6.QtWidgets import QApplication, QMessageBox
+from PySide6.QtWidgets import QMessageBox
 
 from os import path, remove
 from string import Template
 from tempfile import TemporaryDirectory
 from subprocess import Popen, TimeoutExpired, PIPE
-from platform import system
 from shutil import which
 from sys import argv
 
@@ -36,6 +35,7 @@ If not found, it will send an alert to the user.
 LATEX_PATH = which('latex')
 DVIPNG_PATH = which('dvipng')
 PACKAGES = ["calligra", "amsfonts", "inputenc"]
+SHOW_GUI_MESSAGES = "--test-build" not in argv
 
 DEFAULT_LATEX_DOC = Template(r"""
 \documentclass[]{minimal}
@@ -54,6 +54,20 @@ $$$$ $markup $$$$
 """)
 
 
+def show_message(msg: str) -> None:
+    """
+    Shows a GUI message if GUI messages are enabled
+    """
+    if SHOW_GUI_MESSAGES:
+        QMessageBox.warning(None, "LogarithmPlotter - Latex", msg)
+
+
+class MissingPackageException(Exception): pass
+
+
+class RenderError(Exception): pass
+
+
 class Latex(QObject):
     """
     Base class to convert Latex equations into PNG images with custom font color and size.
@@ -77,22 +91,20 @@ class Latex(QObject):
         valid_install = True
         if LATEX_PATH is None:
             print("No Latex installation found.")
-            if "--test-build" not in argv:
-                msg = QCoreApplication.translate("latex",
-                                                 "No Latex installation found.\nIf you already have a latex distribution installed, make sure it's installed on your path.\nOtherwise, you can download a Latex distribution like TeX Live at https://tug.org/texlive/.")
-                QMessageBox.warning(None, "LogarithmPlotter - Latex setup", msg)
+            msg = QCoreApplication.translate("latex",
+                                             "No Latex installation found.\nIf you already have a latex distribution installed, make sure it's installed on your path.\nOtherwise, you can download a Latex distribution like TeX Live at https://tug.org/texlive/.")
+            show_message(msg)
             valid_install = False
         elif DVIPNG_PATH is None:
             print("DVIPNG not found.")
-            if "--test-build" not in argv:
-                msg = QCoreApplication.translate("latex",
-                                                 "DVIPNG was not found. Make sure you include it from your Latex distribution.")
-                QMessageBox.warning(None, "LogarithmPlotter - Latex setup", msg)
+            msg = QCoreApplication.translate("latex",
+                                             "DVIPNG was not found. Make sure you include it from your Latex distribution.")
+            show_message(msg)
             valid_install = False
         else:
             try:
                 self.render("", 14, QColor(0, 0, 0, 255))
-            except Exception as e:
+            except MissingPackageException:
                 valid_install = False  # Should have sent an error message if failed to render
         return valid_install
 
@@ -105,20 +117,17 @@ class Latex(QObject):
         if self.latexSupported and not path.exists(export_path + ".png"):
             print("Rendering", latex_markup, export_path)
             # Generating file
-            try:
-                latex_path = path.join(self.tempdir.name, str(markup_hash))
-                # If the formula is just recolored or the font is just changed, no need to recreate the DVI.
-                if not path.exists(latex_path + ".dvi"):
-                    self.create_latex_doc(latex_path, latex_markup)
-                    self.convert_latex_to_dvi(latex_path)
-                    self.cleanup(latex_path)
-                # Creating four pictures of different sizes to better handle dpi.
-                self.convert_dvi_to_png(latex_path, export_path, font_size, color)
-                # self.convert_dvi_to_png(latex_path, export_path+"@2", font_size*2, color)
-                # self.convert_dvi_to_png(latex_path, export_path+"@3", font_size*3, color)
-                # self.convert_dvi_to_png(latex_path, export_path+"@4", font_size*4, color)
-            except Exception as e:  # One of the processes failed. A message will be sent every time.
-                raise e
+            latex_path = path.join(self.tempdir.name, str(markup_hash))
+            # If the formula is just recolored or the font is just changed, no need to recreate the DVI.
+            if not path.exists(latex_path + ".dvi"):
+                self.create_latex_doc(latex_path, latex_markup)
+                self.convert_latex_to_dvi(latex_path)
+                self.cleanup(latex_path)
+            # Creating four pictures of different sizes to better handle dpi.
+            self.convert_dvi_to_png(latex_path, export_path, font_size, color)
+            # self.convert_dvi_to_png(latex_path, export_path+"@2", font_size*2, color)
+            # self.convert_dvi_to_png(latex_path, export_path+"@3", font_size*3, color)
+            # self.convert_dvi_to_png(latex_path, export_path+"@4", font_size*4, color)
         img = QImage(export_path)
         # Small hack, not very optimized since we load the image twice, but you can't pass a QImage to QML and expect it to be loaded
         return f'{export_path}.png,{img.width()},{img.height()}'
@@ -147,7 +156,6 @@ class Latex(QObject):
         """
         Creates a temporary latex document with base file_hash as file name and a given expression markup latex_markup.
         """
-        ltx_path = export_path + ".tex"
         f = open(export_path + ".tex", 'w')
         f.write(DEFAULT_LATEX_DOC.substitute(markup=latex_markup))
         f.close()
@@ -193,10 +201,10 @@ class Latex(QObject):
                 output = str(out, 'utf8') + "\n" + str(err, 'utf8')
                 msg = QCoreApplication.translate("latex",
                                                  "An exception occured within the creation of the latex formula.\nProcess '{}' ended with a non-zero return code {}:\n\n{}\nPlease make sure your latex installation is correct and report a bug if so.")
-                msg = msg.format(cmd, proc.returncode, output)
-                QMessageBox.warning(None, "LogarithmPlotter - Latex", msg)
-                raise Exception(f"{cmd} process exited with return code {str(proc.returncode)}:\n{str(out, 'utf8')}\n{str(err, 'utf8')}")
-        except TimeoutExpired as e:
+                show_message(msg.format(cmd, proc.returncode, output))
+                raise RenderError(
+                    f"{cmd} process exited with return code {str(proc.returncode)}:\n{str(out, 'utf8')}\n{str(err, 'utf8')}")
+        except TimeoutExpired:
             # Process timed out
             proc.kill()
             out, err = proc.communicate()
@@ -207,12 +215,12 @@ class Latex(QObject):
                         # Package missing.
                         msg = QCoreApplication.translate("latex",
                                                          "Your LaTeX installation does not include some required packages:\n\n- {} (https://ctan.org/pkg/{})\n\nMake sure said package is installed, or disable the LaTeX rendering in LogarithmPlotter.")
-                        QMessageBox.warning(None, "LogarithmPlotter - Latex", msg.format(pkg, pkg))
-                        raise Exception("Latex: Missing package " + pkg)
+                        show_message(msg.format(pkg, pkg))
+                        raise MissingPackageException("Latex: Missing package " + pkg)
             msg = QCoreApplication.translate("latex",
                                              "An exception occured within the creation of the latex formula.\nProcess '{}' took too long to finish:\n{}\nPlease make sure your latex installation is correct and report a bug if so.")
-            QMessageBox.warning(None, "LogarithmPlotter - Latex", msg.format(cmd, output))
-            raise Exception(f"{cmd} process timed out:\n{output}")
+            show_message(msg.format(cmd, output))
+            raise RenderError(f"{cmd} process timed out:\n{output}")
 
     def cleanup(self, export_path):
         """
diff --git a/ci/drone.yml b/ci/drone.yml
index de4165e..5ef917d 100644
--- a/ci/drone.yml
+++ b/ci/drone.yml
@@ -14,6 +14,7 @@ steps:
 - name: Tests
   image: ad5001/ubuntu-pyside6-xvfb:noble-6.7.2
   commands:
+  - apt install -y texlive-base dvipng texlive-latex-extra # Install latex dependencies.
   - pytest --cov --cov-report term-missing
   - xvfb-run python3 run.py --test-build --no-check-for-updates
   - xvfb-run python3 run.py --test-build --no-check-for-updates ./ci/test1.lpf
diff --git a/poetry.lock b/poetry.lock
index e874544..cf83dea 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -315,6 +315,25 @@ pytest = ">=4.6"
 [package.extras]
 testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
 
+[[package]]
+name = "pytest-qt"
+version = "4.4.0"
+description = "pytest support for PyQt and PySide applications"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pytest-qt-4.4.0.tar.gz", hash = "sha256:76896142a940a4285339008d6928a36d4be74afec7e634577e842c9cc5c56844"},
+    {file = "pytest_qt-4.4.0-py3-none-any.whl", hash = "sha256:001ed2f8641764b394cf286dc8a4203e40eaf9fff75bf0bfe5103f7f8d0c591d"},
+]
+
+[package.dependencies]
+pluggy = ">=1.1"
+pytest = "*"
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+doc = ["sphinx", "sphinx-rtd-theme"]
+
 [[package]]
 name = "pywin32-ctypes"
 version = "0.2.3"
@@ -402,4 +421,4 @@ type = ["pytest-mypy"]
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.9,<3.13"
-content-hash = "8ce304f6a3fbab24428232c1a7d0b59ea412094e82d6b8ce47e4d93462cc235a"
+content-hash = "4693a671e927103ceeb946f688b84fdc56b8b39b2cd772d8d32475e1236d8a07"
diff --git a/pyproject.toml b/pyproject.toml
index 0aff704..bf23573 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,8 +15,5 @@ PySide6-Essentials = "^6.7.2"
 pyinstaller = "^6.10.0"
 pytest = "^8.3.3"
 pytest-cov = "^5.0.0"
+pytest-qt = "^4.4.0"
 stdeb = "^0.10.0"
-
-[build-system]
-requires = ["poetry-core"]
-build-backend = "poetry.core.masonry.api"
diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh
new file mode 100644
index 0000000..b1a1ec4
--- /dev/null
+++ b/scripts/run-tests.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+cd "$(dirname "$(readlink -f "$0" || realpath "$0")")/.."
+
+# Run python tests
+pytest --cov --cov-report term-missing
+
+
diff --git a/tests/python/test_latex.py b/tests/python/test_latex.py
new file mode 100644
index 0000000..8f1d8a2
--- /dev/null
+++ b/tests/python/test_latex.py
@@ -0,0 +1,68 @@
+import pytest
+from tempfile import TemporaryDirectory
+from shutil import which
+from os.path import exists
+from re import match
+from PySide6.QtGui import QColor
+
+from LogarithmPlotter.util import latex
+
+latex.SHOW_GUI_MESSAGES = False
+
+
+@pytest.fixture()
+def latex_obj():
+    directory = TemporaryDirectory()
+    obj = latex.Latex(directory)
+    if not obj.checkLatexInstallation():
+        raise Exception("Cannot run LaTeX tests without a proper LaTeX installation. Make sure to install a LaTeX distribution, DVIPNG, and the calligra package, and run the tests again.")
+    yield obj
+    directory.cleanup()
+
+
+class TestLatex:
+    def test_check_install(self, latex_obj: latex.Latex) -> None:
+        assert latex_obj.latexSupported == True
+        assert latex_obj.checkLatexInstallation() == True
+        bkp = [latex.DVIPNG_PATH, latex.LATEX_PATH]
+        # Check what happens when one is missing.
+        latex.DVIPNG_PATH = None
+        assert latex_obj.latexSupported == False
+        assert latex_obj.checkLatexInstallation() == False
+        latex.DVIPNG_PATH = bkp[0]
+        latex.LATEX_PATH = None
+        assert latex_obj.latexSupported == False
+        assert latex_obj.checkLatexInstallation() == False
+        # Reset
+        [latex.DVIPNG_PATH, latex.LATEX_PATH] = bkp
+
+    def test_render(self, latex_obj: latex.Latex) -> None:
+        result = latex_obj.render(r"\frac{d\sqrt{\mathrm{f}(x \times 2.3)}}{dx}", 14, QColor(0, 0, 0, 255))
+        # Ensure result format
+        assert type(result) == str
+        [path, width, height] = result.split(",")
+        assert exists(path)
+        assert match(r"\d+", width)
+        assert match(r"\d+", height)
+        # Ensure it returns errors on invalid latex.
+        with pytest.raises(latex.RenderError):
+            latex_obj.render(r"\nonexistant", 14, QColor(0, 0, 0, 255))
+        # Replace latex bin with one that returns errors
+        bkp = latex.LATEX_PATH
+        latex.LATEX_PATH = which("false")
+        with pytest.raises(latex.RenderError):
+            latex_obj.render(r"\mathrm{f}(x)", 14, QColor(0, 0, 0, 255))
+        latex.LATEX_PATH = bkp
+
+    def test_prerendered(self, latex_obj: latex.Latex) -> None:
+        args = [r"\frac{d\sqrt{\mathrm{f}(x \times 2.3)}}{dx}", 14, QColor(0, 0, 0, 255)]
+        latex_obj.render(*args)
+        prerendered = latex_obj.findPrerendered(*args)
+        assert type(prerendered) == str
+        [path, width, height] = prerendered.split(",")
+        assert exists(path)
+        assert match(r"\d+", width)
+        assert match(r"\d+", height)
+        prerendered2 = latex_obj.findPrerendered(args[0], args[1]+2, args[2])
+        assert prerendered2 == ""
+

From cb0db7fae1f18b20c1ee63d09ba68979cba9cedb Mon Sep 17 00:00:00 2001
From: Ad5001 <mail@ad5001.eu>
Date: Wed, 18 Sep 2024 22:52:53 +0200
Subject: [PATCH 2/2] Running tests from script

---
 ci/drone.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ci/drone.yml b/ci/drone.yml
index 5ef917d..5760b95 100644
--- a/ci/drone.yml
+++ b/ci/drone.yml
@@ -15,7 +15,7 @@ steps:
   image: ad5001/ubuntu-pyside6-xvfb:noble-6.7.2
   commands:
   - apt install -y texlive-base dvipng texlive-latex-extra # Install latex dependencies.
-  - pytest --cov --cov-report term-missing
+  - bash scripts/run-tests.sh
   - xvfb-run python3 run.py --test-build --no-check-for-updates
   - xvfb-run python3 run.py --test-build --no-check-for-updates ./ci/test1.lpf
   - xvfb-run python3 run.py --test-build --no-check-for-updates ./ci/test2.lpf