From d741a75de8965309c3f0b364005bbbf2736f8752 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:14:00 +0100 Subject: [PATCH] Clean up and add test coverage --- novelwriter/tools/dictionaries.py | 80 ++++++---- tests/test_tools/test_tools_dictionaries.py | 159 ++++++++++++++++++++ 2 files changed, 209 insertions(+), 30 deletions(-) create mode 100644 tests/test_tools/test_tools_dictionaries.py diff --git a/novelwriter/tools/dictionaries.py b/novelwriter/tools/dictionaries.py index 4edfb4aa..847925a0 100644 --- a/novelwriter/tools/dictionaries.py +++ b/novelwriter/tools/dictionaries.py @@ -39,6 +39,7 @@ from PyQt5.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.common import formatInt, getFileSize +from novelwriter.error import formatException logger = logging.getLogger(__name__) @@ -156,7 +157,7 @@ class GuiDictionaries(QDialog): i.stem for i in hunspell.iterdir() if i.is_file() and i.suffix == ".aff" ) self._appendLog(self.tr( - "{0} additional dictionaries currently installed" + "Additional dictionaries found: {0}" ).format(len(self._currDicts))) qApp.processEvents() @@ -196,29 +197,21 @@ class GuiDictionaries(QDialog): @pyqtSlot() def _doImportHunspell(self): """Import a hunspell dictionary from .sox or .oxt file.""" - temp = self.huInput.text() - output = self._installPath - if not output: - return - if output and temp and (path := Path(temp)).is_file(): - hunspell = output / "hunspell" - hunspell.mkdir(exist_ok=True) - try: - with ZipFile(path, mode="r") as zipObj: - for item in zipObj.namelist(): - zPath = Path(item) - if zPath.suffix in (".aff", ".dic"): - with zipObj.open(item) as zF: - oPath = hunspell / zPath.name - oPath.write_bytes(zF.read()) - size = getFileSize(oPath) - self._appendLog(self.tr( - "Added: {0} [{1}B]" - ).format(zPath.name, formatInt(size))) - except Exception as exc: - SHARED.error(self.tr("Could not process dictionary file."), exc=exc) - else: - SHARED.error(self.tr("File not found.")) + procErr = self.tr("Could not process dictionary file") + if self._installPath: + temp = self.huInput.text() + if temp and (path := Path(temp)).is_file(): + hunspell = self._installPath / "hunspell" + hunspell.mkdir(exist_ok=True) + try: + nAff, nDic = self._extractDicts(path, hunspell) + if nAff == 0 or nDic == 0: + self._appendLog(procErr, err=True) + except Exception as exc: + self._appendLog(procErr, err=True) + self._appendLog(formatException(exc), err=True) + else: + self._appendLog(procErr, err=True) return @pyqtSlot() @@ -243,13 +236,40 @@ class GuiDictionaries(QDialog): # Internal Functions ## - def _appendLog(self, text: str) -> None: + def _extractDicts(self, path: Path, output: Path) -> tuple[int, int]: + """Extract a zip archive and return the number of .aff and .dic + files found in it. + """ + nAff = nDic = 0 + with ZipFile(path, mode="r") as zipObj: + for item in zipObj.namelist(): + zPath = Path(item) + if zPath.suffix not in (".aff", ".dic"): + continue + nAff += 1 if zPath.suffix == ".aff" else 0 + nDic += 1 if zPath.suffix == ".dic" else 0 + with zipObj.open(item) as zF: + oPath = output / zPath.name + oPath.write_bytes(zF.read()) + size = getFileSize(oPath) + self._appendLog(self.tr( + "Added: {0} [{1}B]" + ).format(zPath.name, formatInt(size))) + return nAff, nDic + + def _appendLog(self, text: str, err: bool = False) -> None: """Append a line to the log output.""" - self.infoBox.moveCursor(QTextCursor.MoveOperation.End) - if self.infoBox.textCursor().position() > 0: - self.infoBox.insertPlainText("\n") - self.infoBox.insertPlainText(text) - self.infoBox.moveCursor(QTextCursor.MoveOperation.End) + cursor = self.infoBox.textCursor() + cursor.movePosition(QTextCursor.MoveOperation.End) + if cursor.position() > 0: + cursor.insertText("\n") + if err: + cursor.insertHtml(f"{text}") + else: + cursor.insertText(text) + cursor.movePosition(QTextCursor.MoveOperation.End) + cursor.deleteChar() + self.infoBox.setTextCursor(cursor) return # END Class GuiDictionaries diff --git a/tests/test_tools/test_tools_dictionaries.py b/tests/test_tools/test_tools_dictionaries.py new file mode 100644 index 00000000..5c3b471e --- /dev/null +++ b/tests/test_tools/test_tools_dictionaries.py @@ -0,0 +1,159 @@ +""" +novelWriter – Dictionary Downloader Tester +========================================== + +This file is a part of novelWriter +Copyright 2018–2023, Veronica Berglyd Olsen + +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 +import enchant + +from zipfile import ZipFile + +from tools import getGuiItem +from mocked import causeException + +from PyQt5.QtGui import QDesktopServices +from PyQt5.QtWidgets import QFileDialog + +from novelwriter import SHARED +from novelwriter.tools.dictionaries import GuiDictionaries + + +@pytest.mark.gui +def testToolDictionaries_Main(qtbot, monkeypatch, nwGUI, fncPath): + """Test the Dictionaries downloader tool.""" + monkeypatch.setattr(enchant, "get_user_config_dir", lambda *a: str(fncPath)) + + # Fail to open + with monkeypatch.context() as mp: + mp.setattr(enchant, "get_user_config_dir", lambda *a: causeException) + nwGUI.showDictionariesDialog() + assert SHARED.alert is not None + assert SHARED.alert.logMessage == "Could not initialise the dialog." + + # Open the tool + nwGUI.showDictionariesDialog() + qtbot.waitUntil(lambda: getGuiItem("GuiDictionaries") is not None, timeout=1000) + + nwDicts = getGuiItem("GuiDictionaries") + assert isinstance(nwDicts, GuiDictionaries) + assert nwDicts.isVisible() + assert nwDicts.inPath.text() == str(fncPath) + + # Allow Open Dir + SHARED._alert = None + with monkeypatch.context() as mp: + mp.setattr(QDesktopServices, "openUrl", lambda *a: None) + nwDicts._doOpenInstallLocation() + assert SHARED.alert is None + + # Fail Open Dir + nwDicts.inPath.setText("/foo/bar") + nwDicts._doOpenInstallLocation() + assert SHARED.alert is not None + assert SHARED.alert.logMessage == "Path not found." + nwDicts.inPath.setText(str(fncPath)) + + # Create Mock Dicts + foDict = fncPath / "freeoffice.sox" + with ZipFile(foDict, mode="w") as zipObj: + zipObj.writestr("en_GB.aff", "foobar") + zipObj.writestr("en_GB.dic", "foobar") + zipObj.writestr("README.txt", "foobar") + + loDict = fncPath / "libreoffice.oxt" + with ZipFile(loDict, mode="w") as zipObj: + zipObj.writestr("en_US/en_US.aff", "foobar") + zipObj.writestr("en_US/en_US.dic", "foobar") + zipObj.writestr("README.txt", "foobar") + + emDict = fncPath / "empty.oxt" + with ZipFile(emDict, mode="w") as zipObj: + zipObj.writestr("README.txt", "foobar") + + noFile = fncPath / "foobar.oxt" + noDict = fncPath / "foobar.sox" + noDict.write_bytes(b"foobar") + + assert nwDicts.infoBox.toPlainText().splitlines()[-1] == ( + "Additional dictionaries found: 0" + ) + + # Import Free Office Dictionary + with monkeypatch.context() as mp: + mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(foDict), "")) + nwDicts._doBrowseHunspell() + assert nwDicts.huInput.text() == str(foDict) + nwDicts._doImportHunspell() + assert (fncPath / "hunspell").is_dir() + assert (fncPath / "hunspell" / "en_GB.aff").is_file() + assert (fncPath / "hunspell" / "en_GB.dic").is_file() + assert nwDicts.infoBox.blockCount() == 3 + + # Import Libre Office Dictionary + with monkeypatch.context() as mp: + mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(loDict), "")) + nwDicts._doBrowseHunspell() + assert nwDicts.huInput.text() == str(loDict) + nwDicts._doImportHunspell() + assert (fncPath / "hunspell").is_dir() + assert (fncPath / "hunspell" / "en_US.aff").is_file() + assert (fncPath / "hunspell" / "en_US.dic").is_file() + assert nwDicts.infoBox.blockCount() == 5 + + # Handle Unreadable File + with monkeypatch.context() as mp: + mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(noDict), "")) + nwDicts._doBrowseHunspell() + assert nwDicts.huInput.text() == str(noDict) + nwDicts._doImportHunspell() + assert nwDicts.infoBox.blockCount() == 7 + + # Handle File w/No Dicts + with monkeypatch.context() as mp: + mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(emDict), "")) + nwDicts._doBrowseHunspell() + assert nwDicts.huInput.text() == str(emDict) + nwDicts._doImportHunspell() + assert nwDicts.infoBox.blockCount() == 8 + assert nwDicts.infoBox.toPlainText().splitlines()[-1] == ( + "Could not process dictionary file" + ) + + # Handle Non-Existing File + with monkeypatch.context() as mp: + mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(noFile), "")) + nwDicts._doBrowseHunspell() + nwDicts._doImportHunspell() + assert nwDicts.infoBox.blockCount() == 9 + assert nwDicts.infoBox.toPlainText().splitlines()[-1] == ( + "Could not process dictionary file" + ) + + # Re-init + nwDicts.initDialog() + assert nwDicts.infoBox.blockCount() == 10 + assert nwDicts.infoBox.toPlainText().splitlines()[-1] == ( + "Additional dictionaries found: 2" + ) + + # Close + nwDicts._doClose() + # qtbot.stop() + +# END Test testToolDictionaries_Main