Latex tests

This commit is contained in:
Ad5001 2024-09-18 22:51:23 +02:00
parent 5211821a84
commit 4ac7de48b0
Signed by: Ad5001
GPG key ID: EF45F9C6AFE20160
6 changed files with 139 additions and 39 deletions

View file

@ -18,13 +18,12 @@
from PySide6.QtCore import QObject, Slot, Property, QCoreApplication from PySide6.QtCore import QObject, Slot, Property, QCoreApplication
from PySide6.QtGui import QImage, QColor from PySide6.QtGui import QImage, QColor
from PySide6.QtWidgets import QApplication, QMessageBox from PySide6.QtWidgets import QMessageBox
from os import path, remove from os import path, remove
from string import Template from string import Template
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from subprocess import Popen, TimeoutExpired, PIPE from subprocess import Popen, TimeoutExpired, PIPE
from platform import system
from shutil import which from shutil import which
from sys import argv from sys import argv
@ -36,6 +35,7 @@ If not found, it will send an alert to the user.
LATEX_PATH = which('latex') LATEX_PATH = which('latex')
DVIPNG_PATH = which('dvipng') DVIPNG_PATH = which('dvipng')
PACKAGES = ["calligra", "amsfonts", "inputenc"] PACKAGES = ["calligra", "amsfonts", "inputenc"]
SHOW_GUI_MESSAGES = "--test-build" not in argv
DEFAULT_LATEX_DOC = Template(r""" DEFAULT_LATEX_DOC = Template(r"""
\documentclass[]{minimal} \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): class Latex(QObject):
""" """
Base class to convert Latex equations into PNG images with custom font color and size. 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 valid_install = True
if LATEX_PATH is None: if LATEX_PATH is None:
print("No Latex installation found.") print("No Latex installation found.")
if "--test-build" not in argv: msg = QCoreApplication.translate("latex",
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/.")
"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)
QMessageBox.warning(None, "LogarithmPlotter - Latex setup", msg)
valid_install = False valid_install = False
elif DVIPNG_PATH is None: elif DVIPNG_PATH is None:
print("DVIPNG not found.") print("DVIPNG not found.")
if "--test-build" not in argv: msg = QCoreApplication.translate("latex",
msg = QCoreApplication.translate("latex", "DVIPNG was not found. Make sure you include it from your Latex distribution.")
"DVIPNG was not found. Make sure you include it from your Latex distribution.") show_message(msg)
QMessageBox.warning(None, "LogarithmPlotter - Latex setup", msg)
valid_install = False valid_install = False
else: else:
try: try:
self.render("", 14, QColor(0, 0, 0, 255)) 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 valid_install = False # Should have sent an error message if failed to render
return valid_install return valid_install
@ -105,20 +117,17 @@ class Latex(QObject):
if self.latexSupported and not path.exists(export_path + ".png"): if self.latexSupported and not path.exists(export_path + ".png"):
print("Rendering", latex_markup, export_path) print("Rendering", latex_markup, export_path)
# Generating file # Generating file
try: latex_path = path.join(self.tempdir.name, str(markup_hash))
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 the formula is just recolored or the font is just changed, no need to recreate the DVI. if not path.exists(latex_path + ".dvi"):
if not path.exists(latex_path + ".dvi"): self.create_latex_doc(latex_path, latex_markup)
self.create_latex_doc(latex_path, latex_markup) self.convert_latex_to_dvi(latex_path)
self.convert_latex_to_dvi(latex_path) self.cleanup(latex_path)
self.cleanup(latex_path) # Creating four pictures of different sizes to better handle dpi.
# 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, 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+"@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+"@3", font_size*3, color) # self.convert_dvi_to_png(latex_path, export_path+"@4", font_size*4, 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
img = QImage(export_path) 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 # 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()}' 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. 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 = open(export_path + ".tex", 'w')
f.write(DEFAULT_LATEX_DOC.substitute(markup=latex_markup)) f.write(DEFAULT_LATEX_DOC.substitute(markup=latex_markup))
f.close() f.close()
@ -193,10 +201,10 @@ class Latex(QObject):
output = str(out, 'utf8') + "\n" + str(err, 'utf8') output = str(out, 'utf8') + "\n" + str(err, 'utf8')
msg = QCoreApplication.translate("latex", 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.") "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) show_message(msg.format(cmd, proc.returncode, output))
QMessageBox.warning(None, "LogarithmPlotter - Latex", msg) raise RenderError(
raise Exception(f"{cmd} process exited with return code {str(proc.returncode)}:\n{str(out, 'utf8')}\n{str(err, 'utf8')}") f"{cmd} process exited with return code {str(proc.returncode)}:\n{str(out, 'utf8')}\n{str(err, 'utf8')}")
except TimeoutExpired as e: except TimeoutExpired:
# Process timed out # Process timed out
proc.kill() proc.kill()
out, err = proc.communicate() out, err = proc.communicate()
@ -207,12 +215,12 @@ class Latex(QObject):
# Package missing. # Package missing.
msg = QCoreApplication.translate("latex", 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.") "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)) show_message(msg.format(pkg, pkg))
raise Exception("Latex: Missing package " + pkg) raise MissingPackageException("Latex: Missing package " + pkg)
msg = QCoreApplication.translate("latex", 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.") "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)) show_message(msg.format(cmd, output))
raise Exception(f"{cmd} process timed out:\n{output}") raise RenderError(f"{cmd} process timed out:\n{output}")
def cleanup(self, export_path): def cleanup(self, export_path):
""" """

View file

@ -14,6 +14,7 @@ steps:
- name: Tests - name: Tests
image: ad5001/ubuntu-pyside6-xvfb:noble-6.7.2 image: ad5001/ubuntu-pyside6-xvfb:noble-6.7.2
commands: commands:
- apt install -y texlive-base dvipng texlive-latex-extra # Install latex dependencies.
- pytest --cov --cov-report term-missing - 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
- 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/test1.lpf

21
poetry.lock generated
View file

@ -315,6 +315,25 @@ pytest = ">=4.6"
[package.extras] [package.extras]
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 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]] [[package]]
name = "pywin32-ctypes" name = "pywin32-ctypes"
version = "0.2.3" version = "0.2.3"
@ -402,4 +421,4 @@ type = ["pytest-mypy"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.9,<3.13" python-versions = ">=3.9,<3.13"
content-hash = "8ce304f6a3fbab24428232c1a7d0b59ea412094e82d6b8ce47e4d93462cc235a" content-hash = "4693a671e927103ceeb946f688b84fdc56b8b39b2cd772d8d32475e1236d8a07"

View file

@ -15,8 +15,5 @@ PySide6-Essentials = "^6.7.2"
pyinstaller = "^6.10.0" pyinstaller = "^6.10.0"
pytest = "^8.3.3" pytest = "^8.3.3"
pytest-cov = "^5.0.0" pytest-cov = "^5.0.0"
pytest-qt = "^4.4.0"
stdeb = "^0.10.0" stdeb = "^0.10.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

7
scripts/run-tests.sh Normal file
View file

@ -0,0 +1,7 @@
#!/bin/bash
cd "$(dirname "$(readlink -f "$0" || realpath "$0")")/.."
# Run python tests
pytest --cov --cov-report term-missing

View file

@ -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 == ""