diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py index b2f92a96..83ff2540 100644 --- a/nw/core/spellcheck.py +++ b/nw/core/spellcheck.py @@ -28,8 +28,7 @@ import nw import logging import os - -from difflib import get_close_matches +import difflib from nw.constants import nwConst, isoLanguage @@ -70,14 +69,16 @@ class NWSpellCheck(): """ if self.projectDict is not None and newWord not in self.projDict: newWord = newWord.strip() - self.projDict.append(newWord) try: with open(self.projectDict, mode="a+", encoding="utf-8") as outFile: outFile.write("%s\n" % newWord) + self.projDict.append(newWord) except Exception as e: logger.error("Failed to add word to project word list %s" % str(self.projectDict)) logger.error(str(e)) - return + return False + return True + return False def listDictionaries(self): """Dummy function. @@ -109,9 +110,12 @@ class NWSpellCheck(): """ self.projDict = [] if projectDict is not None: - self.projectDict = projectDict if not os.path.isfile(projectDict): - return + self.projectDict = None + return False + else: + self.projectDict = projectDict + try: logger.debug("Loading project word list") with open(projectDict, mode="r", encoding="utf-8") as wordsFile: @@ -123,7 +127,9 @@ class NWSpellCheck(): except Exception as e: logger.error("Failed to load project word list") logger.error(str(e)) - return + return False + + return True # END Class NWSpellCheck @@ -287,7 +293,7 @@ class NWSpellSimple(NWSpellCheck): if len(theWord) == 0: return [] - theMatches = get_close_matches(theWord.lower(), self.WORDS, n=10, cutoff=0.75) + theMatches = difflib.get_close_matches(theWord.lower(), self.WORDS, n=10, cutoff=0.75) theOptions = [] for aWord in theMatches: if len(aWord) == 0: @@ -314,14 +320,12 @@ class NWSpellSimple(NWSpellCheck): retList = [] for dictFile in os.listdir(self.mainConf.dictPath): - theBits = os.path.splitext(dictFile) - if len(theBits) != 2: - continue - if theBits[1] != ".dict": + fRoot, fExt = os.path.splitext(dictFile) + if fExt != ".dict": continue - spName = "%s [%s]" % (self.expandLanguage(theBits[0]), nwConst.SP_INTERNAL) - retList.append((theBits[0], spName)) + spName = "%s [%s]" % (self.expandLanguage(fRoot), nwConst.SP_INTERNAL) + retList.append((fRoot, spName)) return retList diff --git a/tests/README.md b/tests/README.md index a559e00c..51dd9dca 100644 --- a/tests/README.md +++ b/tests/README.md @@ -59,12 +59,12 @@ Available markers are: To filter specific groups of tests, use the `-k` switch. The commands for the respective test categories are listed below. -| Type | Test Target | Source File(s) | Marker | Filter | -| :--- | :----------------- | :------------------ | :-------- | :-------------------- | -| Unit | Core functions | nw/core/tools.py | `-m core` | `-k testCoreTools` | -| Unit | NWDoc class | nw/core/document.py | `-m core` | `-k testCoreDocument` | -| Unit | NWIndex class | nw/core/index.py | `-m core` | `-k testCoreIndex` | -| Unit | NWItem class | nw/core/item.py | `-m core` | `-k testCoreItem` | -| Unit | NWTree class | nw/core/tree.py | `-m core` | `-k testCoreTree` | -| Unit | OptionsState class | nw/core/options.py | `-m core` | `-k testCoreOptions` | - +| Type | Test Target | Source File(s) | Marker | Filter | +| :--- | :----------------- | :-------------------- | :-------- | :-------------------- | +| Unit | Core functions | nw/core/tools.py | `-m core` | `-k testCoreTools` | +| Unit | NWDoc class | nw/core/document.py | `-m core` | `-k testCoreDocument` | +| Unit | NWIndex class | nw/core/index.py | `-m core` | `-k testCoreIndex` | +| Unit | NWItem class | nw/core/item.py | `-m core` | `-k testCoreItem` | +| Unit | NWSpell* classes | nw/core/spellcheck.py | `-m core` | `-k testCoreSpell` | +| Unit | NWTree class | nw/core/tree.py | `-m core` | `-k testCoreTree` | +| Unit | OptionsState class | nw/core/options.py | `-m core` | `-k testCoreOptions` | diff --git a/tests/dummy.py b/tests/dummy.py index c97be669..fbb91cbd 100644 --- a/tests/dummy.py +++ b/tests/dummy.py @@ -47,5 +47,5 @@ class StatusBar(): # Dummy functions that will raise errors instead. # =========================================================================== # -def dummyIO(*args, **kwargs): +def causeOSError(*args, **kwargs): raise OSError diff --git a/tests/test_core_document.py b/tests/test_core_document.py index 904c9d25..9e16c66e 100644 --- a/tests/test_core_document.py +++ b/tests/test_core_document.py @@ -5,6 +5,8 @@ import os import pytest +from dummy import causeOSError + from nw.core import NWProject, NWDoc from nw.core.item import NWItem from nw.constants import nwItemClass, nwItemLayout @@ -75,10 +77,7 @@ def testCoreDocument_LoadSave(monkeypatch, dummyGUI, nwMinimal): assert inFile.read() == theText # Cause open() to fail while saving - def dummyIO(*args, **kwargs): - raise OSError - - monkeypatch.setattr("builtins.open", dummyIO) + monkeypatch.setattr("builtins.open", causeOSError) assert not theDoc.saveDocument(theText) monkeypatch.undo() @@ -91,7 +90,7 @@ def testCoreDocument_LoadSave(monkeypatch, dummyGUI, nwMinimal): assert os.path.isfile(docPath) # Cause the delete to fail - monkeypatch.setattr("os.unlink", dummyIO) + monkeypatch.setattr("os.unlink", causeOSError) assert not theDoc.deleteDocument(xHandle) monkeypatch.undo() diff --git a/tests/test_core_options.py b/tests/test_core_options.py index 20830d69..963a12c4 100644 --- a/tests/test_core_options.py +++ b/tests/test_core_options.py @@ -6,6 +6,8 @@ import os import json import pytest +from dummy import causeOSError + from nw.core import NWProject from nw.core.options import OptionState from nw.constants import nwFiles @@ -44,10 +46,7 @@ def testCoreOptions_LoadSave(monkeypatch, dummyGUI, tmpDir): assert theProject.projMeta == tmpDir # Cause open() to fail - def dummyIO(*args, **kwargs): - raise OSError - - monkeypatch.setattr("builtins.open", dummyIO) + monkeypatch.setattr("builtins.open", causeOSError) assert not theOpts.loadSettings() assert not theOpts.saveSettings() monkeypatch.undo() diff --git a/tests/test_core_spell.py b/tests/test_core_spell.py new file mode 100644 index 00000000..6b6d7207 --- /dev/null +++ b/tests/test_core_spell.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +"""novelWriter Spell Check Class Tester +""" + +import os +import sys +import pytest + +from difflib import get_close_matches + +from dummy import causeOSError +from tools import readFile, writeFile + +from nw.core.spellcheck import NWSpellCheck, NWSpellEnchant, NWSpellSimple +from nw.constants import nwConst + +@pytest.mark.core +def testCoreSpell_Super(monkeypatch, tmpDir, tmpConf): + """Test the spell checker super class + """ + wList = os.path.join(tmpDir, "wordlist.txt") + writeFile(wList, "a_word\nb_word\nc_word\n") + + spChk = NWSpellCheck() + spChk.mainConf = tmpConf + + # Check that dummy functions return results that reflects that spell + # checking is effectively disabled + assert spChk.setLanguage("", "") is None + assert spChk.checkWord("") + assert spChk.suggestWords("") == [] + assert spChk.listDictionaries() == [] + assert spChk.describeDict() == ("", "") + + # Check language info + assert NWSpellCheck.expandLanguage("en") == "English" + assert NWSpellCheck.expandLanguage("en_GB") == "English (GB)" + + # Add a word to the user's dictionary + assert spChk._readProjectDictionary("dummy") is False + monkeypatch.setattr("builtins.open", causeOSError) + assert spChk._readProjectDictionary(wList) is False + monkeypatch.undo() + assert spChk._readProjectDictionary(wList) is True + assert spChk.projectDict == wList + + # Cannot write to file + monkeypatch.setattr("builtins.open", causeOSError) + assert spChk.addWord("d_word") is False + monkeypatch.undo() + assert readFile(wList) == "a_word\nb_word\nc_word\n" + + # First time, OK + assert spChk.addWord("d_word") is True + assert readFile(wList) == "a_word\nb_word\nc_word\nd_word\n" + + # But not added twice + assert spChk.addWord("d_word") is False + assert readFile(wList) == "a_word\nb_word\nc_word\nd_word\n" + +# END Test testCoreSpell_Super + +@pytest.mark.core +def testCoreSpell_Enchant(monkeypatch, tmpDir, tmpConf): + """Test the pyenchant spell checker + """ + wList = os.path.join(tmpDir, "wordlist.txt") + writeFile(wList, "a_word\nb_word\nc_word\n") + + # Block the enchant package (and trigger the dummy class) + monkeypatch.setitem(sys.modules, "enchant", None) + spChk = NWSpellEnchant() + + spChk.setLanguage("en", wList) + assert spChk.setLanguage("", "") is None + assert spChk.checkWord("") + assert spChk.suggestWords("") == [] + assert spChk.listDictionaries() == [] + assert spChk.describeDict() == ("", "") + + monkeypatch.undo() + + # Load the proper enchant package + spChk = NWSpellEnchant() + spChk.mainConf = tmpConf + spChk.setLanguage("en", wList) + + assert spChk.checkWord("a_word") + assert spChk.checkWord("b_word") + assert spChk.checkWord("c_word") + assert not spChk.checkWord("d_word") + + spChk.addWord("d_word") + assert spChk.checkWord("d_word") + + wSuggest = spChk.suggestWords("wrod") + assert len(wSuggest) > 0 + assert "word" in wSuggest + + dList = spChk.listDictionaries() + assert len(dList) > 0 + + aTag, aName = spChk.describeDict() + assert aTag == "en" + assert aName != "" + +# END Test testCoreSpell_Enchant + +@pytest.mark.core +def testCoreSpell_Simple(monkeypatch, tmpDir, tmpConf): + """Test the fallback simple spell checker + """ + wList = os.path.join(tmpDir, "wordlist.txt") + wDict = os.path.join(tmpDir, "en.dict") + writeFile(wList, "a_word\nb_word\nc_word\n") + writeFile(wDict, "# Comment\ne_word\nf_word\ng_word\n") + + spChk = NWSpellSimple() + spChk.mainConf = tmpConf + spChk.mainConf.dictPath = tmpDir + + # Load dictionary, but fail + monkeypatch.setattr("builtins.open", causeOSError) + spChk.setLanguage("en", wList) + assert spChk.spellLanguage is None + assert spChk.WORDS == spChk.projDict + monkeypatch.undo() + + # Load dictionary properly + spChk.setLanguage("en", wList) + assert spChk.projDict == ["a_word", "b_word", "c_word"] + assert spChk.WORDS == ["e_word", "f_word", "g_word", "a_word", "b_word", "c_word"] + + # Check words + assert spChk.checkWord("a_word") + assert spChk.checkWord("b_word") + assert spChk.checkWord("c_word") + assert not spChk.checkWord("d_word") + assert spChk.checkWord("e_word") + assert spChk.checkWord("f_word") + assert spChk.checkWord("g_word") + + # Add word + spChk.addWord("d_word") + assert spChk.checkWord("d_word") + + # Check spelling + assert spChk.suggestWords(" \t") == [] + + wSuggest = spChk.suggestWords("d_wrod") + assert len(wSuggest) > 0 + assert "d_word" in wSuggest + + # Break the matching + monkeypatch.setattr("difflib.get_close_matches", lambda *args, **kwargs: [""]) + assert spChk.suggestWords("word") == [] + monkeypatch.undo() + + # Capitalisation + wSuggest = spChk.suggestWords("D_wrod") + assert len(wSuggest) > 0 + assert "D_word" in wSuggest + + # List dictionaries + assert spChk.listDictionaries() == [("en", "English [%s]" % nwConst.SP_INTERNAL)] + + # Description + aTag, aName = spChk.describeDict() + assert aTag == "en" + assert aName == nwConst.SP_INTERNAL + +# END Test testCoreSpell_Simple diff --git a/tests/tools.py b/tests/tools.py index e35d9e08..185a9fa0 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -70,3 +70,15 @@ def getGuiItem(theName): if qWidget.objectName() == theName: return qWidget return None + +def readFile(fileName): + """Returns the content of a file as a string. + """ + with open(fileName, mode="r", encoding="utf8") as inFile: + return inFile.read() + +def writeFile(fileName, fileData): + """Write the contents of a string to a file. + """ + with open(fileName, mode="w", encoding="utf8") as outFile: + outFile.write(fileData)