From 4ac7de48b05527a07d0ab1e25d2f1662120f0785 Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Wed, 18 Sep 2024 22:51:23 +0200 Subject: [PATCH] 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 == "" +