diff --git a/nw/core/document.py b/nw/core/document.py index 2597eac8..7ae8f4de 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -168,7 +168,8 @@ class NWDoc(): os.unlink(docPath) os.rename(docTemp, docPath) - self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName) + if self._theItem is not None: + self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName) return True diff --git a/nw/core/item.py b/nw/core/item.py index 6793af39..477f2df9 100644 --- a/nw/core/item.py +++ b/nw/core/item.py @@ -43,7 +43,7 @@ class NWItem(): self.itemName = "" self.itemHandle = None self.itemParent = None - self.itemOrder = None + self.itemOrder = 0 self.itemType = nwItemType.NO_TYPE self.itemClass = nwItemClass.NO_CLASS self.itemLayout = nwItemLayout.NO_LAYOUT @@ -96,20 +96,21 @@ class NWItem(): return False if "handle" in xItem.attrib: - self.itemHandle = xItem.attrib["handle"] + self.setHandle(xItem.attrib["handle"]) else: logger.error("XML item entry does not have a handle") return False if "parent" in xItem.attrib: - self.itemParent = xItem.attrib["parent"] + self.setParent(xItem.attrib["parent"]) + + if "order" in xItem.attrib: + self.setOrder(xItem.attrib["order"]) retStatus = True for xValue in xItem: if xValue.tag == "name": self.setName(xValue.text) - elif xValue.tag == "order": - self.setOrder(xValue.text) elif xValue.tag == "type": self.setType(xValue.text) elif xValue.tag == "class": @@ -156,7 +157,10 @@ class NWItem(): def setName(self, theName): """Set the item name. """ - self.itemName = theName.strip() + if isinstance(theName, str): + self.itemName = theName.strip() + else: + self.itemName = "" return def setHandle(self, theHandle): diff --git a/nw/core/options.py b/nw/core/options.py index 35358fc0..e0a937dc 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -176,11 +176,7 @@ class OptionState(): """ if getGroup in self.theState: if getName in self.theState[getGroup]: - try: - return self.theState[getGroup][getName] - except Exception as e: - logger.warning(str(e)) - return defaultValue + return self.theState[getGroup][getName] return defaultValue def getString(self, getGroup, getName, defaultValue): @@ -189,11 +185,7 @@ class OptionState(): """ if getGroup in self.theState: if getName in self.theState[getGroup]: - try: - return str(self.theState[getGroup][getName]) - except Exception as e: - logger.warning(str(e)) - return defaultValue + return str(self.theState[getGroup][getName]) return defaultValue def getInt(self, getGroup, getName, defaultValue): @@ -228,11 +220,7 @@ class OptionState(): """ if getGroup in self.theState: if getName in self.theState[getGroup]: - try: - return bool(self.theState[getGroup][getName]) - except Exception as e: - logger.warning(str(e)) - return defaultValue + return bool(self.theState[getGroup][getName]) return defaultValue ## diff --git a/nw/core/project.py b/nw/core/project.py index f8e45194..1a834d73 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -28,12 +28,10 @@ import nw import logging import os +import shutil from lxml import etree from time import time -from shutil import make_archive, unpack_archive, copyfile - -from PyQt5.QtWidgets import QMessageBox from nw.core.tree import NWTree from nw.core.item import NWItem @@ -436,26 +434,15 @@ class NWProject(): xRoot = nwXML.getroot() nwxRoot = xRoot.tag - appVersion = "Unknown" - hexVersion = "0x0" - fileVersion = "Unknown" - self.saveCount = 0 - self.autoCount = 0 - - if "appVersion" in xRoot.attrib: - appVersion = xRoot.attrib["appVersion"] - if "hexVersion" in xRoot.attrib: - hexVersion = xRoot.attrib["hexVersion"] - if "fileVersion" in xRoot.attrib: - fileVersion = xRoot.attrib["fileVersion"] + appVersion = xRoot.attrib.get("appVersion", "Unknown") + hexVersion = xRoot.attrib.get("hexVersion", "0x0") + fileVersion = xRoot.attrib.get("fileVersion", "Unknown") # The following are deprecated and will be removed - if "saveCount" in xRoot.attrib: - self.saveCount = checkInt(xRoot.attrib["saveCount"], 0, False) - if "autoCount" in xRoot.attrib: - self.autoCount = checkInt(xRoot.attrib["autoCount"], 0, False) - if "editTime" in xRoot.attrib: - self.editTime = checkInt(xRoot.attrib["editTime"], 0, False) + # The settings have been moved to the tag + self.saveCount = checkInt(xRoot.attrib.get("saveCount", 0), 0, False) + self.autoCount = checkInt(xRoot.attrib.get("autoCount", 0), 0, False) + self.editTime = checkInt(xRoot.attrib.get("editTime", 0), 0, False) logger.verbose("XML root is %s" % nwxRoot) logger.verbose("File version is %s" % fileVersion) @@ -483,20 +470,19 @@ class NWProject(): # parser will lose the autoReplace settings if allowed to # read the file. Introduced in version 0.10. - if fileVersion == "1.0" and self.mainConf.showGUI: - msgBox = QMessageBox() - msgRes = msgBox.question(self.theParent, "Old Project Version", ( + if fileVersion == "1.0": + msgRes = self.theParent.askQuestion("Old Project Version", ( "The project file and data is created by a novelWriter version " "lower than 0.7. Do you want to upgrade the project to the " "most recent format?

Note that after the upgrade, you " "cannot open the project with an older version of novelWriter " "any more, so make sure you have a recent backup." )) - if msgRes != QMessageBox.Yes: + if not msgRes: self.clearProject() return False - elif fileVersion != "1.1" and fileVersion != "1.2" and self.mainConf.showGUI: + elif fileVersion != "1.1" and fileVersion != "1.2": self.makeAlert(( "Unknown or unsupported novelWriter project file format. " "The project cannot be opened by this version of novelWriter. " @@ -510,9 +496,8 @@ class NWProject(): # Check novelWriter Version # ========================= - if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI: - msgBox = QMessageBox() - msgRes = msgBox.question(self.theParent, "Version Conflict", ( + if int(hexVersion, 16) > int(nw.__hexversion__, 16): + msgRes = self.theParent.askQuestion("Version Conflict", ( "This project was saved by a newer version of novelWriter, version %s. " "This is version %s. If you continue to open the project, some attributes " "and settings may not be preserved, but the overall project should be fine. " @@ -520,7 +505,7 @@ class NWProject(): ) % ( appVersion, nw.__version__ )) - if msgRes != QMessageBox.Yes: + if not msgRes: self.clearProject() return False @@ -835,15 +820,15 @@ class NWProject(): try: self._clearLockFile() - make_archive(baseName, "zip", self.projPath, ".") + shutil.make_archive(baseName, "zip", self.projPath, ".") self._writeLockFile() + logger.info("Backup written to: %s" % archName) if doNotify: self.theParent.makeAlert( "Backup archive file written to: %s.zip" % os.path.join(cleanName, archName), nwAlert.INFO ) - else: - logger.info("Backup written to: %s" % archName) + except Exception as e: self.theParent.makeAlert( ["Could not write backup archive.", str(e)], @@ -874,7 +859,7 @@ class NWProject(): self.setProjectPath(projPath, newProject=True) try: - unpack_archive(pkgSample, projPath) + shutil.unpack_archive(pkgSample, projPath) isSuccess = True except Exception as e: self.makeAlert( @@ -887,14 +872,14 @@ class NWProject(): try: srcProj = os.path.join(srcSample, nwFiles.PROJ_FILE) dstProj = os.path.join(projPath, nwFiles.PROJ_FILE) - copyfile(srcProj, dstProj) + shutil.copyfile(srcProj, dstProj) srcContent = os.path.join(srcSample, "content") dstContent = os.path.join(projPath, "content") for srcFile in os.listdir(srcContent): srcDoc = os.path.join(srcContent, srcFile) dstDoc = os.path.join(dstContent, srcFile) - copyfile(srcDoc, dstDoc) + shutil.copyfile(srcDoc, dstDoc) isSuccess = True @@ -998,11 +983,15 @@ class NWProject(): "You must set a valid backup path in preferences to use " "the automatic project backup feature." ), nwAlert.WARN) + return False + if self.projName == "": self.theParent.makeAlert(( "You must set a valid project name in project settings to " "use the automatic project backup feature." ), nwAlert.WARN) + return False + return True def setSpellCheck(self, theMode): @@ -1011,12 +1000,15 @@ class NWProject(): if self.spellCheck != theMode: self.spellCheck = theMode self.setProjectChanged(True) - return True + return self.spellCheck def setSpellLang(self, theLang): """Set the project-specific spell check language. """ - self.projLang = checkString(theLang, None, True) + theLang = checkString(theLang, None, True) + if self.projLang != theLang: + self.projLang = theLang + self.setProjectChanged(True) return True def setAutoOutline(self, theMode): @@ -1025,7 +1017,7 @@ class NWProject(): if self.autoOutline != theMode: self.autoOutline = theMode self.setProjectChanged(True) - return True + return self.autoOutline def setTreeOrder(self, newOrder): """A list representing the linear/flattened order of project @@ -1072,7 +1064,7 @@ class NWProject(): if nwItem.itemStatus in replaceMap.keys(): nwItem.setStatus(replaceMap[nwItem.itemStatus]) self.setProjectChanged(True) - return + return True def setImportColours(self, newCols): """Update the list of note file importance flags. Also iterate @@ -1084,14 +1076,15 @@ class NWProject(): if nwItem.itemStatus in replaceMap.keys(): nwItem.setStatus(replaceMap[nwItem.itemStatus]) self.setProjectChanged(True) - return + return True def setAutoReplace(self, autoReplace): """Update the auto-replace dictionary. This replaces the entire dictionary, so alterations have to be made in a copy. """ self.autoReplace = autoReplace - return + self.setProjectChanged(True) + return True def setTitleFormat(self, titleFormat): """Set the formatting of titles in the project. @@ -1099,7 +1092,7 @@ class NWProject(): for valKey, valEntry in titleFormat.items(): if valKey in self.titleFormat: self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey], False) - return + return True def setProjectChanged(self, bValue): """Toggle the project changed flag, and propagate the @@ -1132,13 +1125,11 @@ class NWProject(): sentItems = [] iterItems = self.projTree.handles() n = 0 - nMax = len(iterItems) + nMax = min(len(iterItems), 10000) while n < nMax: tHandle = iterItems[n] tItem = self.projTree[tHandle] n += 1 - if n > 10000: - return # Just in case if tItem is None: # Technically a bug since treeOrder is built from the # same data as projTree @@ -1154,10 +1145,11 @@ class NWProject(): yield tItem elif tItem.itemParent in iterItems: # Item's parent exists, but hasn't been sent yet, so add - # it again to the end + # it again to the end, but make sure this doesn't get + # out hand, so we cap at 10000 items logger.warning("Item %s found before its parent" % tHandle) iterItems.append(tHandle) - nMax = len(iterItems) + nMax = min(len(iterItems), 10000) else: # Item is orphaned logger.error("Item %s has no parent in current tree" % tHandle) @@ -1196,13 +1188,12 @@ class NWProject(): if not os.path.isfile(lockFile): return [] + theLines = [] try: with open(lockFile, mode="r", encoding="utf8") as inFile: theData = inFile.read() theLines = theData.splitlines() - if len(theLines) == 4: - return theLines - else: + if len(theLines) != 4: return ["ERROR"] except Exception as e: @@ -1210,7 +1201,7 @@ class NWProject(): logger.error(str(e)) return ["ERROR"] - return ["ERROR"] + return theLines def _writeLockFile(self): """Writes a lock file to the project folder. @@ -1243,13 +1234,12 @@ class NWProject(): if os.path.isfile(lockFile): try: os.unlink(lockFile) - return True except Exception as e: logger.error("Failed to remove project lockfile") logger.error(str(e)) return False - return None + return True def _checkFolder(self, thePath): """Check if a folder exists, and if it doesn't, create it. @@ -1292,7 +1282,7 @@ class NWProject(): back into the project tree. """ if self.projPath is None: - return + return False # Then check the files in the data folder logger.debug("Checking files in project content folder") @@ -1352,7 +1342,7 @@ class NWProject(): orphItem.setLayout(oLayout) self.projTree.append(oHandle, None, orphItem) - return + return True def _appendSessionStats(self): """Append session statistics to the sessions log file. @@ -1363,21 +1353,27 @@ class NWProject(): sessionFile = os.path.join(self.projMeta, nwFiles.SESS_STATS) isFile = os.path.isfile(sessionFile) - with open(sessionFile, mode="a+", encoding="utf8") as outFile: - if not isFile: - # It's a new file, so add a header - if self.lastWCount > 0: - outFile.write("# Offset %d\n" % self.lastWCount) - outFile.write("# %-17s %-19s %8s %8s\n" % ( - "Start Time", "End Time", "Novel", "Notes" + try: + with open(sessionFile, mode="a+", encoding="utf8") as outFile: + if not isFile: + # It's a new file, so add a header + if self.lastWCount > 0: + outFile.write("# Offset %d\n" % self.lastWCount) + outFile.write("# %-17s %-19s %8s %8s\n" % ( + "Start Time", "End Time", "Novel", "Notes" + )) + + outFile.write("%-19s %-19s %8d %8d\n" % ( + formatTimeStamp(self.projOpened), + formatTimeStamp(time()), + self.novelWCount, + self.notesWCount, )) - outFile.write("%-19s %-19s %8d %8d\n" % ( - formatTimeStamp(self.projOpened), - formatTimeStamp(time()), - self.novelWCount, - self.notesWCount, - )) + except Exception as e: + logger.error("Failed to write session stats file") + logger.error(str(e)) + return False return True @@ -1490,7 +1486,8 @@ class NWProject(): os.unlink(rmFile) except Exception as e: logger.error(str(e)) + return False - return + return True # END Class NWProject 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/nw/core/status.py b/nw/core/status.py index 7a1c7127..ccec2e73 100644 --- a/nw/core/status.py +++ b/nw/core/status.py @@ -79,6 +79,7 @@ class NWStatus(): theStatus = checkInt(theStatus, 0, False) if theStatus >= 0 and theStatus < self._theLength: return self._theLabels[theStatus] + return self._theLabels[0] def setNewEntries(self, newList): """Update the list of entries after they have been modified by @@ -108,7 +109,8 @@ class NWStatus(): return def countEntry(self, theLabel): - """Lookup the usage count of a given entry. + """Increment the counter for a given label. This should be used + together with resetCounts in a loop over project items. """ theIndex = self.lookupEntry(theLabel) if theIndex is not None: @@ -136,18 +138,9 @@ class NWStatus(): for xChild in xParent: theLabels.append(xChild.text) - if "red" in xChild.attrib: - cR = checkInt(xChild.attrib["red"], 0, False) - else: - cR = 0 - if "green" in xChild.attrib: - cG = checkInt(xChild.attrib["green"], 0, False) - else: - cG = 0 - if "blue" in xChild.attrib: - cB = checkInt(xChild.attrib["blue"], 0, False) - else: - cB = 0 + cR = checkInt(xChild.attrib.get("red", 0), 0, False) + cG = checkInt(xChild.attrib.get("green", 0), 0, False) + cB = checkInt(xChild.attrib.get("blue", 0), 0, False) theColours.append((cR, cG, cB)) if len(theLabels) > 0: diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index df6c4a09..f3166d02 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -68,16 +68,16 @@ class ToHtml(Tokenizer): # Setters ## - def setPreview(self, forPreview, doComments, doSynopsis): + def setPreview(self, doComments, doSynopsis): """If we're using this class to generate markdown preview, we need to make a few changes to formatting, which is managed by these flags. """ - if forPreview: - self.genMode = self.M_PREVIEW - self.doKeywords = True - self.doComments = doComments - self.doSynopsis = doSynopsis + self.genMode = self.M_PREVIEW + self.doKeywords = True + self.doComments = doComments + self.doSynopsis = doSynopsis + return def setStyles(self, cssStyles): @@ -141,11 +141,13 @@ class ToHtml(Tokenizer): # For novel files for export, we bump the titles one level # up as this is more useful for printing and word processor # imports. - h1 = "h1 class='title'" + h1Cl = " class='title'" + h1 = "h1" h2 = "h1" h3 = "h2" h4 = "h3" else: + h1Cl = "" h1 = "h1" h2 = "h2" h3 = "h3" @@ -193,7 +195,7 @@ class ToHtml(Tokenizer): else: aNm = "" - # Process TextType + # Process Text Type if tType == self.T_EMPTY: if parStyle is None: parStyle = "" @@ -214,7 +216,7 @@ class ToHtml(Tokenizer): elif tType == self.T_HEAD1: tHead = tText.replace(r"\\", "
") - tmpResult.append("<%s%s>%s%s\n" % (h1, hStyle, aNm, tHead, h1)) + tmpResult.append("<%s%s%s>%s%s\n" % (h1, h1Cl, hStyle, aNm, tHead, h1)) elif tType == self.T_HEAD2: tHead = tText.replace(r"\\", "
") diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 7870493c..6039121d 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -180,7 +180,7 @@ class Tokenizer(): ## def addRootHeading(self, theHandle): - """Add a heading at the start if a new root folder. + """Add a heading at the start of a new root folder. """ theItem = self.theProject.projTree[theHandle] if theItem is None: @@ -205,7 +205,7 @@ class Tokenizer(): self.theHandle = theHandle self.theItem = self.theProject.projTree[theHandle] if self.theItem is None: - return + return False if theText is not None: # If the text is set, just use that @@ -234,7 +234,7 @@ class Tokenizer(): self.isNote = self.theItem.itemLayout == nwItemLayout.NOTE self.isNovel = self.isBook or self.isUnNum or self.isChap or self.isScene - return + return True def getResult(self): """Return the result from the conversion. @@ -244,6 +244,8 @@ class Tokenizer(): def getResultSize(self): """Return the size of the result from the conversion. """ + if self.theResult is None: + return 0 return len(self.theResult) def getFilteredMarkdown(self): @@ -445,7 +447,7 @@ class Tokenizer(): """ # No special header formatting for notes and no-layout files if self.isNone or self.isNote: - return + return False # For novel files, we need to handle chapter numbering, scene # numbering, and scene breaks @@ -479,8 +481,7 @@ class Tokenizer(): if self.isUnNum: tTemp = self._formatHeading(self.fmtUnNum, tToken[2]) elif tToken[2].startswith("*"): - tTemp = self._formatHeading(self.fmtUnNum, tToken[2]) - tTemp = tTemp[1:].lstrip() + tTemp = self._formatHeading(self.fmtUnNum, tToken[2][1:].lstrip()) else: self.numChapter += 1 tTemp = self._formatHeading(self.fmtChapter, tToken[2]) @@ -663,7 +664,7 @@ class Tokenizer(): self.A_LEFT ) - return + return True ## # Internal Functions diff --git a/nw/core/tools.py b/nw/core/tools.py index ef9fbc6b..342f62bb 100644 --- a/nw/core/tools.py +++ b/nw/core/tools.py @@ -141,6 +141,15 @@ def _numberToWordEN(numVal): tenWord = "" hunWord = "" + if not isinstance(numVal, int): + return "[NaN]" + + if numVal < 0: + return "[Negative]" + + if numVal > 999: + return "[Out of Range]" + if numVal == 0: return "Zero" @@ -166,19 +175,20 @@ def _numberToWordEN(numVal): 5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine", } + retVale = "" hunWord = theHundreds.get(hunVal, "") if tenVal == 10: oneWord = theTeens.get(oneVal, "") - return f"{hunWord} {oneWord}".strip() + retVale = f"{hunWord} {oneWord}".strip() else: oneWord = theOnes.get(oneVal, "") if tenVal == 0: - return f"{hunWord} {oneWord}".strip() + retVale = f"{hunWord} {oneWord}".strip() else: tenWord = theTens.get(tenVal, "") if oneVal == 0: - return f"{hunWord} {tenWord}".strip() + retVale = f"{hunWord} {tenWord}".strip() else: - return f"{hunWord} {tenWord}-{oneWord}".strip() + retVale = f"{hunWord} {tenWord}-{oneWord}".strip() - return "" + return retVale diff --git a/nw/core/tree.py b/nw/core/tree.py index ac3ab45c..43678b73 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -48,8 +48,10 @@ class NWTree(): self._treeOrder = [] # The order of the tree items on the tree view self._treeRoots = [] # The root items of the tree self._trashRoot = None # The handle of the trash root folder + self._archRoot = None # The handle of the archive root folder self._theIndex = 0 # The current iterator index self._treeChanged = False # True if tree structure has changed + self._handleSeed = None # Used for generating handles for testing return @@ -83,14 +85,15 @@ class NWTree(): if tHandle is None: tHandle = self._makeHandle() + if tHandle in self._projTree: + logger.warning("Duplicate handle %s detected, skipping" % tHandle) + return False + logger.verbose("Adding item %s with parent %s" % (str(tHandle), str(pHandle))) nwItem.setHandle(tHandle) nwItem.setParent(pHandle) - self._projTree[tHandle] = nwItem - self._treeOrder.append(tHandle) - if nwItem.itemType == nwItemType.ROOT: logger.verbose("Item %s is a root item" % str(tHandle)) self._treeRoots.append(tHandle) @@ -104,10 +107,13 @@ class NWTree(): self._trashRoot = tHandle else: logger.error("Only one trash folder allowed") + return False + self._projTree[tHandle] = nwItem + self._treeOrder.append(tHandle) self._setTreeChanged(True) - return + return True def packXML(self, xParent): """Pack the content of the tree into the provided XML object. In @@ -176,8 +182,9 @@ class NWTree(): except Exception as e: logger.error(str(e)) + return False - return + return True def sumWords(self): """Loops over all entries and adds up the word counts. @@ -244,6 +251,8 @@ class NWTree(): return True for aRoot in self._treeRoots: tItem = self.__getitem__(aRoot) + if tItem is None: + continue if theClass == tItem.itemClass: return False return True @@ -397,7 +406,7 @@ class NWTree(): del self._projTree[tHandle] else: logger.warning("Failed to delete item %s: item not found" % tHandle) - return False + return if tHandle in self._treeRoots: self._treeRoots.remove(tHandle) @@ -408,7 +417,7 @@ class NWTree(): self._setTreeChanged(True) - return True + return def __contains__(self, tHandle): """Checks if a handle exists in the tree. diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index e74e7ada..f3badad5 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -169,7 +169,7 @@ class GuiDocViewer(QTextBrowser): sPos = self.verticalScrollBar().value() aDoc = ToHtml(self.theProject, self.theParent) - aDoc.setPreview(True, self.mainConf.viewComments, self.mainConf.viewSynopsis) + aDoc.setPreview(self.mainConf.viewComments, self.mainConf.viewSynopsis) aDoc.setLinkHeaders(True) # Be extra careful here to prevent crashes when first opening a diff --git a/nw/guimain.py b/nw/guimain.py index 801de5af..9efc9a2e 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -81,14 +81,14 @@ class GuiMain(QMainWindow): # Core Classes # ============ - # Core Classes and settings + # Core Classes and Settings self.theTheme = GuiTheme(self) self.theProject = NWProject(self) self.theIndex = NWIndex(self.theProject, self) self.hasProject = False self.isFocusMode = False - # Prepare main window + # Prepare Main Window self.resize(*self.mainConf.getWinSize()) self._setWindowTitle() self.setWindowIcon(QIcon(self.mainConf.appIcon)) @@ -982,6 +982,13 @@ class GuiMain(QMainWindow): return + def askQuestion(self, theTitle, theQuestion): + """Ask the user a Yes/No question. + """ + msgBox = QMessageBox() + msgRes = msgBox.question(self, theTitle, theQuestion) + return msgRes == QMessageBox.Yes + def reportConfErr(self): """Checks if the Config module has any errors to report, and let the user know if this is the case. The Config module caches @@ -1073,10 +1080,10 @@ class GuiMain(QMainWindow): self.isFocusMode = not self.isFocusMode if self.isFocusMode: - logger.debug("Activating Focus mode") + logger.debug("Activating Focus Mode") self.tabWidget.setCurrentWidget(self.splitDocs) else: - logger.debug("Deactivating Focus mode") + logger.debug("Deactivating Focus Mode") isVisible = not self.isFocusMode self.treePane.setVisible(isVisible) diff --git a/pytest.ini b/pytest.ini index 0c3af3ba..b61d59ec 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,5 @@ [pytest] markers = - project: Project classes tests error: Test various error handling scenarios core: Core functionality tests gui: Qt5 GUI tests diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..cd1be55b --- /dev/null +++ b/tests/README.md @@ -0,0 +1,74 @@ +# novelWriter Tests + +The test suite uses PyTest for testing. + +## Dependencies + +* `python3-pytest` for the basic framework (required) +* `python3-pytestqt` for Qt support (required) +* `python3-pytest-cov` for code coverage reports (optional) +* `python3-pytest-xvfb` for headless tests (optional) + +## HowTo + +### Basic Usage + +To run all tests, type: +```bash +pytest-3 -v +``` + +The `-v` switch enables verbose mode, with one test per line. +For a more compact view, omit this switch. + +### Headless + +To run tests in headless mode, either use the Qt `offscreen` mode: +```bash +export QT_QPA_PLATFORM=offscreen +``` + +or run with `xvfb`: +```bash +xvfb-run pytest-3 -v +``` + +### Test Coverage + +To add test coverage, run the following: +```bash +pytest-3 -v --cov=nw --cov-report=html +``` + +The `--cov-report` switch generates an html report, omit it to print a coverage summary to the terminal. +The html coverage report will be available in the `htmlcov` folder. + +### Test Markers (Categories) + +To run with specific test markers, add the `-m` switch: +```bash +pytest-3 -v -m core +``` + +Available markers are: + +* '`core`' for unit tests covering the classes in the `nw/core` folder + +## Tests + +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 | NWProject class | nw/core/project.py | `-m core` | `-k testCoreProject` | +| Unit | NWSpell* classes | nw/core/spellcheck.py | `-m core` | `-k testCoreSpell` | +| Unit | NWStatus class | nw/core/status.py | `-m core` | `-k testCoreStatus` | +| Unit | NWTree class | nw/core/tree.py | `-m core` | `-k testCoreTree` | +| Unit | OptionsState class | nw/core/options.py | `-m core` | `-k testCoreOptions` | +| Unit | ToHtml class | nw/core/tohtml.py | `-m core` | `-k testCoreToHtml` | +| Unit | Tokenizer class | nw/core/tokenizer.py | `-m core` | `-k testCoreToken` | diff --git a/tests/conftest.py b/tests/conftest.py index b8cc2fef..0d66d0ea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,7 @@ import pytest import shutil import os -from nwdummy import DummyMain +from dummy import DummyMain from PyQt5.QtWidgets import QMessageBox @@ -20,94 +20,42 @@ from nw.config import Config # noqa: E402 ## @pytest.fixture(scope="session") -def nwTemp(): +def tmpDir(): """A temporary folder for the test session. This folder is presistent after the test so that the status of generated files can be checked. The folder is instead cleared before a new test session. """ testDir = os.path.dirname(__file__) - tempDir = os.path.join(testDir, "temp") - if os.path.isdir(tempDir): - shutil.rmtree(tempDir) - if not os.path.isdir(tempDir): - os.mkdir(tempDir) - return tempDir + theDir = os.path.join(testDir, "temp") + if os.path.isdir(theDir): + shutil.rmtree(theDir) + if not os.path.isdir(theDir): + os.mkdir(theDir) + return theDir @pytest.fixture(scope="session") -def nwRef(): +def refDir(): """The folder where all the reference files are stored for verifying the results of tests. """ testDir = os.path.dirname(__file__) - refDir = os.path.join(testDir, "reference") - return refDir - -## -# novelWriter Objects -## + theDir = os.path.join(testDir, "reference") + return theDir @pytest.fixture(scope="session") -def tmpConf(nwTemp): - """Create a temporary novelWriter configuration object. +def outDir(tmpDir): + """An output folder for test results """ - theConf = Config() - theConf.initConfig(nwTemp, nwTemp) - theConf.setLastPath("") - return theConf - -@pytest.fixture(scope="session") -def nwConf(nwRef, nwTemp): - """Temporary novelWriter configuration used for the dummy instance - of novelWriter's main GUI. - """ - theConf = Config() - theConf.initConfig(nwRef, nwTemp) - return theConf - -@pytest.fixture(scope="session") -def nwDummy(nwRef, nwTemp, nwConf): - """Create a dummy instance of novelWriter's main GUI class. - """ - theDummy = DummyMain() - theDummy.mainConf = nwConf - return theDummy - -## -# Temporary Test Folders -## - -@pytest.fixture(scope="session") -def nwTempProj(nwTemp): - """A temporary folder for project tests. - """ - projDir = os.path.join(nwTemp, "proj") - if not os.path.isdir(projDir): - os.mkdir(projDir) - return projDir - -@pytest.fixture(scope="session") -def nwTempGUI(nwTemp): - """A temporary folder for GUI tests. - """ - guiDir = os.path.join(nwTemp, "gui") - if not os.path.isdir(guiDir): - os.mkdir(guiDir) - return guiDir - -@pytest.fixture(scope="session") -def nwTempBuild(nwTemp): - """A temporary folder for build tests. - """ - buildDir = os.path.join(nwTemp, "build") - if not os.path.isdir(buildDir): - os.mkdir(buildDir) - return buildDir + theDir = os.path.join(tmpDir, "results") + if not os.path.isdir(theDir): + os.mkdir(theDir) + return theDir @pytest.fixture(scope="function") -def nwFuncTemp(nwTemp): +def fncDir(tmpDir): """A temporary folder for a single test function. """ - funcDir = os.path.join(nwTemp, "ftemp") + funcDir = os.path.join(tmpDir, "ftemp") if os.path.isdir(funcDir): shutil.rmtree(funcDir) if not os.path.isdir(funcDir): @@ -118,16 +66,37 @@ def nwFuncTemp(nwTemp): return ## -# Temp Folders for Projects +# novelWriter Objects ## @pytest.fixture(scope="function") -def nwMinimal(nwTemp): +def tmpConf(tmpDir): + """Create a temporary novelWriter configuration object. + """ + theConf = Config() + theConf.initConfig(tmpDir, tmpDir) + theConf.setLastPath("") + return theConf + +@pytest.fixture(scope="function") +def dummyGUI(tmpConf): + """Create a dummy instance of novelWriter's main GUI class. + """ + theDummy = DummyMain() + theDummy.mainConf = tmpConf + return theDummy + +## +# Temp Project Folders +## + +@pytest.fixture(scope="function") +def nwMinimal(tmpDir): """A minimal novelWriter example project. """ testDir = os.path.dirname(__file__) minimalStore = os.path.join(testDir, "minimal") - minimalDir = os.path.join(nwTemp, "minimal") + minimalDir = os.path.join(tmpDir, "minimal") if os.path.isdir(minimalDir): shutil.rmtree(minimalDir) shutil.copytree(minimalStore, minimalDir) @@ -143,13 +112,13 @@ def nwMinimal(nwTemp): return @pytest.fixture(scope="function") -def nwLipsum(nwTemp): +def nwLipsum(tmpDir): """A medium sized novelWriter example project with a lot of Lorem Ipsum dummy text. """ testDir = os.path.dirname(__file__) lipsumStore = os.path.join(testDir, "lipsum") - lipsumDir = os.path.join(nwTemp, "lipsum") + lipsumDir = os.path.join(tmpDir, "lipsum") if os.path.isdir(lipsumDir): shutil.rmtree(lipsumDir) shutil.copytree(lipsumStore, lipsumDir) @@ -165,12 +134,12 @@ def nwLipsum(nwTemp): return @pytest.fixture(scope="function") -def nwOldProj(nwTemp): +def nwOldProj(tmpDir): """A minimal movelWriter project using the old folder structure. """ testDir = os.path.dirname(__file__) oldProjStore = os.path.join(testDir, "oldproj") - oldProjDir = os.path.join(nwTemp, "oldproj") + oldProjDir = os.path.join(tmpDir, "oldproj") if os.path.isdir(oldProjDir): shutil.rmtree(oldProjDir) shutil.copytree(oldProjStore, oldProjDir) @@ -199,4 +168,30 @@ def yesToAll(monkeypatch): monkeypatch.setattr( QMessageBox, "critical", lambda *args, **kwargs: QMessageBox.Yes ) + yield + monkeypatch.undo() return + +# =============================================================================================== # + +## +# Temporary Test Folders +## + +@pytest.fixture(scope="session") +def nwTempGUI(tmpDir): + """A temporary folder for GUI tests. + """ + guiDir = os.path.join(tmpDir, "gui") + if not os.path.isdir(guiDir): + os.mkdir(guiDir) + return guiDir + +@pytest.fixture(scope="session") +def nwTempBuild(tmpDir): + """A temporary folder for build tests. + """ + buildDir = os.path.join(tmpDir, "build") + if not os.path.isdir(buildDir): + os.mkdir(buildDir) + return buildDir diff --git a/tests/dummy.py b/tests/dummy.py new file mode 100644 index 00000000..0f26e0ff --- /dev/null +++ b/tests/dummy.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +"""novelWriter Test Dummy GUI Classes +""" + +# =========================================================================== # +# Mock GUI +# =========================================================================== # + +class DummyMain(): + + def __init__(self): + self.mainConf = None + self.hasProject = True + self.theIndex = None + self.theProject = None + self.statusBar = StatusBar() + + # Test Variables + self.askResponse = True + self.lastAlert = "" + + return + + def makeAlert(self, theMessage, theLevel): + print("%s: %s" % (str(theLevel), theMessage)) + self.lastAlert = str(theMessage) + return + + def askQuestion(self, theTitle, theQustion): + print("Question: %s" % theQustion) + return self.askResponse + + def setStatus(self, theMessage): + return + + def setProjectStatus(self, isChanged): + return + + def openProject(self, projPath): + return + + def rebuildIndex(self): + return + + # Test Functions + + def undo(self): + self.askResponse = True + return + + def clear(self): + self.lastAlert = "" + return + +# END Class GuiMain + +class StatusBar(): + + def __init__(self): + return + + def setStatus(self, theText): + return + +# END Class StatusBar + +# =========================================================================== # +# Error Functions +# Dummy functions that will raise errors instead. +# =========================================================================== # + +def causeOSError(*args, **kwargs): + raise OSError diff --git a/tests/lipsum/ToC.json b/tests/lipsum/ToC.json deleted file mode 100644 index 4d22a21c..00000000 --- a/tests/lipsum/ToC.json +++ /dev/null @@ -1,77 +0,0 @@ -[ - [ - "content/04468803b92e1.nwd", - "WORLD", - "Ancient Europe" - ], - [ - "content/2426c6f0ca922.nwd", - "PLOT", - "Main" - ], - [ - "content/441420a886d82.nwd", - "NOVEL", - "Chapter Two" - ], - [ - "content/47666c91c7ccf.nwd", - "NOVEL", - "Scene Five" - ], - [ - "content/4c4f28287af27.nwd", - "CHARACTER", - "Mr. Nobody" - ], - [ - "content/7a992350f3eb6.nwd", - "NOVEL", - "Lorem Ipsum" - ], - [ - "content/846352075de7d.nwd", - "NOVEL", - "Interlude" - ], - [ - "content/88243afbe5ed8.nwd", - "NOVEL", - "Scene One" - ], - [ - "content/88d59a277361b.nwd", - "NOVEL", - "Prologue" - ], - [ - "content/8c58a65414c23.nwd", - "NOVEL", - "Front Matter" - ], - [ - "content/db7e733775d4d.nwd", - "NOVEL", - "Act One" - ], - [ - "content/eb103bc70c90c.nwd", - "NOVEL", - "Scene Three" - ], - [ - "content/f8c0562e50f1b.nwd", - "NOVEL", - "Scene Four" - ], - [ - "content/f96ec11c6a3da.nwd", - "NOVEL", - "Scene Two" - ], - [ - "content/fb609cd8319dc.nwd", - "NOVEL", - "Chapter One" - ] -] \ No newline at end of file diff --git a/tests/minimal/ToC.json b/tests/minimal/ToC.json deleted file mode 100644 index 5881c392..00000000 --- a/tests/minimal/ToC.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - [ - "content/8c659a11cd429.nwd", - "NOVEL", - "New Scene" - ], - [ - "content/a35baf2e93843.nwd", - "NOVEL", - "Title Page" - ], - [ - "content/f5ab3e30151e1.nwd", - "NOVEL", - "New Chapter" - ] -] \ No newline at end of file diff --git a/tests/nwdummy.py b/tests/nwdummy.py deleted file mode 100644 index 4b5302b7..00000000 --- a/tests/nwdummy.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter Test Dummy GUI Classes -""" - -class DummyMain(): - - def __init__(self): - self.mainConf = None - self.hasProject = True - self.statusBar = StatusBar() - return - - def makeAlert(self, theMessage, theLevel): - print("%s: %s" % (str(theLevel), theMessage)) - return - - def setStatus(self, theMessage): - return - - def setProjectStatus(self, isChanged): - return - - def openProject(self, projPath): - return - - def rebuildIndex(self): - return - -# END Class GuiMain - -class StatusBar(): - - def __init__(self): - return - - def setStatus(self, theText): - return - -# END Class StatusBar diff --git a/tests/reference/proj/1_tagsIndex.json b/tests/reference/coreIndex_LoadSave_tagsIndex.json similarity index 100% rename from tests/reference/proj/1_tagsIndex.json rename to tests/reference/coreIndex_LoadSave_tagsIndex.json diff --git a/tests/reference/proj/1_nwProject.nwx b/tests/reference/coreProject_1_nwProject.nwx similarity index 85% rename from tests/reference/proj/1_nwProject.nwx rename to tests/reference/coreProject_1_nwProject.nwx index 61ece12f..d8b774bb 100644 --- a/tests/reference/proj/1_nwProject.nwx +++ b/tests/reference/coreProject_1_nwProject.nwx @@ -39,35 +39,35 @@ - + Novel ROOT NOVEL New False - + Plot ROOT PLOT New False - + Characters ROOT CHARACTER New False - + World ROOT WORLD New False - + Title Page FILE NOVEL @@ -79,14 +79,14 @@ 0 0 - + New Chapter FOLDER NOVEL New False - + New Chapter FILE NOVEL @@ -98,7 +98,7 @@ 0 0 - + New Scene FILE NOVEL diff --git a/tests/reference/proj/4_nwProject.nwx b/tests/reference/coreProject_2_nwProject.nwx similarity index 81% rename from tests/reference/proj/4_nwProject.nwx rename to tests/reference/coreProject_2_nwProject.nwx index 4c9c30eb..fc7e7ab2 100644 --- a/tests/reference/proj/4_nwProject.nwx +++ b/tests/reference/coreProject_2_nwProject.nwx @@ -41,56 +41,56 @@ - + Novel ROOT NOVEL New False - + Plot ROOT PLOT New False - + Characters ROOT CHARACTER New False - + Locations ROOT WORLD New False - + Timeline ROOT TIMELINE New False - + Objects ROOT OBJECT New False - + Entity ROOT ENTITY New False - + Title Page FILE NOVEL @@ -102,14 +102,14 @@ 0 0 - + Chapter 1 FOLDER NOVEL New False - + Chapter 1 FILE NOVEL @@ -121,7 +121,7 @@ 0 0 - + Scene 1.1 FILE NOVEL @@ -133,7 +133,7 @@ 0 0 - + Scene 1.2 FILE NOVEL @@ -145,7 +145,7 @@ 0 0 - + Scene 1.3 FILE NOVEL @@ -157,14 +157,14 @@ 0 0 - + Chapter 2 FOLDER NOVEL New False - + Chapter 2 FILE NOVEL @@ -176,7 +176,7 @@ 0 0 - + Scene 2.1 FILE NOVEL @@ -188,7 +188,7 @@ 0 0 - + Scene 2.2 FILE NOVEL @@ -200,7 +200,7 @@ 0 0 - + Scene 2.3 FILE NOVEL @@ -212,14 +212,14 @@ 0 0 - + Chapter 3 FOLDER NOVEL New False - + Chapter 3 FILE NOVEL @@ -231,7 +231,7 @@ 0 0 - + Scene 3.1 FILE NOVEL @@ -243,7 +243,7 @@ 0 0 - + Scene 3.2 FILE NOVEL @@ -255,7 +255,7 @@ 0 0 - + Scene 3.3 FILE NOVEL diff --git a/tests/reference/proj/5_nwProject.nwx b/tests/reference/coreProject_3_nwProject.nwx similarity index 83% rename from tests/reference/proj/5_nwProject.nwx rename to tests/reference/coreProject_3_nwProject.nwx index 9935736e..11d007aa 100644 --- a/tests/reference/proj/5_nwProject.nwx +++ b/tests/reference/coreProject_3_nwProject.nwx @@ -41,56 +41,56 @@ - + Novel ROOT NOVEL New False - + Plot ROOT PLOT New False - + Characters ROOT CHARACTER New False - + Locations ROOT WORLD New False - + Timeline ROOT TIMELINE New False - + Objects ROOT OBJECT New False - + Entity ROOT ENTITY New False - + Title Page FILE NOVEL @@ -102,7 +102,7 @@ 0 0 - + Scene 1 FILE NOVEL @@ -114,7 +114,7 @@ 0 0 - + Scene 2 FILE NOVEL @@ -126,7 +126,7 @@ 0 0 - + Scene 3 FILE NOVEL @@ -138,7 +138,7 @@ 0 0 - + Scene 4 FILE NOVEL @@ -150,7 +150,7 @@ 0 0 - + Scene 5 FILE NOVEL @@ -162,7 +162,7 @@ 0 0 - + Scene 6 FILE NOVEL diff --git a/tests/reference/proj/2_nwProject.nwx b/tests/reference/coreProject_4_nwProject.nwx similarity index 82% rename from tests/reference/proj/2_nwProject.nwx rename to tests/reference/coreProject_4_nwProject.nwx index cc7b54ea..9ea0617f 100644 --- a/tests/reference/proj/2_nwProject.nwx +++ b/tests/reference/coreProject_4_nwProject.nwx @@ -39,35 +39,35 @@ - + Novel ROOT NOVEL New False - + Plot ROOT PLOT New False - + Characters ROOT CHARACTER New False - + World ROOT WORLD New False - + Title Page FILE NOVEL @@ -79,14 +79,14 @@ 0 0 - + New Chapter FOLDER NOVEL New False - + New Chapter FILE NOVEL @@ -98,7 +98,7 @@ 0 0 - + New Scene FILE NOVEL @@ -110,28 +110,28 @@ 0 0 - + Timeline ROOT TIMELINE New False - + Object ROOT OBJECT New False - + Custom1 ROOT CUSTOM New False - + Custom2 ROOT CUSTOM diff --git a/tests/reference/proj/3_nwProject.nwx b/tests/reference/coreProject_5_nwProject.nwx similarity index 84% rename from tests/reference/proj/3_nwProject.nwx rename to tests/reference/coreProject_5_nwProject.nwx index c9e450d6..66488434 100644 --- a/tests/reference/proj/3_nwProject.nwx +++ b/tests/reference/coreProject_5_nwProject.nwx @@ -39,35 +39,35 @@ - + Novel ROOT NOVEL New False - + Plot ROOT PLOT New False - + Characters ROOT CHARACTER New False - + World ROOT WORLD New False - + Title Page FILE NOVEL @@ -79,14 +79,14 @@ 0 0 - + New Chapter FOLDER NOVEL New False - + New Chapter FILE NOVEL @@ -98,7 +98,7 @@ 0 0 - + New Scene FILE NOVEL @@ -110,7 +110,7 @@ 0 0 - + Hello FILE NOVEL @@ -122,7 +122,7 @@ 0 0 - + Jane FILE CHARACTER diff --git a/tests/test_common.py b/tests/test_common.py index 2a24f31f..135627fc 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -9,7 +9,7 @@ from nw.common import ( checkString, checkBool, checkInt, colRange, formatInt, transferCase, fuzzyTime, checkHandle, formatTimeStamp, formatTime ) -from nwtools import cmpList +from tools import cmpList @pytest.mark.core def testCheckString(): diff --git a/tests/test_config.py b/tests/test_config.py index 5d72cdfd..94ff5d1c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,14 +5,14 @@ import pytest import os -from nwtools import cmpFiles +from tools import cmpFiles @pytest.mark.core -def testConfigCore(tmpConf, nwTemp, nwRef): - refConf = os.path.join(nwRef, "novelwriter.conf") +def testConfigCore(tmpConf, tmpDir, refDir): + refConf = os.path.join(refDir, "novelwriter.conf") testConf = os.path.join(tmpConf.confPath, "novelwriter.conf") - assert tmpConf.confPath == nwTemp + assert tmpConf.confPath == tmpDir assert tmpConf.saveConfig() assert cmpFiles(testConf, refConf, [2, 9]) assert not tmpConf.confChanged @@ -21,29 +21,29 @@ def testConfigCore(tmpConf, nwTemp, nwRef): assert not tmpConf.confChanged @pytest.mark.core -def testConfigSetConfPath(tmpConf, nwTemp): +def testConfigSetConfPath(tmpConf, tmpDir): assert tmpConf.setConfPath(None) assert not tmpConf.setConfPath(os.path.join("somewhere", "over", "the", "rainbow")) - assert tmpConf.setConfPath(os.path.join(nwTemp, "novelwriter.conf")) - assert tmpConf.confPath == nwTemp + assert tmpConf.setConfPath(os.path.join(tmpDir, "novelwriter.conf")) + assert tmpConf.confPath == tmpDir assert tmpConf.confFile == "novelwriter.conf" assert not tmpConf.confChanged @pytest.mark.core -def testConfigSetDataPath(tmpConf, nwTemp): +def testConfigSetDataPath(tmpConf, tmpDir): assert tmpConf.setDataPath(None) assert not tmpConf.setDataPath(os.path.join("somewhere", "over", "the", "rainbow")) - assert tmpConf.setDataPath(nwTemp) - assert tmpConf.dataPath == nwTemp + assert tmpConf.setDataPath(tmpDir) + assert tmpConf.dataPath == tmpDir assert not tmpConf.confChanged @pytest.mark.core -def testConfigSetWinSize(tmpConf, nwTemp, nwRef): - refConf = os.path.join(nwRef, "novelwriter.conf") +def testConfigSetWinSize(tmpConf, tmpDir, refDir): + refConf = os.path.join(refDir, "novelwriter.conf") testConf = os.path.join(tmpConf.confPath, "novelwriter.conf") tmpConf.guiScale = 1.0 - assert tmpConf.confPath == nwTemp + assert tmpConf.confPath == tmpDir assert tmpConf.setWinSize(1205, 655) assert not tmpConf.confChanged assert tmpConf.setWinSize(70, 70) @@ -55,11 +55,11 @@ def testConfigSetWinSize(tmpConf, nwTemp, nwRef): assert not tmpConf.confChanged @pytest.mark.core -def testConfigSetTreeColWidths(tmpConf, nwTemp, nwRef): - refConf = os.path.join(nwRef, "novelwriter.conf") +def testConfigSetTreeColWidths(tmpConf, tmpDir, refDir): + refConf = os.path.join(refDir, "novelwriter.conf") testConf = os.path.join(tmpConf.confPath, "novelwriter.conf") - assert tmpConf.confPath == nwTemp + assert tmpConf.confPath == tmpDir tmpConf.guiScale = 1.0 assert tmpConf.setTreeColWidths([10, 20, 25]) @@ -77,11 +77,11 @@ def testConfigSetTreeColWidths(tmpConf, nwTemp, nwRef): assert not tmpConf.confChanged @pytest.mark.core -def testConfigSetPanePos(tmpConf, nwTemp, nwRef): - refConf = os.path.join(nwRef, "novelwriter.conf") +def testConfigSetPanePos(tmpConf, tmpDir, refDir): + refConf = os.path.join(refDir, "novelwriter.conf") testConf = os.path.join(tmpConf.confPath, "novelwriter.conf") - assert tmpConf.confPath == nwTemp + assert tmpConf.confPath == tmpDir tmpConf.guiScale = 2.0 assert tmpConf.setMainPanePos([200, 700]) @@ -113,11 +113,11 @@ def testConfigSetPanePos(tmpConf, nwTemp, nwRef): assert not tmpConf.confChanged @pytest.mark.core -def testConfigFlags(tmpConf, nwTemp, nwRef): - refConf = os.path.join(nwRef, "novelwriter.conf") +def testConfigFlags(tmpConf, tmpDir, refDir): + refConf = os.path.join(refDir, "novelwriter.conf") testConf = os.path.join(tmpConf.confPath, "novelwriter.conf") - assert tmpConf.confPath == nwTemp + assert tmpConf.confPath == tmpDir assert not tmpConf.setShowRefPanel(False) assert tmpConf.setShowRefPanel(True) @@ -137,8 +137,8 @@ def testConfigFlags(tmpConf, nwTemp, nwRef): assert not tmpConf.confChanged @pytest.mark.core -def testTextSizes(tmpConf, nwTemp, nwRef): - assert tmpConf.confPath == nwTemp +def testTextSizes(tmpConf, tmpDir, refDir): + assert tmpConf.confPath == tmpDir tmpConf.guiScale = 2.0 assert tmpConf.getTextWidth() == 1200 diff --git a/tests/test_core_document.py b/tests/test_core_document.py new file mode 100644 index 00000000..9e16c66e --- /dev/null +++ b/tests/test_core_document.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +"""novelWriter NWDoc Class Tester +""" + +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 + +@pytest.mark.core +def testCoreDocument_LoadSave(monkeypatch, dummyGUI, nwMinimal): + """Test loading and saving a document with the NWDoc class. + """ + theProject = NWProject(dummyGUI) + assert theProject.openProject(nwMinimal) + assert theProject.projPath == nwMinimal + + theDoc = NWDoc(theProject, dummyGUI) + sHandle = "8c659a11cd429" + + # Not a valid handle + assert theDoc.openDocument("dummy") is None + + # Non-existent handle + assert theDoc.openDocument("0000000000000") is None + + # Cause open() to fail while loading + def dummyOpen(*args, **kwargs): + raise OSError + + monkeypatch.setattr("builtins.open", dummyOpen) + assert theDoc.openDocument(sHandle) is None + monkeypatch.undo() + + # Load the text + assert theDoc.openDocument(sHandle) == "### New Scene\n\n" + + # Try to open a new (non-existent) file + nHandle = theProject.projTree.findRoot(nwItemClass.NOVEL) + assert nHandle is not None + xHandle = theProject.newFile("New File", nwItemClass.NOVEL, nHandle) + assert theDoc.openDocument(xHandle) == "" + + # Check cached item + assert isinstance(theDoc._theItem, NWItem) + assert theDoc.openDocument(xHandle, isOrphan=True) == "" + assert theDoc._theItem is None + + # Set handle and save again + theText = "### Test File\n\nText ...\n\n" + assert theDoc.openDocument(xHandle) == "" + assert theDoc.saveDocument(theText) + + # Save again to ensure temp file and previous file is handled + assert theDoc.saveDocument(theText) + + # Check file content + docPath = os.path.join(nwMinimal, "content", xHandle+".nwd") + with open(docPath, mode="r", encoding="utf8") as inFile: + assert inFile.read() == ( + "%%~name: New File\n" + f"%%~path: a508bb932959c/{xHandle}\n" + "%%~kind: NOVEL/SCENE\n" + "### Test File\n\n" + "Text ...\n\n" + ) + + # Force no meta data + theDoc._theItem = None + assert theDoc.saveDocument(theText) + + with open(docPath, mode="r", encoding="utf8") as inFile: + assert inFile.read() == theText + + # Cause open() to fail while saving + monkeypatch.setattr("builtins.open", causeOSError) + assert not theDoc.saveDocument(theText) + monkeypatch.undo() + + # Saving with no handle + theDoc.clearDocument() + assert not theDoc.saveDocument(theText) + + # Delete the last document + assert not theDoc.deleteDocument("dummy") + assert os.path.isfile(docPath) + + # Cause the delete to fail + monkeypatch.setattr("os.unlink", causeOSError) + assert not theDoc.deleteDocument(xHandle) + monkeypatch.undo() + + # Make the delete pass + assert theDoc.deleteDocument(xHandle) + assert not os.path.isfile(docPath) + +# END Test testCoreDocument_Load + +@pytest.mark.core +def testCoreDocument_Methods(monkeypatch, dummyGUI, nwMinimal): + """Test other methods of the NWDoc class. + """ + theProject = NWProject(dummyGUI) + assert theProject.openProject(nwMinimal) + assert theProject.projPath == nwMinimal + + theDoc = NWDoc(theProject, dummyGUI) + sHandle = "8c659a11cd429" + docPath = os.path.join(nwMinimal, "content", sHandle+".nwd") + + assert theDoc.openDocument(sHandle) == "### New Scene\n\n" + + # Check location + assert theDoc.getFileLocation() == docPath + + # Check the item + assert theDoc.getCurrentItem() is not None + assert theDoc.getCurrentItem().itemHandle == sHandle + + # Check the meta + theName, theParent, theClass, theLayout = theDoc.getMeta() + assert theName == "New Scene" + assert theParent == "a6d311a93600a" + assert theClass == nwItemClass.NOVEL + assert theLayout == nwItemLayout.SCENE + + # Add meta data garbage + assert theDoc.saveDocument("%%~ stuff\n### Test File\n\nText ...\n\n") + with open(docPath, mode="r", encoding="utf8") as inFile: + assert inFile.read() == ( + "%%~name: New Scene\n" + f"%%~path: a6d311a93600a/{sHandle}\n" + "%%~kind: NOVEL/SCENE\n" + "%%~ stuff\n" + "### Test File\n\n" + "Text ...\n\n" + ) + + assert theDoc.openDocument(sHandle) == "### Test File\n\nText ...\n\n" + +# END Test testCoreDocument_Methods diff --git a/tests/test_index.py b/tests/test_core_index.py similarity index 85% rename from tests/test_index.py rename to tests/test_core_index.py index 9fabae5b..ad87c7f3 100644 --- a/tests/test_index.py +++ b/tests/test_core_index.py @@ -8,28 +8,28 @@ import json from shutil import copyfile -from nwtools import cmpFiles +from tools import cmpFiles from nw.core.project import NWProject from nw.core.index import NWIndex from nw.constants import nwItemClass, nwItemLayout -@pytest.mark.project -def testIndexBuildCheck(monkeypatch, nwLipsum, nwDummy, nwTempProj, nwRef): +@pytest.mark.core +def testCoreIndex_LoadSave(monkeypatch, nwLipsum, dummyGUI, outDir, refDir): """Test core functionality of scaning, saving, loading and checking the index cache file. """ projFile = os.path.join(nwLipsum, "meta", "tagsIndex.json") - testFile = os.path.join(nwTempProj, "1_tagsIndex.json") - refFile = os.path.join(nwRef, "proj", "1_tagsIndex.json") + testFile = os.path.join(outDir, "coreIndex_LoadSave_tagsIndex.json") + compFile = os.path.join(refDir, "coreIndex_LoadSave_tagsIndex.json") - theProject = NWProject(nwDummy) + theProject = NWProject(dummyGUI) theProject.projTree.setSeed(42) assert theProject.openProject(nwLipsum) monkeypatch.setattr("nw.core.index.time", lambda: 123.4) - theIndex = NWIndex(theProject, nwDummy) + theIndex = NWIndex(theProject, dummyGUI) notIndexable = { "b3643d0f92e32": False, # Novel ROOT "45e6b01ca35c1": False, # Chapter One FOLDER @@ -136,17 +136,19 @@ def testIndexBuildCheck(monkeypatch, nwLipsum, nwDummy, nwTempProj, nwRef): assert theProject.closeProject() copyfile(projFile, testFile) - assert cmpFiles(testFile, refFile) + assert cmpFiles(testFile, compFile) -@pytest.mark.project -def testIndexScanThis(nwMinimal, nwDummy): +# END Test testCoreIndex_LoadSave + +@pytest.mark.core +def testCoreIndex_ScanThis(nwMinimal, dummyGUI): """Test the tag scanner function scanThis. """ - theProject = NWProject(nwDummy) + theProject = NWProject(dummyGUI) theProject.projTree.setSeed(42) assert theProject.openProject(nwMinimal) - theIndex = NWIndex(theProject, nwDummy) + theIndex = NWIndex(theProject, dummyGUI) isValid, theBits, thePos = theIndex.scanThis("tag: this, and this") assert not isValid @@ -162,40 +164,42 @@ def testIndexScanThis(nwMinimal, nwDummy): isValid, theBits, thePos = theIndex.scanThis("@a:") assert isValid - assert str(theBits) == "['@a']" - assert str(thePos) == "[0]" + assert theBits == ["@a"] + assert thePos == [0] isValid, theBits, thePos = theIndex.scanThis("@a:b") assert isValid - assert str(theBits) == "['@a', 'b']" - assert str(thePos) == "[0, 3]" + assert theBits == ["@a", "b"] + assert thePos == [0, 3] isValid, theBits, thePos = theIndex.scanThis("@a:b,c,d") assert isValid - assert str(theBits) == "['@a', 'b', 'c', 'd']" - assert str(thePos) == "[0, 3, 5, 7]" + assert theBits == ["@a", "b", "c", "d"] + assert thePos == [0, 3, 5, 7] isValid, theBits, thePos = theIndex.scanThis("@a : b , c , d") assert isValid - assert str(theBits) == "['@a', 'b', 'c', 'd']" - assert str(thePos) == "[0, 5, 9, 13]" + assert theBits == ["@a", "b", "c", "d"] + assert thePos == [0, 5, 9, 13] isValid, theBits, thePos = theIndex.scanThis("@tag: this, and this") assert isValid - assert str(theBits) == "['@tag', 'this', 'and this']" - assert str(thePos) == "[0, 6, 12]" + assert theBits == ["@tag", "this", "and this"] + assert thePos == [0, 6, 12] assert theProject.closeProject() -@pytest.mark.project -def testIndexCheckThese(nwMinimal, nwDummy): +# END Test testCoreIndex_ScanThis + +@pytest.mark.core +def testCoreIndex_CheckThese(nwMinimal, dummyGUI): """Test the tag checker function checkThese. """ - theProject = NWProject(nwDummy) + theProject = NWProject(dummyGUI) theProject.projTree.setSeed(42) assert theProject.openProject(nwMinimal) - theIndex = NWIndex(theProject, nwDummy) + theIndex = NWIndex(theProject, dummyGUI) nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") nItem = theProject.projTree[nHandle] @@ -209,29 +213,32 @@ def testIndexCheckThese(nwMinimal, nwDummy): "# Hello World!\n" "@pov: Jane" )) - assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle + assert theIndex.tagIndex == {"Jane": [2, cHandle, "CHARACTER", "T000001"]} assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!" - assert str(theIndex.checkThese(["@tag", "Jane"], cItem)) == "[True, True]" - assert str(theIndex.checkThese(["@tag", "John"], cItem)) == "[True, True]" - assert str(theIndex.checkThese(["@tag", "Jane"], nItem)) == "[True, False]" - assert str(theIndex.checkThese(["@tag", "John"], nItem)) == "[True, True]" - assert str(theIndex.checkThese(["@pov", "John"], nItem)) == "[True, False]" - assert str(theIndex.checkThese(["@pov", "Jane"], nItem)) == "[True, True]" - assert str(theIndex.checkThese(["@ pov", "Jane"], nItem)) == "[False, False]" - assert str(theIndex.checkThese(["@what", "Jane"], nItem)) == "[False, False]" + assert theIndex.checkThese([], cItem) == [] + assert theIndex.checkThese(["@tag", "Jane"], cItem) == [True, True] + assert theIndex.checkThese(["@tag", "John"], cItem) == [True, True] + assert theIndex.checkThese(["@tag", "Jane"], nItem) == [True, False] + assert theIndex.checkThese(["@tag", "John"], nItem) == [True, True] + assert theIndex.checkThese(["@pov", "John"], nItem) == [True, False] + assert theIndex.checkThese(["@pov", "Jane"], nItem) == [True, True] + assert theIndex.checkThese(["@ pov", "Jane"], nItem) == [False, False] + assert theIndex.checkThese(["@what", "Jane"], nItem) == [False, False] assert theProject.closeProject() -@pytest.mark.project -def testIndexScanText(nwMinimal, nwDummy): - """Check the index data extraction functions. +# END Test testCoreIndex_CheckThese + +@pytest.mark.core +def testCoreIndex_ScanText(nwMinimal, dummyGUI): + """Check the index text scanner. """ - theProject = NWProject(nwDummy) + theProject = NWProject(dummyGUI) theProject.projTree.setSeed(42) assert theProject.openProject(nwMinimal) - theIndex = NWIndex(theProject, nwDummy) + theIndex = NWIndex(theProject, dummyGUI) # Some items for fail to scan tests dHandle = theProject.newFolder("Folder", nwItemClass.NOVEL, "a508bb932959c") @@ -392,21 +399,23 @@ def testIndexScanText(nwMinimal, nwDummy): "% synopsis: Synopsis One.\n\n" "Paragraph One.\n\n" )) - assert str(theIndex.refIndex[sHandle]["T000001"]["tags"]) == ( - "[[3, '@pov', 'One'], [5, '@char', 'Two']]" + assert theIndex.refIndex[sHandle]["T000001"]["tags"] == ( + [[3, "@pov", "One"], [5, "@char", "Two"]] ) assert theProject.closeProject() -@pytest.mark.project -def testIndexExtractData(nwMinimal, nwDummy): +# END Test testCoreIndex_ScanText + +@pytest.mark.core +def testCoreIndex_ExtractData(nwMinimal, dummyGUI): """Check the index data extraction functions. """ - theProject = NWProject(nwDummy) + theProject = NWProject(dummyGUI) theProject.projTree.setSeed(42) assert theProject.openProject(nwMinimal) - theIndex = NWIndex(theProject, nwDummy) + theIndex = NWIndex(theProject, dummyGUI) nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") @@ -424,7 +433,13 @@ def testIndexExtractData(nwMinimal, nwDummy): )) # The novel structure should contain the pointer to the novel file header - assert str(theIndex.getNovelStructure()) == "['%s:T000001']" % nHandle + assert theIndex.getNovelStructure() == ["%s:T000001" % nHandle] + + # Check that excluded files can be skipped + theProject.projTree[nHandle].setExported(False) + assert theIndex.getNovelStructure(skipExcluded=False) == ["%s:T000001" % nHandle] + assert theIndex.getNovelStructure(skipExcluded=True) == [] + assert theIndex.getNovelStructure() == [] # The novel file should have the correct counts cC, wC, pC = theIndex.getCounts(nHandle) @@ -443,8 +458,8 @@ def testIndexExtractData(nwMinimal, nwDummy): # The novel file should now refer to Jane as @pov and @char theRefs = theIndex.getReferences(nHandle) - assert str(theRefs["@pov"]) == "['Jane']" - assert str(theRefs["@char"]) == "['Jane']" + assert theRefs["@pov"] == ["Jane"] + assert theRefs["@char"] == ["Jane"] ## # getBackReferenceList @@ -455,7 +470,7 @@ def testIndexExtractData(nwMinimal, nwDummy): # The character file should have a record of the reference from the novel file theRefs = theIndex.getBackReferenceList(cHandle) - assert str(theRefs) == "{'%s': 'T000001'}" % nHandle + assert theRefs == {nHandle: "T000001"} ## # getTagSource @@ -535,3 +550,5 @@ def testIndexExtractData(nwMinimal, nwDummy): assert pC == 2 assert theProject.closeProject() + +# END Test testCoreIndex_ExtractData diff --git a/tests/test_item.py b/tests/test_core_item.py similarity index 65% rename from tests/test_item.py rename to tests/test_core_item.py index 95aa8470..d592f7c0 100644 --- a/tests/test_item.py +++ b/tests/test_core_item.py @@ -6,13 +6,15 @@ import pytest from lxml import etree -from nw.core.project import NWProject, NWItem +from nw.core import NWProject +from nw.core.item import NWItem from nw.constants import nwItemClass, nwItemType, nwItemLayout -@pytest.mark.project -def testItemSettersSimple(nwDummy): - - theProject = NWProject(nwDummy) +@pytest.mark.core +def testCoreItem_Setters(dummyGUI): + """Test all the simple setters for the NWItem class. + """ + theProject = NWProject(dummyGUI) theItem = NWItem(theProject) # Name @@ -20,12 +22,16 @@ def testItemSettersSimple(nwDummy): assert theItem.itemName == "A Name" theItem.setName("\t A Name ") assert theItem.itemName == "A Name" + theItem.setName(123) + assert theItem.itemName == "" # Handle theItem.setHandle(123) assert theItem.itemHandle is None theItem.setHandle("0123456789abcdef") assert theItem.itemHandle is None + theItem.setHandle("0123456789abg") + assert theItem.itemHandle is None theItem.setHandle("0123456789abc") assert theItem.itemHandle == "0123456789abc" @@ -36,6 +42,8 @@ def testItemSettersSimple(nwDummy): assert theItem.itemParent is None theItem.setParent("0123456789abcdef") assert theItem.itemParent is None + theItem.setParent("0123456789abg") + assert theItem.itemParent is None theItem.setParent("0123456789abc") assert theItem.itemParent == "0123456789abc" @@ -59,6 +67,19 @@ def testItemSettersSimple(nwDummy): theItem.setStatus("Main") assert theItem.itemStatus == "Main" + # Importance + theItem.itemClass = nwItemClass.NOVEL + theItem.setStatus("Nonsense") + assert theItem.itemStatus == "New" + theItem.setStatus("New") + assert theItem.itemStatus == "New" + theItem.setStatus("Note") + assert theItem.itemStatus == "Note" + theItem.setStatus("Draft") + assert theItem.itemStatus == "Draft" + theItem.setStatus("Finished") + assert theItem.itemStatus == "Finished" + # Expanded theItem.setExpanded(8) assert not theItem.isExpanded @@ -73,6 +94,20 @@ def testItemSettersSimple(nwDummy): theItem.setExpanded(True) assert theItem.isExpanded + # Exported + theItem.setExported(8) + assert not theItem.isExported + theItem.setExported(None) + assert not theItem.isExported + theItem.setExported("None") + assert not theItem.isExported + theItem.setExported("What?") + assert not theItem.isExported + theItem.setExported("True") + assert theItem.isExported + theItem.setExported(True) + assert theItem.isExported + # CharCount theItem.setCharCount(None) assert theItem.charCount == 0 @@ -105,10 +140,47 @@ def testItemSettersSimple(nwDummy): theItem.setCursorPos(1) assert theItem.cursorPos == 1 -@pytest.mark.project -def testItemClassSetter(nwDummy): + # Initial Count + theItem.setWordCount(234) + theItem.saveInitialCount() + assert theItem.initCount == 234 - theProject = NWProject(nwDummy) +# END Test testCoreItem_Setters + +@pytest.mark.core +def testCoreItem_TypeSetter(dummyGUI): + """Test the setter for all the nwItemType values for the NWItem + class. + """ + theProject = NWProject(dummyGUI) + theItem = NWItem(theProject) + + # Type + theItem.setType(None) + assert theItem.itemType == nwItemType.NO_TYPE + theItem.setType("NONSENSE") + assert theItem.itemType == nwItemType.NO_TYPE + theItem.setType("NO_TYPE") + assert theItem.itemType == nwItemType.NO_TYPE + theItem.setType("ROOT") + assert theItem.itemType == nwItemType.ROOT + theItem.setType("FOLDER") + assert theItem.itemType == nwItemType.FOLDER + theItem.setType("FILE") + assert theItem.itemType == nwItemType.FILE + theItem.setType("TRASH") + assert theItem.itemType == nwItemType.TRASH + theItem.setType(nwItemType.ROOT) + assert theItem.itemType == nwItemType.ROOT + +# END Test testCoreItem_TypeSetter + +@pytest.mark.core +def testCoreItem_ClassSetter(dummyGUI): + """Test the setter for all the nwItemClass values for the NWItem + class. + """ + theProject = NWProject(dummyGUI) theItem = NWItem(theProject) # Class @@ -138,33 +210,17 @@ def testItemClassSetter(nwDummy): assert theItem.itemClass == nwItemClass.ARCHIVE theItem.setClass("TRASH") assert theItem.itemClass == nwItemClass.TRASH + theItem.setClass(nwItemClass.NOVEL) + assert theItem.itemClass == nwItemClass.NOVEL -@pytest.mark.project -def testItemTypeSetter(nwDummy): +# END Test testCoreItem_ClassSetter - theProject = NWProject(nwDummy) - theItem = NWItem(theProject) - - # Type - theItem.setType(None) - assert theItem.itemType == nwItemType.NO_TYPE - theItem.setType("NONSENSE") - assert theItem.itemType == nwItemType.NO_TYPE - theItem.setType("NO_TYPE") - assert theItem.itemType == nwItemType.NO_TYPE - theItem.setType("ROOT") - assert theItem.itemType == nwItemType.ROOT - theItem.setType("FOLDER") - assert theItem.itemType == nwItemType.FOLDER - theItem.setType("FILE") - assert theItem.itemType == nwItemType.FILE - theItem.setType("TRASH") - assert theItem.itemType == nwItemType.TRASH - -@pytest.mark.project -def testItemLayoutSetter(nwDummy): - - theProject = NWProject(nwDummy) +@pytest.mark.core +def testCoreItem_LayoutSetter(dummyGUI): + """Test the setter for all the nwItemLayout values for the NWItem + class. + """ + theProject = NWProject(dummyGUI) theItem = NWItem(theProject) # Layout @@ -190,14 +246,22 @@ def testItemLayoutSetter(nwDummy): assert theItem.itemLayout == nwItemLayout.SCENE theItem.setLayout("NOTE") assert theItem.itemLayout == nwItemLayout.NOTE + theItem.setLayout(nwItemLayout.NOTE) + assert theItem.itemLayout == nwItemLayout.NOTE -@pytest.mark.project -def testItemXMLPackUnpack(nwDummy): +# END Test testCoreItem_LayoutSetter - theProject = NWProject(nwDummy) - theItem = NWItem(theProject) +@pytest.mark.core +def testCoreItem_XMLPackUnpack(dummyGUI): + """Test packing and unpacking XML objects for the NWItem class. + """ + theProject = NWProject(dummyGUI) nwXML = etree.Element("novelWriterXML") + # File + # ==== + + theItem = NWItem(theProject) theItem.setHandle("0123456789abc") theItem.setParent("0123456789abc") theItem.setOrder(1) @@ -206,7 +270,7 @@ def testItemXMLPackUnpack(nwDummy): theItem.setType("FILE") theItem.setStatus("Main") theItem.setLayout("NOTE") - theItem.setExpanded(True) + theItem.setExported(False) theItem.setParaCount(3) theItem.setWordCount(5) theItem.setCharCount(7) @@ -219,17 +283,18 @@ def testItemXMLPackUnpack(nwDummy): b"" b"" b"A NameFILENOVELNew" - b"TrueNOTE7" + b"FalseNOTE7" b"5311" b"" ) # Unpack + theItem = NWItem(theProject) assert theItem.unpackXML(xContent[0]) assert theItem.itemHandle == "0123456789abc" assert theItem.itemParent == "0123456789abc" assert theItem.itemOrder == 1 - assert theItem.isExpanded + assert theItem.isExported is False assert theItem.paraCount == 3 assert theItem.wordCount == 5 assert theItem.charCount == 7 @@ -238,6 +303,52 @@ def testItemXMLPackUnpack(nwDummy): assert theItem.itemType == nwItemType.FILE assert theItem.itemLayout == nwItemLayout.NOTE + # Folder + # ====== + + theItem = NWItem(theProject) + theItem.setHandle("0123456789abc") + theItem.setParent("0123456789abc") + theItem.setOrder(1) + theItem.setName("A Name") + theItem.setClass("NOVEL") + theItem.setType("FOLDER") + theItem.setStatus("Main") + theItem.setLayout("NOTE") + theItem.setExpanded(True) + theItem.setExported(False) + theItem.setParaCount(3) + theItem.setWordCount(5) + theItem.setCharCount(7) + theItem.setCursorPos(11) + + # Pack + xContent = etree.SubElement(nwXML, "content") + theItem.packXML(xContent) + assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( + b"" + b"" + b"A NameFOLDERNOVELNew" + b"True" + b"" + ) + + # Unpack + theItem = NWItem(theProject) + assert theItem.unpackXML(xContent[0]) + assert theItem.itemHandle == "0123456789abc" + assert theItem.itemParent == "0123456789abc" + assert theItem.itemOrder == 1 + assert theItem.isExpanded is True + assert theItem.isExported is True + assert theItem.paraCount == 0 + assert theItem.wordCount == 0 + assert theItem.charCount == 0 + assert theItem.cursorPos == 0 + assert theItem.itemClass == nwItemClass.NOVEL + assert theItem.itemType == nwItemType.FOLDER + assert theItem.itemLayout == nwItemLayout.NO_LAYOUT + # Errors ## Not an Item @@ -268,3 +379,5 @@ def testItemXMLPackUnpack(nwDummy): assert etree.tostring(xDummy, pretty_print=False, encoding="utf-8") == ( b"" ) + +# END Test testCoreItem_XMLPackUnpack diff --git a/tests/test_core_options.py b/tests/test_core_options.py new file mode 100644 index 00000000..963a12c4 --- /dev/null +++ b/tests/test_core_options.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +"""novelWriter OptionState Class Tester +""" + +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 + +@pytest.mark.core +def testCoreOptions_LoadSave(monkeypatch, dummyGUI, tmpDir): + """Test loading and saving from the OptionState class. + """ + theProject = NWProject(dummyGUI) + theOpts = OptionState(theProject) + + # Write a test file + optFile = os.path.join(tmpDir, nwFiles.OPTS_FILE) + with open(optFile, mode="w+", encoding="utf8") as outFile: + json.dump({ + "GuiBuildNovel": { + "winWidth": 1000, + "winHeight": 700, + "addNovel": True, + "addNotes": False, + "textFont": "Cantarell", + "dummyItem": None, + }, + "DummyGroup": { + "dummyItem": None, + }, + }, outFile) + + # Load and save with no path set + theProject.projMeta = None + assert not theOpts.loadSettings() + assert not theOpts.saveSettings() + + # Set path + theProject.projMeta = tmpDir + assert theProject.projMeta == tmpDir + + # Cause open() to fail + monkeypatch.setattr("builtins.open", causeOSError) + assert not theOpts.loadSettings() + assert not theOpts.saveSettings() + monkeypatch.undo() + + # Load proper + assert theOpts.loadSettings() + + # Check that unwanted items have been removed + assert theOpts.theState == { + "GuiBuildNovel": { + "winWidth": 1000, + "winHeight": 700, + "addNovel": True, + "addNotes": False, + "textFont": "Cantarell", + }, + } + + # Save proper + assert theOpts.saveSettings() + + # Load again to check we get the values back + assert theOpts.loadSettings() + assert theOpts.theState == { + "GuiBuildNovel": { + "winWidth": 1000, + "winHeight": 700, + "addNovel": True, + "addNotes": False, + "textFont": "Cantarell", + }, + } + +# END Test testCoreOptions_LoadSave + +@pytest.mark.core +def testCoreOptions_SetGet(monkeypatch, dummyGUI, tmpDir): + """Test setting and getting values from the OptionState class. + """ + theProject = NWProject(dummyGUI) + theOpts = OptionState(theProject) + + # Set invalid values + assert not theOpts.setValue("DummyGroup", "dummyItem", None) + assert not theOpts.setValue("GuiBuildNovel", "dummyItem", None) + + # Set valid value + assert theOpts.setValue("GuiBuildNovel", "winWidth", 100) + + # Set some values of different types + assert theOpts.setValue("GuiBuildNovel", "winWidth", 100) + assert theOpts.setValue("GuiBuildNovel", "winHeight", 12.34) + assert theOpts.setValue("GuiBuildNovel", "addNovel", True) + assert theOpts.setValue("GuiBuildNovel", "textFont", "Cantarell") + + # Generic get, doesn't check type + assert theOpts.getValue("GuiBuildNovel", "winWidth", None) == 100 + assert theOpts.getValue("GuiBuildNovel", "winHeight", None) == 12.34 + assert theOpts.getValue("GuiBuildNovel", "addNovel", None) is True + assert theOpts.getValue("GuiBuildNovel", "textFont", None) == "Cantarell" + assert theOpts.getValue("GuiBuildNovel", "dummyItem", None) is None + + # Get type-specific + assert theOpts.getString("GuiBuildNovel", "winWidth", None) == "100" + assert theOpts.getString("GuiBuildNovel", "dummyItem", None) is None + assert theOpts.getInt("GuiBuildNovel", "winWidth", None) == 100 + assert theOpts.getInt("GuiBuildNovel", "textFont", None) is None + assert theOpts.getInt("GuiBuildNovel", "dummyItem", None) is None + assert theOpts.getFloat("GuiBuildNovel", "winWidth", None) == 100.0 + assert theOpts.getFloat("GuiBuildNovel", "textFont", None) is None + assert theOpts.getFloat("GuiBuildNovel", "dummyItem", None) is None + assert theOpts.getBool("GuiBuildNovel", "addNovel", None) is True + assert theOpts.getBool("GuiBuildNovel", "dummyItem", None) is None + + # Check integer validators + assert theOpts.validIntRange(5, 0, 9, 3) == 5 + assert theOpts.validIntRange(5, 0, 4, 3) == 3 + assert theOpts.validIntRange(5, 0, 5, 3) == 5 + assert theOpts.validIntRange(0, 0, 5, 3) == 0 + + assert theOpts.validIntTuple(0, (0, 1, 2), 3) == 0 + assert theOpts.validIntTuple(5, (0, 1, 2), 3) == 3 + +# END Test testCoreOptions_SetGet diff --git a/tests/test_core_project.py b/tests/test_core_project.py new file mode 100644 index 00000000..fe847983 --- /dev/null +++ b/tests/test_core_project.py @@ -0,0 +1,1215 @@ +# -*- coding: utf-8 -*- +"""novelWriter Project Class Tester +""" + +import pytest +import os + +from shutil import copyfile +from zipfile import ZipFile +from lxml import etree + +from tools import cmpFiles, writeFile, readFile +from dummy import causeOSError + +from nw.core.project import NWProject +from nw.constants import nwItemClass, nwItemType, nwItemLayout, nwFiles +from nw.common import formatTimeStamp + +@pytest.mark.core +def testCoreProject_NewMinimal(fncDir, outDir, refDir, tmpDir, dummyGUI): + """Create a new project from a project wizard dictionary. With + default setting, creating a Minimal project. + """ + projFile = os.path.join(fncDir, "nwProject.nwx") + testFile = os.path.join(outDir, "coreProject_1_nwProject.nwx") + compFile = os.path.join(refDir, "coreProject_1_nwProject.nwx") + + theProject = NWProject(dummyGUI) + theProject.projTree.setSeed(42) + + # Setting no data should fail + assert not theProject.newProject({}) + + # Try again with a proper path + assert theProject.newProject({"projPath": fncDir}) + assert theProject.saveProject() + assert theProject.closeProject() + + # Creating the project once more should fail + assert not theProject.newProject({"projPath": fncDir}) + + # Check the new project + copyfile(projFile, testFile) + assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) + + # Open again + assert theProject.openProject(projFile) + + # Save and close + assert theProject.saveProject() + assert theProject.closeProject() + copyfile(projFile, testFile) + assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) + assert not theProject.projChanged + + # Open a second time + assert theProject.openProject(projFile) + assert not theProject.openProject(projFile) + assert theProject.openProject(projFile, overrideLock=True) + assert theProject.saveProject() + assert theProject.closeProject() + copyfile(projFile, testFile) + assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) + +# END Test testCoreProject_NewMinimal + +@pytest.mark.core +def testCoreProject_NewCustomA(fncDir, outDir, refDir, dummyGUI): + """Create a new project from a project wizard dictionary. + Custom type with chapters and scenes. + """ + projFile = os.path.join(fncDir, "nwProject.nwx") + testFile = os.path.join(outDir, "coreProject_2_nwProject.nwx") + compFile = os.path.join(refDir, "coreProject_2_nwProject.nwx") + + projData = { + "projName": "Test Custom", + "projTitle": "Test Novel", + "projAuthors": "Jane Doe\nJohn Doh\n", + "projPath": fncDir, + "popSample": False, + "popMinimal": False, + "popCustom": True, + "addRoots": [ + nwItemClass.PLOT, + nwItemClass.CHARACTER, + nwItemClass.WORLD, + nwItemClass.TIMELINE, + nwItemClass.OBJECT, + nwItemClass.ENTITY, + ], + "numChapters": 3, + "numScenes": 3, + "chFolders": True, + } + theProject = NWProject(dummyGUI) + theProject.projTree.setSeed(42) + + assert theProject.newProject(projData) + assert theProject.saveProject() + assert theProject.closeProject() + + copyfile(projFile, testFile) + assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) + +# END Test testCoreProject_NewCustomA + +@pytest.mark.core +def testCoreProject_NewCustomB(fncDir, outDir, refDir, dummyGUI): + """Create a new project from a project wizard dictionary. + Custom type without chapters, but with scenes. + """ + projFile = os.path.join(fncDir, "nwProject.nwx") + testFile = os.path.join(outDir, "coreProject_3_nwProject.nwx") + compFile = os.path.join(refDir, "coreProject_3_nwProject.nwx") + + projData = { + "projName": "Test Custom", + "projTitle": "Test Novel", + "projAuthors": "Jane Doe\nJohn Doh\n", + "projPath": fncDir, + "popSample": False, + "popMinimal": False, + "popCustom": True, + "addRoots": [ + nwItemClass.PLOT, + nwItemClass.CHARACTER, + nwItemClass.WORLD, + nwItemClass.TIMELINE, + nwItemClass.OBJECT, + nwItemClass.ENTITY, + ], + "numChapters": 0, + "numScenes": 6, + "chFolders": True, + } + theProject = NWProject(dummyGUI) + theProject.projTree.setSeed(42) + + assert theProject.newProject(projData) + assert theProject.saveProject() + assert theProject.closeProject() + + copyfile(projFile, testFile) + assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) + +# END Test testCoreProject_NewCustomB + +@pytest.mark.core +def testCoreProject_NewSampleA(fncDir, tmpConf, dummyGUI, tmpDir): + """Check that we can create a new project can be created from the + provided sample project via a zip file. + """ + projData = { + "projName": "Test Sample", + "projTitle": "Test Novel", + "projAuthors": "Jane Doe\nJohn Doh\n", + "projPath": fncDir, + "popSample": True, + "popMinimal": False, + "popCustom": False, + } + theProject = NWProject(dummyGUI) + theProject.projTree.setSeed(42) + theProject.mainConf = tmpConf + + # Sample set, but no path + assert not theProject.newProject({"popSample": True}) + + # Force the lookup path for assets to our temp folder + srcSample = os.path.abspath(os.path.join(tmpConf.appRoot, "sample")) + dstSample = os.path.join(tmpDir, "sample.zip") + tmpConf.assetPath = tmpDir + + # Create and open a defective zip file + with open(dstSample, mode="w+") as outFile: + outFile.write("foo") + + assert not theProject.newProject(projData) + os.unlink(dstSample) + + # Create a real zip file, and unpack it + with ZipFile(dstSample, "w") as zipObj: + zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx") + for docFile in os.listdir(os.path.join(srcSample, "content")): + srcDoc = os.path.join(srcSample, "content", docFile) + zipObj.write(srcDoc, "content/"+docFile) + + assert theProject.newProject(projData) + assert theProject.openProject(fncDir) + assert theProject.projName == "Sample Project" + assert theProject.saveProject() + assert theProject.closeProject() + os.unlink(dstSample) + +# END Test testCoreProject_NewSampleA + +@pytest.mark.core +def testCoreProject_NewSampleB(monkeypatch, fncDir, tmpConf, dummyGUI, tmpDir): + """Check that we can create a new project can be created from the + provided sample project folder. + """ + projData = { + "projName": "Test Sample", + "projTitle": "Test Novel", + "projAuthors": "Jane Doe\nJohn Doh\n", + "projPath": fncDir, + "popSample": True, + "popMinimal": False, + "popCustom": False, + } + theProject = NWProject(dummyGUI) + theProject.projTree.setSeed(42) + theProject.mainConf = tmpConf + + # Make sure we do not pick up the nw/assets/sample.zip file + tmpConf.assetPath = tmpDir + + # Set a fake project file name + monkeypatch.setattr(nwFiles, "PROJ_FILE", "nothing.nwx") + assert not theProject.newProject(projData) + + monkeypatch.setattr(nwFiles, "PROJ_FILE", "nwProject.nwx") + assert theProject.newProject(projData) + assert theProject.openProject(fncDir) + assert theProject.projName == "Sample Project" + assert theProject.saveProject() + assert theProject.closeProject() + + # Misdirect the appRoot path so neither is possible + tmpConf.appRoot = tmpDir + assert not theProject.newProject(projData) + +# END Test testCoreProject_NewSampleB + +@pytest.mark.core +def testCoreProject_NewRoot(fncDir, outDir, refDir, dummyGUI): + """Check that new root folders can be added to the project. + """ + projFile = os.path.join(fncDir, "nwProject.nwx") + testFile = os.path.join(outDir, "coreProject_4_nwProject.nwx") + compFile = os.path.join(refDir, "coreProject_4_nwProject.nwx") + + theProject = NWProject(dummyGUI) + theProject.projTree.setSeed(42) + + assert theProject.newProject({"projPath": fncDir}) + assert theProject.setProjectPath(fncDir) + assert theProject.saveProject() + assert theProject.closeProject() + assert theProject.openProject(projFile) + + assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), type(None)) + assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), type(None)) + assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), type(None)) + assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), type(None)) + assert isinstance(theProject.newRoot("Timeline", nwItemClass.TIMELINE), str) + assert isinstance(theProject.newRoot("Object", nwItemClass.OBJECT), str) + assert isinstance(theProject.newRoot("Custom1", nwItemClass.CUSTOM), str) + assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str) + + assert theProject.projChanged + assert theProject.saveProject() + assert theProject.closeProject() + + copyfile(projFile, testFile) + assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) + assert not theProject.projChanged + +# END Test testCoreProject_NewRoot + +@pytest.mark.core +def testCoreProject_NewFile(fncDir, outDir, refDir, dummyGUI): + """Check that new files can be added to the project. + """ + projFile = os.path.join(fncDir, "nwProject.nwx") + testFile = os.path.join(outDir, "coreProject_5_nwProject.nwx") + compFile = os.path.join(refDir, "coreProject_5_nwProject.nwx") + + theProject = NWProject(dummyGUI) + theProject.projTree.setSeed(42) + + assert theProject.newProject({"projPath": fncDir}) + assert theProject.setProjectPath(fncDir) + assert theProject.saveProject() + assert theProject.closeProject() + assert theProject.openProject(projFile) + + assert isinstance(theProject.newFile("Hello", nwItemClass.NOVEL, "31489056e0916"), str) + assert isinstance(theProject.newFile("Jane", nwItemClass.CHARACTER, "71ee45a3c0db9"), str) + assert theProject.projChanged + assert theProject.saveProject() + assert theProject.closeProject() + + copyfile(projFile, testFile) + assert cmpFiles(testFile, compFile, [2, 6, 7, 8]) + assert not theProject.projChanged + +# END Test testCoreProject_NewFile + +@pytest.mark.core +def testCoreProject_Open(monkeypatch, nwMinimal, dummyGUI): + """Test opening a project. + """ + theProject = NWProject(dummyGUI) + + # Rename the project file to check handling + rName = os.path.join(nwMinimal, nwFiles.PROJ_FILE) + wName = os.path.join(nwMinimal, nwFiles.PROJ_FILE+"_sdfghj") + os.rename(rName, wName) + assert theProject.openProject(nwMinimal) is False + os.rename(wName, rName) + + # Fail on folder structure check + monkeypatch.setattr("os.mkdir", causeOSError) + assert theProject.openProject(nwMinimal) is False + monkeypatch.undo() + + # Fail on lock file + theProject.setProjectPath(nwMinimal) + assert theProject._writeLockFile() + assert theProject.openProject(nwMinimal) is False + + # Fail to read lockfile (which still opens the project) + monkeypatch.setattr("builtins.open", causeOSError) + assert theProject.openProject(nwMinimal) is True + monkeypatch.undo() + assert theProject.closeProject() + + # Force open with lockfile + theProject.setProjectPath(nwMinimal) + assert theProject._writeLockFile() + assert theProject.openProject(nwMinimal, overrideLock=True) is True + assert theProject.closeProject() + + # Make a junk XML file + oName = os.path.join(nwMinimal, nwFiles.PROJ_FILE[:-3]+"orig") + bName = os.path.join(nwMinimal, nwFiles.PROJ_FILE[:-3]+"bak") + os.rename(rName, oName) + writeFile(rName, "dummy") + assert theProject.openProject(nwMinimal) is False + + # Also write a jun XML backup file + writeFile(bName, "dummy") + assert theProject.openProject(nwMinimal) is False + + # Wrong root item + writeFile(rName, "\n") + assert theProject.openProject(nwMinimal) is False + + # Wrong file version + writeFile(rName, ( + "\n" + "\n" + "\n" + )) + dummyGUI.askResponse = False + assert theProject.openProject(nwMinimal) is False + dummyGUI.undo() + + # Future file version + writeFile(rName, ( + "\n" + "\n" + "\n" + )) + assert theProject.openProject(nwMinimal) is False + + # Larger hex version + writeFile(rName, ( + "\n" + "\n" + "\n" + )) + dummyGUI.askResponse = False + assert theProject.openProject(nwMinimal) is False + dummyGUI.undo() + + # Test skipping XML entries + writeFile(rName, ( + "\n" + "\n" + "\n" + "\n" + "\n" + )) + assert theProject.openProject(nwMinimal) is True + assert theProject.closeProject() + + # Test deprecated XML entries + writeFile(rName, ( + "\n" + "\n" + "\n" + "\n" + "B\n" + "\n" + "\n" + "\n" + )) + assert theProject.openProject(nwMinimal) is True + assert theProject.autoReplace == {"A": "B"} + assert theProject.closeProject() + + # Clean up XML files + os.unlink(rName) + os.unlink(bName) + os.rename(oName, rName) + + # Add some legacy stuff that cannot be removed + writeFile(os.path.join(nwMinimal, "junk"), "dummy") + os.mkdir(os.path.join(nwMinimal, "data_0")) + writeFile(os.path.join(nwMinimal, "data_0", "junk"), "dummy") + dummyGUI.clear() + assert theProject.openProject(nwMinimal) is True + assert "data_0" in dummyGUI.lastAlert + assert theProject.closeProject() + +# END Test testCoreProject_Open + +@pytest.mark.core +def testCoreProject_Save(monkeypatch, nwMinimal, dummyGUI, refDir): + """Test saving a project. + """ + theProject = NWProject(dummyGUI) + testFile = os.path.join(nwMinimal, "nwProject.nwx") + compFile = os.path.join(refDir, os.path.pardir, "minimal", "nwProject.nwx") + + # Nothing to save + assert theProject.saveProject() is False + + # Open test project + assert theProject.openProject(nwMinimal) + + # Fail on folder structure check + monkeypatch.setattr("os.path.isdir", lambda *args: False) + assert theProject.saveProject() is False + monkeypatch.undo() + + # Fail on open file + monkeypatch.setattr("builtins.open", causeOSError) + assert theProject.saveProject() is False + monkeypatch.undo() + + # Successful save + saveCount = theProject.saveCount + autoCount = theProject.autoCount + assert theProject.saveProject() is True + assert theProject.saveCount == saveCount + 1 + assert theProject.autoCount == autoCount + assert cmpFiles(testFile, compFile, [2, 6, 7, 8, 9]) + + # Successful autosave + saveCount = theProject.saveCount + autoCount = theProject.autoCount + assert theProject.saveProject(autoSave=True) is True + assert theProject.saveCount == saveCount + assert theProject.autoCount == autoCount + 1 + assert cmpFiles(testFile, compFile, [2, 6, 7, 8, 9]) + + # Close test project + assert theProject.closeProject() + +# END Test testCoreProject_Save + +@pytest.mark.core +def testCoreProject_LockFile(monkeypatch, fncDir, dummyGUI): + """Test lock file functions for the project folder. + """ + theProject = NWProject(dummyGUI) + + lockFile = os.path.join(fncDir, nwFiles.PROJ_LOCK) + + # No project + assert theProject._writeLockFile() is False + assert theProject._readLockFile() == ["ERROR"] + assert theProject._clearLockFile() is False + + theProject.projPath = fncDir + theProject.mainConf.hostName = "TestHost" + theProject.mainConf.osType = "TestOS" + theProject.mainConf.kernelVer = "1.0" + + # Block open + monkeypatch.setattr("builtins.open", causeOSError) + assert theProject._writeLockFile() is False + monkeypatch.undo() + + # Write lock file + monkeypatch.setattr("nw.core.project.time", lambda: 123.4) + assert theProject._writeLockFile() is True + monkeypatch.undo() + assert readFile(lockFile) == "TestHost\nTestOS\n1.0\n123\n" + + # Block open + monkeypatch.setattr("builtins.open", causeOSError) + assert theProject._readLockFile() == ["ERROR"] + monkeypatch.undo() + + # Read lock file + assert theProject._readLockFile() == ["TestHost", "TestOS", "1.0", "123"] + + # Block unlink + monkeypatch.setattr("os.unlink", causeOSError) + assert os.path.isfile(lockFile) + assert theProject._clearLockFile() is False + assert os.path.isfile(lockFile) + monkeypatch.undo() + + # Clear file + assert os.path.isfile(lockFile) + assert theProject._clearLockFile() is True + assert not os.path.isfile(lockFile) + + # Read again, no file + assert theProject._readLockFile() == [] + + # Read an invalid lock file + writeFile(lockFile, "A\nB") + assert theProject._readLockFile() == ["ERROR"] + assert theProject._clearLockFile() is True + +# END Test testCoreProject_LockFile + +@pytest.mark.core +def testCoreProject_Helpers(monkeypatch, fncDir, dummyGUI): + """Test helper functions for the project folder. + """ + theProject = NWProject(dummyGUI) + + # No path + assert theProject.ensureFolderStructure() is False + + # Set the correct dir + theProject.projPath = fncDir + + # Block user's home folder + monkeypatch.setattr("os.path.expanduser", lambda *args, **kwargs: fncDir) + assert theProject.ensureFolderStructure() is False + monkeypatch.undo() + + # Create a file to block meta folder + metaDir = os.path.join(fncDir, "meta") + writeFile(metaDir, "dummy") + assert theProject.ensureFolderStructure() is False + os.unlink(metaDir) + + # Create a file to block cache folder + cacheDir = os.path.join(fncDir, "cache") + writeFile(cacheDir, "dummy") + assert theProject.ensureFolderStructure() is False + os.unlink(cacheDir) + + # Create a file to block content folder + contentDir = os.path.join(fncDir, "content") + writeFile(contentDir, "dummy") + assert theProject.ensureFolderStructure() is False + os.unlink(contentDir) + + # Now, do it right + assert theProject.ensureFolderStructure() is True + assert os.path.isdir(metaDir) + assert os.path.isdir(cacheDir) + assert os.path.isdir(contentDir) + +# END Test testCoreProject_Helpers + +@pytest.mark.core +def testCoreProject_AccessItems(nwMinimal, dummyGUI): + """Test helper functions for the project folder. + """ + theProject = NWProject(dummyGUI) + theProject.openProject(nwMinimal) + + # Move Novel ROOT to after its files + oldOrder = [ + "a508bb932959c", # ROOT: Novel + "a35baf2e93843", # FILE: Title Page + "a6d311a93600a", # FOLDER: New Chapter + "f5ab3e30151e1", # FILE: New Chapter + "8c659a11cd429", # FILE: New Scene + "7695ce551d265", # ROOT: Plot + "afb3043c7b2b3", # ROOT: Characters + "9d5247ab588e0", # ROOT: World + ] + newOrder = [ + "a35baf2e93843", # FILE: Title Page + "f5ab3e30151e1", # FILE: New Chapter + "8c659a11cd429", # FILE: New Scene + "a6d311a93600a", # FOLDER: New Chapter + "a508bb932959c", # ROOT: Novel + "7695ce551d265", # ROOT: Plot + "afb3043c7b2b3", # ROOT: Characters + "9d5247ab588e0", # ROOT: World + ] + assert theProject.projTree.handles() == oldOrder + assert theProject.setTreeOrder(newOrder) + assert theProject.projTree.handles() == newOrder + + # Add a non-existing item + theProject.projTree._treeOrder.append("01234567789abc") + + # Add an item with a non-existent parent + nHandle = theProject.newFile("Test File", nwItemClass.NOVEL, "a6d311a93600a") + theProject.projTree[nHandle].setParent("cba9876543210") + assert theProject.projTree[nHandle].itemParent == "cba9876543210" + + retOrder = [] + for tItem in theProject.getProjectItems(): + retOrder.append(tItem.itemHandle) + + assert retOrder == [ + "a508bb932959c", # ROOT: Novel + "7695ce551d265", # ROOT: Plot + "afb3043c7b2b3", # ROOT: Characters + "9d5247ab588e0", # ROOT: World + nHandle, # FILE: Test File + "a35baf2e93843", # FILE: Title Page + "a6d311a93600a", # FOLDER: New Chapter + "f5ab3e30151e1", # FILE: New Chapter + "8c659a11cd429", # FILE: New Scene + ] + assert theProject.projTree[nHandle].itemParent is None + +# END Test testCoreProject_AccessItems + +@pytest.mark.core +def testCoreProject_Methods(monkeypatch, nwMinimal, dummyGUI, tmpDir): + """Test other project class methods and functions. + """ + theProject = NWProject(dummyGUI) + theProject.projTree.setSeed(42) + assert theProject.openProject(nwMinimal) + assert theProject.projPath == nwMinimal + + # Setting project path + assert theProject.setProjectPath(None) + assert theProject.projPath is None + assert theProject.setProjectPath("") + assert theProject.projPath is None + assert theProject.setProjectPath("~") + assert theProject.projPath == os.path.expanduser("~") + + # Create a new folder and populate it + projPath = os.path.join(nwMinimal, "dummy1") + assert theProject.setProjectPath(projPath, newProject=True) + + # Make os.mkdir fail + monkeypatch.setattr("os.mkdir", causeOSError) + projPath = os.path.join(nwMinimal, "dummy2") + assert not theProject.setProjectPath(projPath, newProject=True) + + # Set back + assert theProject.setProjectPath(nwMinimal) + + # Project Name + assert theProject.setProjectName(" A Name ") + assert theProject.projName == "A Name" + + # Project Title + assert theProject.setBookTitle(" A Title ") + assert theProject.bookTitle == "A Title" + + # Project Authors + assert not theProject.setBookAuthors([]) + assert theProject.setBookAuthors(" Jane Doe \n John Doh \n ") + assert theProject.bookAuthors == ["Jane Doe", "John Doh"] + + # Trash folder + # Should create on first call, and just returned on later calls + assert theProject.projTree["73475cb40a568"] is None + assert theProject.trashFolder() == "73475cb40a568" + assert theProject.trashFolder() == "73475cb40a568" + + # Project backup + assert theProject.doBackup is True + assert theProject.setProjBackup(False) + assert theProject.doBackup is False + + assert not theProject.setProjBackup(True) + theProject.mainConf.backupPath = tmpDir + assert theProject.setProjBackup(True) + + assert theProject.setProjectName("") + assert not theProject.setProjBackup(True) + assert theProject.setProjectName("A Name") + assert theProject.setProjBackup(True) + + # Spell check + theProject.projChanged = False + assert theProject.setSpellCheck(True) + assert not theProject.setSpellCheck(False) + assert theProject.projChanged + + # Spell language + theProject.projChanged = False + assert theProject.setSpellLang(None) + assert theProject.projLang is None + assert theProject.setSpellLang("None") + assert theProject.projLang is None + assert theProject.setSpellLang("en_GB") + assert theProject.projLang == "en_GB" + assert theProject.projChanged + + # Automatic outline update + theProject.projChanged = False + assert theProject.setAutoOutline(True) + assert not theProject.setAutoOutline(False) + assert theProject.projChanged + + # Last edited + theProject.projChanged = False + assert theProject.setLastEdited("0123456789abc") + assert theProject.lastEdited == "0123456789abc" + assert theProject.projChanged + + # Last viewed + theProject.projChanged = False + assert theProject.setLastViewed("0123456789abc") + assert theProject.lastViewed == "0123456789abc" + assert theProject.projChanged + + # Autoreplace + theProject.projChanged = False + assert theProject.setAutoReplace({"A": "B", "C": "D"}) + assert theProject.autoReplace == {"A": "B", "C": "D"} + assert theProject.projChanged + + # Change project tree order + oldOrder = [ + "a508bb932959c", "a35baf2e93843", "a6d311a93600a", + "f5ab3e30151e1", "8c659a11cd429", "7695ce551d265", + "afb3043c7b2b3", "9d5247ab588e0", "73475cb40a568", + ] + newOrder = [ + "f5ab3e30151e1", "8c659a11cd429", "7695ce551d265", + "a508bb932959c", "a35baf2e93843", "a6d311a93600a", + "afb3043c7b2b3", "9d5247ab588e0", + ] + assert theProject.projTree.handles() == oldOrder + assert theProject.setTreeOrder(newOrder) + assert theProject.projTree.handles() == newOrder + assert theProject.setTreeOrder(oldOrder) + assert theProject.projTree.handles() == oldOrder + + # Change status + theProject.projTree["a35baf2e93843"].setStatus("Finished") + theProject.projTree["a6d311a93600a"].setStatus("Draft") + theProject.projTree["f5ab3e30151e1"].setStatus("Note") + theProject.projTree["8c659a11cd429"].setStatus("Finished") + newList = [ + ("New", 1, 1, 1, "New"), + ("Draft", 2, 2, 2, "Note"), # These are swapped + ("Note", 3, 3, 3, "Draft"), # These are swapped + ("Edited", 4, 4, 4, "Finished"), # Renamed + ("Finished", 5, 5, 5, None), # New, with reused name + ] + assert theProject.setStatusColours(newList) + assert theProject.statusItems._theLabels == [ + "New", "Draft", "Note", "Edited", "Finished" + ] + assert theProject.statusItems._theColours == [ + (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5) + ] + assert theProject.projTree["a35baf2e93843"].itemStatus == "Edited" # Renamed + assert theProject.projTree["a6d311a93600a"].itemStatus == "Note" # Swapped + assert theProject.projTree["f5ab3e30151e1"].itemStatus == "Draft" # Swapped + assert theProject.projTree["8c659a11cd429"].itemStatus == "Edited" # Renamed + + # Change importance + fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "afb3043c7b2b3") + theProject.projTree[fHandle].setStatus("Main") + newList = [ + ("New", 1, 1, 1, "New"), + ("Minor", 2, 2, 2, "Minor"), + ("Major", 3, 3, 3, "Major"), + ("Min", 4, 4, 4, "Main"), + ("Max", 5, 5, 5, None), + ] + assert theProject.setImportColours(newList) + assert theProject.importItems._theLabels == [ + "New", "Minor", "Major", "Min", "Max" + ] + assert theProject.importItems._theColours == [ + (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5) + ] + assert theProject.projTree[fHandle].itemStatus == "Min" + + # Check status counts + assert theProject.statusItems._theCounts == [0, 0, 0, 0, 0] + assert theProject.importItems._theCounts == [0, 0, 0, 0, 0] + theProject.countStatus() + assert theProject.statusItems._theCounts == [1, 1, 1, 2, 0] + assert theProject.importItems._theCounts == [3, 0, 0, 1, 0] + + # Check word counts + theProject.currWCount = 200 + theProject.lastWCount = 100 + assert theProject.getSessionWordCount() == 100 + + # Session stats + monkeypatch.setattr("os.path.isdir", lambda *args, **kwargs: False) + assert not theProject._appendSessionStats() + monkeypatch.undo() + + # Block open + monkeypatch.setattr("builtins.open", causeOSError) + assert not theProject._appendSessionStats() + monkeypatch.undo() + + # Write entry + assert theProject.projMeta == os.path.join(nwMinimal, "meta") + statsFile = os.path.join(theProject.projMeta, nwFiles.SESS_STATS) + + theProject.projOpened = 1600002000 + theProject.novelWCount = 200 + theProject.notesWCount = 100 + + monkeypatch.setattr("nw.core.project.time", lambda: 1600005600) + assert theProject._appendSessionStats() + monkeypatch.undo() + + assert readFile(statsFile) == ( + "# Offset 100\n" + "# Start Time End Time Novel Notes\n" + "%s %s 200 100\n" + ) % (formatTimeStamp(1600002000), formatTimeStamp(1600005600)) + + # Pack XML Value + xElem = etree.Element("element") + theProject._packProjectValue(xElem, "A", "B", allowNone=False) + assert etree.tostring(xElem, pretty_print=False, encoding="utf-8") == ( + b"B" + ) + + xElem = etree.Element("element") + theProject._packProjectValue(xElem, "A", "", allowNone=False) + assert etree.tostring(xElem, pretty_print=False, encoding="utf-8") == ( + b"" + ) + + # Pack XML Key/Value + xElem = etree.Element("element") + theProject._packProjectKeyValue(xElem, "item", {"A": "B", "C": "D"}) + assert etree.tostring(xElem, pretty_print=False, encoding="utf-8") == ( + b"" + b"" + b"B" + b"D" + b"" + b"" + ) + +# END Test testCoreProject_Methods + +@pytest.mark.core +def testCoreProject_OrphanedFiles(dummyGUI, nwLipsum): + """Check that files in the content folder that are not tracked in + the project XML file are handled correctly by the orphaned files + function. It should also restore as much meta data as possible from + the meta line at the top of the document file. + """ + theProject = NWProject(dummyGUI) + + assert theProject.openProject(nwLipsum) + assert theProject.projTree["636b6aa9b697b"] is None + assert theProject.closeProject() + + # First Item with Meta Data + orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd") + with open(orphPath, mode="w", encoding="utf8") as outFile: + outFile.write("%%~name:Mars\n") + outFile.write("%%~path:5eaea4e8cdee8/636b6aa9b697b\n") + outFile.write("%%~kind:WORLD/NOTE\n") + outFile.write("%%~invalid\n") + outFile.write("\n") + + # Second Item without Meta Data + orphPath = os.path.join(nwLipsum, "content", "736b6aa9b697b.nwd") + with open(orphPath, mode="w", encoding="utf8") as outFile: + outFile.write("\n") + + # Invalid File Name + dummyPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.txt") + with open(dummyPath, mode="w", encoding="utf8") as outFile: + outFile.write("\n") + + # Invalid File Name + dummyPath = os.path.join(nwLipsum, "content", "636b6aa9b697bb.nwd") + with open(dummyPath, mode="w", encoding="utf8") as outFile: + outFile.write("\n") + + # Invalid File Name + dummyPath = os.path.join(nwLipsum, "content", "abcdefghijklm.nwd") + with open(dummyPath, mode="w", encoding="utf8") as outFile: + outFile.write("\n") + + assert theProject.openProject(nwLipsum) + assert theProject.projPath is not None + assert theProject.projTree["636b6aa9b697bb"] is None + assert theProject.projTree["abcdefghijklm"] is None + + # First Item with Meta Data + oItem = theProject.projTree["636b6aa9b697b"] + assert oItem is not None + assert oItem.itemName == "Mars" + assert oItem.itemHandle == "636b6aa9b697b" + assert oItem.itemParent is None + assert oItem.itemClass == nwItemClass.WORLD + assert oItem.itemType == nwItemType.FILE + assert oItem.itemLayout == nwItemLayout.NOTE + + # Second Item without Meta Data + oItem = theProject.projTree["736b6aa9b697b"] + assert oItem is not None + assert oItem.itemName == "Orphaned File 1" + assert oItem.itemHandle == "736b6aa9b697b" + assert oItem.itemParent is None + assert oItem.itemClass == nwItemClass.NO_CLASS + assert oItem.itemType == nwItemType.FILE + assert oItem.itemLayout == nwItemLayout.NO_LAYOUT + + assert theProject.saveProject(nwLipsum) + assert theProject.closeProject() + + # Finally, check that the orphaned files function returns + # if no project is open and no path is set + assert not theProject._scanProjectFolder() + +# END Test testCoreProject_OrphanedFiles + +@pytest.mark.core +def testCoreProject_OldFormat(dummyGUI, nwOldProj): + """Test that a project folder structure of version 1.0 can be + converted to the latest folder structure. Version 1.0 split the + documents into 'data_0' ... 'data_f' folders, which are now all + contained in a single 'content' folder. + """ + theProject = NWProject(dummyGUI) + + # Create dummy files for known legacy files + deleteFiles = [ + os.path.join(nwOldProj, "cache", "nwProject.nwx.0"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.1"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.2"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.3"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.4"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.5"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.6"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.7"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.8"), + os.path.join(nwOldProj, "cache", "nwProject.nwx.9"), + os.path.join(nwOldProj, "meta", "mainOptions.json"), + os.path.join(nwOldProj, "meta", "exportOptions.json"), + os.path.join(nwOldProj, "meta", "outlineOptions.json"), + os.path.join(nwOldProj, "meta", "timelineOptions.json"), + os.path.join(nwOldProj, "meta", "docMergeOptions.json"), + os.path.join(nwOldProj, "meta", "sessionLogOptions.json"), + ] + + # Add some files that shouldn't be there + deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.nwd")) + deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.txt")) + + # Add some folders that shouldn't be there + os.mkdir(os.path.join(nwOldProj, "stuff")) + os.mkdir(os.path.join(nwOldProj, "data_1", "stuff")) + + # Create dummy files + os.mkdir(os.path.join(nwOldProj, "cache")) + for aFile in deleteFiles: + writeFile(aFile, "Hi") + for aFile in deleteFiles: + assert os.path.isfile(aFile) + + # Open project and check that files that are not supposed to be + # there have been removed + assert theProject.openProject(nwOldProj) + for aFile in deleteFiles: + assert not os.path.isfile(aFile) + + assert not os.path.isdir(os.path.join(nwOldProj, "data_1", "stuff")) + assert not os.path.isdir(os.path.join(nwOldProj, "data_1")) + assert not os.path.isdir(os.path.join(nwOldProj, "data_7")) + assert not os.path.isdir(os.path.join(nwOldProj, "data_8")) + assert not os.path.isdir(os.path.join(nwOldProj, "data_9")) + assert not os.path.isdir(os.path.join(nwOldProj, "data_a")) + assert not os.path.isdir(os.path.join(nwOldProj, "data_f")) + + # Check stuff that has been moved + assert os.path.isdir(os.path.join(nwOldProj, "junk")) + assert os.path.isdir(os.path.join(nwOldProj, "junk", "stuff")) + assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.nwd")) + assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.txt")) + + # Check that files we want to keep are in the right place + assert os.path.isdir(os.path.join(nwOldProj, "cache")) + assert os.path.isdir(os.path.join(nwOldProj, "content")) + assert os.path.isdir(os.path.join(nwOldProj, "meta")) + + assert os.path.isfile(os.path.join(nwOldProj, "content", "f528d831f5b24.nwd")) + assert os.path.isfile(os.path.join(nwOldProj, "content", "88124a4292d8b.nwd")) + assert os.path.isfile(os.path.join(nwOldProj, "content", "91239bf2f8b69.nwd")) + assert os.path.isfile(os.path.join(nwOldProj, "content", "19752e7f9d8af.nwd")) + assert os.path.isfile(os.path.join(nwOldProj, "content", "a764d5acf5a21.nwd")) + assert os.path.isfile(os.path.join(nwOldProj, "content", "9058ae29f0dfd.nwd")) + assert os.path.isfile(os.path.join(nwOldProj, "content", "7ff63b8afc4cd.nwd")) + + assert os.path.isfile(os.path.join(nwOldProj, "meta", "tagsIndex.json")) + assert os.path.isfile(os.path.join(nwOldProj, "meta", "sessionInfo.log")) + + # Close the project + theProject.closeProject() + + # Check that new files have been created + assert os.path.isfile(os.path.join(nwOldProj, "meta", "guiOptions.json")) + assert os.path.isfile(os.path.join(nwOldProj, "meta", "sessionStats.log")) + assert os.path.isfile(os.path.join(nwOldProj, "ToC.txt")) + +# END Test testCoreProject_OldFormat + +@pytest.mark.core +def testCoreProject_LegacyData(monkeypatch, dummyGUI, fncDir): + """Test the functins that handle legacy data folders and structure + with additional tests of failure handling. + """ + theProject = NWProject(dummyGUI) + theProject.setProjectPath(fncDir) + + # assert theProject.newProject({"projPath": fncDir}) + # assert theProject.saveProject() + # assert theProject.closeProject() + + # Check behaviour of deprecated files function on OSError + tstFile = os.path.join(fncDir, "ToC.json") + writeFile(tstFile, "dummy") + assert os.path.isfile(tstFile) + + monkeypatch.setattr("os.unlink", causeOSError) + assert not theProject._deprecatedFiles() + monkeypatch.undo() + + assert theProject._deprecatedFiles() + assert not os.path.isfile(tstFile) + + # Check processing non-folders + tstFile = os.path.join(fncDir, "data_0") + writeFile(tstFile, "dummy") + assert os.path.isfile(tstFile) + + errList = [] + errList = theProject._legacyDataFolder(tstFile, errList) + assert len(errList) > 0 + + # Move folder in data folder, shouldn't be there + tstData = os.path.join(fncDir, "data_1") + errItem = os.path.join(fncDir, "data_1", "stuff") + os.mkdir(tstData) + os.mkdir(errItem) + assert os.path.isdir(tstData) + assert os.path.isdir(errItem) + + # This causes a failure to create the 'junk' folder + monkeypatch.setattr("os.mkdir", causeOSError) + errList = [] + errList = theProject._legacyDataFolder(tstData, errList) + assert len(errList) > 0 + monkeypatch.undo() + + # This causes a failure to move 'stuff' to 'junk' + monkeypatch.setattr("os.rename", causeOSError) + errList = [] + errList = theProject._legacyDataFolder(tstData, errList) + assert len(errList) > 0 + monkeypatch.undo() + + # This should be successful + errList = [] + errList = theProject._legacyDataFolder(tstData, errList) + assert len(errList) == 0 + assert os.path.isdir(os.path.join(fncDir, "junk", "stuff")) + + # Check renaming/deleting of old document files + tstData = os.path.join(fncDir, "data_2") + tstDoc1m = os.path.join(tstData, "000000000001_main.nwd") + tstDoc1b = os.path.join(tstData, "000000000001_main.bak") + tstDoc2m = os.path.join(tstData, "000000000002_main.nwd") + tstDoc2b = os.path.join(tstData, "000000000002_main.bak") + tstDoc3m = os.path.join(tstData, "tooshort003_main.nwd") + tstDoc3b = os.path.join(tstData, "tooshort003_main.bak") + + os.mkdir(tstData) + writeFile(tstDoc1m, "dummy") + writeFile(tstDoc1b, "dummy") + writeFile(tstDoc2m, "dummy") + writeFile(tstDoc2b, "dummy") + writeFile(tstDoc3m, "dummy") + writeFile(tstDoc3b, "dummy") + + # Make the above fail + monkeypatch.setattr("os.rename", causeOSError) + monkeypatch.setattr("os.unlink", causeOSError) + errList = [] + errList = theProject._legacyDataFolder(tstData, errList) + assert len(errList) > 0 + assert os.path.isfile(tstDoc1m) + assert os.path.isfile(tstDoc1b) + assert os.path.isfile(tstDoc2m) + assert os.path.isfile(tstDoc2b) + assert os.path.isfile(tstDoc3m) + assert os.path.isfile(tstDoc3b) + monkeypatch.undo() + + # And succeed ... + errList = [] + errList = theProject._legacyDataFolder(tstData, errList) + assert len(errList) == 0 + + assert not os.path.isdir(tstData) + assert os.path.isfile(os.path.join(fncDir, "content", "2000000000001.nwd")) + assert os.path.isfile(os.path.join(fncDir, "content", "2000000000002.nwd")) + assert os.path.isfile(os.path.join(fncDir, "junk", "tooshort003_main.nwd")) + assert os.path.isfile(os.path.join(fncDir, "junk", "tooshort003_main.bak")) + +# END Test testCoreProject_LegacyData + +@pytest.mark.core +def testCoreProject_Backup(monkeypatch, dummyGUI, nwMinimal, tmpDir): + """Test the automated backup feature of the project class. The test + creates a backup of the Minimal test project, and then unzips the + backupd file and checks that the project XML file is identical to + the original file. + """ + theProject = NWProject(dummyGUI) + assert theProject.openProject(nwMinimal) + + # Test faulty settings + + # No project + dummyGUI.hasProject = False + assert not theProject.zipIt(doNotify=False) + dummyGUI.hasProject = True + + # Invalid path + theProject.mainConf.backupPath = None + assert not theProject.zipIt(doNotify=False) + + # Missing project name + theProject.mainConf.backupPath = tmpDir + theProject.projName = "" + assert not theProject.zipIt(doNotify=False) + + # Non-existent folder + theProject.mainConf.backupPath = os.path.join(tmpDir, "nonexistent") + theProject.projName = "Test Minimal" + assert not theProject.zipIt(doNotify=False) + + # Same folder as project (causes infinite loop in zipping) + theProject.mainConf.backupPath = nwMinimal + assert not theProject.zipIt(doNotify=False) + + # Set a valid folder + theProject.mainConf.backupPath = tmpDir + + # Can't make folder + monkeypatch.setattr("os.mkdir", causeOSError) + assert not theProject.zipIt(doNotify=False) + monkeypatch.undo() + + # Can't write archive + monkeypatch.setattr("shutil.make_archive", causeOSError) + assert not theProject.zipIt(doNotify=False) + monkeypatch.undo() + + # Test correct settings + assert theProject.zipIt(doNotify=True) + + theFiles = os.listdir(os.path.join(tmpDir, "Test Minimal")) + assert len(theFiles) == 1 + + theZip = theFiles[0] + assert theZip[:12] == "Backup from " + assert theZip[-4:] == ".zip" + + # Extract the archive + with ZipFile(os.path.join(tmpDir, "Test Minimal", theZip), "r") as inZip: + inZip.extractall(os.path.join(tmpDir, "extract")) + + # Check that the main project file was restored + assert cmpFiles( + os.path.join(nwMinimal, "nwProject.nwx"), + os.path.join(tmpDir, "extract", "nwProject.nwx") + ) + +# END Test testCoreProject_Backup diff --git a/tests/test_core_spell.py b/tests/test_core_spell.py new file mode 100644 index 00000000..1ef94816 --- /dev/null +++ b/tests/test_core_spell.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +"""novelWriter Spell Check Class Tester +""" + +import os +import sys +import pytest + +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/test_core_status.py b/tests/test_core_status.py new file mode 100644 index 00000000..515b4b47 --- /dev/null +++ b/tests/test_core_status.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +"""novelWriter Status Class Tester +""" + +import pytest + +from lxml import etree + +from nw.core.status import NWStatus + +@pytest.mark.core +def testCoreStatus_Entries(): + """Test all the simple setters for the NWItem class. + """ + theStatus = NWStatus() + + # Add entries + theStatus.addEntry("New", (100, 100, 100)) + theStatus.addEntry("Minor", (200, 50, 0)) + theStatus.addEntry("Major", (200, 150, 0)) + theStatus.addEntry("Main", (50, 200, 0)) + + assert theStatus._theLabels == ["New", "Minor", "Major", "Main"] + assert theStatus._theColours == [(100, 100, 100), (200, 50, 0), (200, 150, 0), (50, 200, 0)] + assert theStatus._theCounts == [0, 0, 0, 0] + assert theStatus._theMap["New"] == 0 + assert theStatus._theMap["Minor"] == 1 + assert theStatus._theMap["Major"] == 2 + assert theStatus._theMap["Main"] == 3 + assert theStatus._theLength == 4 + + # Lookups + assert theStatus.lookupEntry(None) is None + assert theStatus.lookupEntry("dummy") is None + assert theStatus.lookupEntry("Main") == 3 + + # Checks + assert theStatus.checkEntry(123) == "New" + assert theStatus.checkEntry("Stuff") == "New" + assert theStatus.checkEntry("New ") == "New" + assert theStatus.checkEntry(" Main ") == "Main" + + # Set new list + newList = [ + ("New", 1, 1, 1, "New"), + ("Minor", 2, 2, 2, "Minor"), + ("Major", 3, 3, 3, "Major"), + ("Min", 4, 4, 4, "Main"), + ("Max", 5, 5, 5, None), + ] + assert theStatus.setNewEntries(None) == {} + assert theStatus.setNewEntries(newList) == {"Main": "Min"} + + assert theStatus._theLabels == ["New", "Minor", "Major", "Min", "Max"] + assert theStatus._theColours == [(1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)] + assert theStatus._theCounts == [0, 0, 0, 0, 0] + assert theStatus._theMap["New"] == 0 + assert theStatus._theMap["Minor"] == 1 + assert theStatus._theMap["Major"] == 2 + assert theStatus._theMap["Min"] == 3 + assert theStatus._theMap["Max"] == 4 + assert theStatus._theLength == 5 + + # Add counts + countTo = [3, 5, 7, 9, 11] + for i, n in enumerate(countTo): + for _ in range(n): + theStatus.countEntry(theStatus._theLabels[i]) + assert theStatus._theCounts == countTo + + # Iterate + for i, (sA, sB, sC) in enumerate(theStatus): + assert sA == theStatus._theLabels[i] + assert sB == theStatus._theColours[i] + assert sC == theStatus._theCounts[i] + + assert theStatus[9] == (None, None, None) + + # Clear counts + theStatus.resetCounts() + assert theStatus._theCounts == [0, 0, 0, 0, 0] + +# END Test testCoreStatus_Entries + +@pytest.mark.core +def testCoreStatus_XMLPackUnpack(): + """Test all the simple setters for the NWItem class. + """ + theStatus = NWStatus() + theStatus.addEntry("New", (100, 100, 100)) + theStatus.addEntry("Minor", (200, 50, 0)) + theStatus.addEntry("Major", (200, 150, 0)) + theStatus.addEntry("Main", (50, 200, 0)) + + countTo = [3, 5, 7, 9] + for i, n in enumerate(countTo): + for _ in range(n): + theStatus.countEntry(theStatus._theLabels[i]) + + nwXML = etree.Element("novelWriterXML") + + # Pack + xStatus = etree.SubElement(nwXML, "status") + theStatus.packXML(xStatus) + assert etree.tostring(xStatus, pretty_print=False, encoding="utf-8") == ( + b"" + b"New" + b"Minor" + b"Major" + b"Main" + b"" + ) + + # Unpack + theStatus = NWStatus() + assert theStatus.unpackXML(xStatus) + assert theStatus._theLabels == ["New", "Minor", "Major", "Main"] + assert theStatus._theColours == [(100, 100, 100), (200, 50, 0), (200, 150, 0), (50, 200, 0)] + assert theStatus._theCounts == [0, 0, 0, 0] + assert theStatus._theMap["New"] == 0 + assert theStatus._theMap["Minor"] == 1 + assert theStatus._theMap["Major"] == 2 + assert theStatus._theMap["Main"] == 3 + assert theStatus._theLength == 4 + +# END Test testCoreStatus_XMLPackUnpack diff --git a/tests/test_core_tohtml.py b/tests/test_core_tohtml.py new file mode 100644 index 00000000..7c228edf --- /dev/null +++ b/tests/test_core_tohtml.py @@ -0,0 +1,373 @@ +# -*- coding: utf-8 -*- +"""novelWriter ToHtml Class Tester +""" + +import pytest + +from nw.core import NWProject, NWIndex, ToHtml + +@pytest.mark.core +def testCoreToHtml_Format(dummyGUI): + """Test all the formatters for the ToHtml class. + """ + theProject = NWProject(dummyGUI) + dummyGUI.theIndex = NWIndex(theProject, dummyGUI) + theHtml = ToHtml(theProject, dummyGUI) + + # Export Mode + # =========== + + assert theHtml._formatSynopsis("synopsis text") == ( + "

Synopsis: synopsis text

\n" + ) + assert theHtml._formatComments("comment text") == ( + "

Comment: comment text

\n" + ) + + assert theHtml._formatKeywords("") == "" + assert theHtml._formatKeywords("tag: Jane") == ( + "
Tag: Jane
" + ) + assert theHtml._formatKeywords("char: Bod, Jane") == ( + "
" + "Characters: " + "Bod, " + "Jane" + "
" + ) + + # Preview Mode + # ============ + + theHtml.setPreview(True, True) + + assert theHtml._formatSynopsis("synopsis text") == ( + "

Synopsis: synopsis text

\n" + ) + assert theHtml._formatComments("comment text") == ( + "

comment text

\n" + ) + + assert theHtml._formatKeywords("") == "" + assert theHtml._formatKeywords("tag: Jane") == ( + "
Tag: Jane
" + ) + assert theHtml._formatKeywords("char: Bod, Jane") == ( + "
" + "Characters: " + "Bod, " + "Jane" + "
" + ) + +# END Test testCoreToHtml_Format + +@pytest.mark.core +def testCoreToHtml_Convert(dummyGUI): + """Test the converter of the ToHtml class. + """ + theProject = NWProject(dummyGUI) + dummyGUI.theIndex = NWIndex(theProject, dummyGUI) + theHtml = ToHtml(theProject, dummyGUI) + + # Export Mode + # =========== + + theHtml.isNovel = True + + # Header 1 + theHtml.theText = "# Title\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == "

Title

\n" + + # Header 2 + theHtml.theText = "## Chapter Title\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == "

Chapter Title

\n" + + # Header 3 + theHtml.theText = "### Scene Title\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == "

Scene Title

\n" + + # Header 4 + theHtml.theText = "#### Section Title\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == "

Section Title

\n" + + theHtml.isNovel = False + theHtml.setLinkHeaders(True) + + # Header 1 + theHtml.theText = "# Heading One\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == "

Heading One

\n" + + # Header 2 + theHtml.theText = "## Heading Two\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == "

Heading Two

\n" + + # Header 3 + theHtml.theText = "### Heading Three\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == "

Heading Three

\n" + + # Header 4 + theHtml.theText = "#### Heading Four\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == "

Heading Four

\n" + + # Text + theHtml.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == ( + "

Some nested bold and italic and " + "strikethrough text here

\n" + ) + + # Text w/Hard Break + theHtml.theText = "Line one \nLine two \nLine three\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == ( + "

Line one
Line two
Line three

\n" + ) + + # Synopsis + theHtml.theText = "%synopsis: The synopsis ...\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == "" + + theHtml.setSynopsis(True) + theHtml.theText = "%synopsis: The synopsis ...\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == ( + "

Synopsis: The synopsis ...

\n" + ) + + # Comment + theHtml.theText = "% A comment ...\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == "" + + theHtml.setComments(True) + theHtml.theText = "% A comment ...\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == ( + "

Comment: A comment ...

\n" + ) + + # Keywords + theHtml.theText = "@char: Bod, Jane\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == "" + + theHtml.setKeywords(True) + theHtml.theText = "@char: Bod, Jane\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == ( + "
Characters: " + "Bod, Jane
" + ) + + # Direct Tests + # ============ + + theHtml.isNovel = True + + # Title + theHtml.theTokens = [ + (theHtml.T_TITLE, 1, "A Title", None, theHtml.A_PBB_NO | theHtml.A_CENTRE), + (theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE), + ] + theHtml.doConvert() + assert theHtml.theResult == ( + "

" + "A Title

\n" + ) + + # Separator + theHtml.theTokens = [ + (theHtml.T_SEP, 1, "* * *", None, theHtml.A_CENTRE), + (theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE), + ] + theHtml.doConvert() + assert theHtml.theResult == "

* * *

\n" + + # Skip + theHtml.theTokens = [ + (theHtml.T_SKIP, 1, "", None, theHtml.A_NONE), + (theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE), + ] + theHtml.doConvert() + assert theHtml.theResult == "

 

\n" + + # Styles + # ====== + + theHtml.setLinkHeaders(False) + + # Align Left + theHtml.setStyles(False) + theHtml.theTokens = [ + (theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_LEFT), + ] + theHtml.doConvert() + assert theHtml.theResult == ( + "

A Title

\n" + ) + + theHtml.setStyles(True) + + # Align Left + theHtml.theTokens = [ + (theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_LEFT), + ] + theHtml.doConvert() + assert theHtml.theResult == ( + "

A Title

\n" + ) + + # Align Right + theHtml.theTokens = [ + (theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_RIGHT), + ] + theHtml.doConvert() + assert theHtml.theResult == ( + "

A Title

\n" + ) + + # Align Centre + theHtml.theTokens = [ + (theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_CENTRE), + ] + theHtml.doConvert() + assert theHtml.theResult == ( + "

A Title

\n" + ) + + # Align Justify + theHtml.theTokens = [ + (theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_JUSTIFY), + ] + theHtml.doConvert() + assert theHtml.theResult == ( + "

A Title

\n" + ) + + # Page Break Always + theHtml.theTokens = [ + (theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB | theHtml.A_PBA), + ] + theHtml.doConvert() + assert theHtml.theResult == ( + "

A Title

\n" + ) + + # Page Break Avoid + theHtml.theTokens = [ + (theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_AV | theHtml.A_PBA_AV), + ] + theHtml.doConvert() + assert theHtml.theResult == ( + "

A Title

\n" + ) + + # Page Break ANever + theHtml.theTokens = [ + (theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_NO | theHtml.A_PBA_NO), + ] + theHtml.doConvert() + assert theHtml.theResult == ( + "

A Title

\n" + ) + + # Preview Mode + # ============ + + theHtml.setPreview(True, True) + + # Text (HTML4) + theHtml.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n" + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == ( + "

Some nested bold and italic and " + "strikethrough " + "text here

\n" + ) + +# END Test testCoreToHtml_Convert + +@pytest.mark.core +def testCoreToHtml_Methods(dummyGUI): + """Test all the other methods of the ToHtml class. + """ + theProject = NWProject(dummyGUI) + theHtml = ToHtml(theProject, dummyGUI) + + # Auto-Replace + docText = "Text with & short–dash, long—dash …\n" + theHtml.theText = docText + theHtml.doAutoReplace() + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theResult == ( + "

Text with <brackets> & short–dash, long—dash …

\n" + ) + + # Revert on MD + assert theHtml.theMarkdown == ( + "Text with <brackets> & short–dash, long—dash …\n\n" + ) + theHtml.doPostProcessing() + assert theHtml.theMarkdown == docText + "\n" + + # With Preview, No Revert + theHtml.setPreview(True, True) + theHtml.theText = docText + theHtml.doAutoReplace() + theHtml.tokenizeText() + theHtml.doConvert() + assert theHtml.theMarkdown == ( + "Text with <brackets> & short–dash, long—dash …\n\n" + ) + theHtml.doPostProcessing() + assert theHtml.theMarkdown == ( + "Text with <brackets> & short–dash, long—dash …\n\n" + ) + + # CSS + # === + + assert len(theHtml.getStyleSheet()) > 1 + assert "p {text-align: left;}" in theHtml.getStyleSheet() + assert "p {text-align: justify;}" not in theHtml.getStyleSheet() + + theHtml.setJustify(True) + assert "p {text-align: left;}" not in theHtml.getStyleSheet() + assert "p {text-align: justify;}" in theHtml.getStyleSheet() + + theHtml.setStyles(False) + assert theHtml.getStyleSheet() == [] + +# END Test testCoreToHtml_Methods diff --git a/tests/test_core_tokenizer.py b/tests/test_core_tokenizer.py new file mode 100644 index 00000000..3d0774b5 --- /dev/null +++ b/tests/test_core_tokenizer.py @@ -0,0 +1,644 @@ +# -*- coding: utf-8 -*- +"""novelWriter Tokenizer Class Tester +""" + +import pytest + +from nw.core import NWProject, NWDoc +from nw.core.tokenizer import Tokenizer + +@pytest.mark.core +def testCoreToken_Setters(dummyGUI): + """Test all the setters for the Tokenizer class. + """ + theProject = NWProject(dummyGUI) + theToken = Tokenizer(theProject, dummyGUI) + + # Verify defaults + assert theToken.fmtTitle == "%title%" + assert theToken.fmtChapter == "%title%" + assert theToken.fmtUnNum == "%title%" + assert theToken.fmtScene == "%title%" + assert theToken.fmtSection == "%title%" + assert theToken.hideScene is False + assert theToken.hideSection is False + assert theToken.linkHeaders is False + assert theToken.doBodyText is True + assert theToken.doSynopsis is False + assert theToken.doComments is False + assert theToken.doKeywords is False + assert theToken.doJustify is False + + # Set new values + theToken.setTitleFormat("T: %title%") + theToken.setChapterFormat("C: %title%") + theToken.setUnNumberedFormat("U: %title%") + theToken.setSceneFormat("S: %title%", True) + theToken.setSectionFormat("X: %title%", True) + theToken.setLinkHeaders(True) + theToken.setBodyText(False) + theToken.setSynopsis(True) + theToken.setComments(True) + theToken.setKeywords(True) + theToken.setJustify(True) + + # Check new values + assert theToken.fmtTitle == "T: %title%" + assert theToken.fmtChapter == "C: %title%" + assert theToken.fmtUnNum == "U: %title%" + assert theToken.fmtScene == "S: %title%" + assert theToken.fmtSection == "X: %title%" + assert theToken.hideScene is True + assert theToken.hideSection is True + assert theToken.linkHeaders is True + assert theToken.doBodyText is False + assert theToken.doSynopsis is True + assert theToken.doComments is True + assert theToken.doKeywords is True + assert theToken.doJustify is True + +# END Test testCoreToken_Setters + +@pytest.mark.core +def testCoreToken_TextOps(monkeypatch, nwMinimal, dummyGUI): + """Test handling files and text in the Tokenizer class. + """ + theProject = NWProject(dummyGUI) + theProject.projTree.setSeed(42) + theToken = Tokenizer(theProject, dummyGUI) + + assert theProject.openProject(nwMinimal) + sHandle = "8c659a11cd429" + + # Set some content to work with + + docText = ( + "### Scene Six\n\n" + "This is text with _italic text_, some **bold text**, some ~~deleted text~~, " + "and some **_mixed text_** and **some _nested_ text**.\n\n" + "#### Replace\n\n" + "Also, replace and .\n\n" + ) + docTextR = docText.replace("", "this").replace("", "that") + + nDoc = NWDoc(theProject, dummyGUI) + nDoc.openDocument(sHandle) + nDoc.saveDocument(docText) + nDoc.clearDocument() + + theProject.setAutoReplace({"A": "this", "B": "that"}) + + assert theProject.saveProject() + + # Root heading + assert theToken.addRootHeading("dummy") is False + assert theToken.addRootHeading(sHandle) is False + assert theToken.addRootHeading("7695ce551d265") is True + assert theToken.theMarkdown == "# Notes: Plot\n\n" + + # Set text + assert theToken.setText("dummy") is False + assert theToken.setText(sHandle) is True + assert theToken.theText == docText + + monkeypatch.setattr("nw.constants.nwConst.MAX_DOCSIZE", 100) + assert theToken.setText(sHandle, docText) is True + assert theToken.theText == ( + "# ERROR\n\n" + "Document 'New Scene' is too big (0.00 MB). Skipping.\n\n" + ) + monkeypatch.undo() + + assert theToken.setText(sHandle, docText) is True + assert theToken.theText == docText + + assert theToken.isNone is False + assert theToken.isTitle is False + assert theToken.isBook is False + assert theToken.isPage is False + assert theToken.isPart is False + assert theToken.isUnNum is False + assert theToken.isChap is False + assert theToken.isScene is True + assert theToken.isNote is False + assert theToken.isNovel is True + + # Auto replace + theToken.doAutoReplace() + assert theToken.theText == docTextR + + # Access + assert theToken.getResult() is None + assert theToken.getResultSize() == 0 + theToken.theResult = "" + assert theToken.getResultSize() == 0 + + # Post Processing + theToken.theResult = r"This is text with escapes: \** \~~ \__" + theToken.doPostProcessing() + assert theToken.theResult == "This is text with escapes: ** ~~ __" + +# END Test testCoreToken_TextOps + +@pytest.mark.core +def testCoreToken_Tokenize(dummyGUI): + """Test the tokenization of the Tokenizer class. + """ + theProject = NWProject(dummyGUI) + theToken = Tokenizer(theProject, dummyGUI) + + # Header 1 + theToken.theText = "# Novel Title\n" + theToken.tokenizeText() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD1, 1, "Novel Title", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + assert theToken.theMarkdown == "# Novel Title\n\n" + + # Header 2 + theToken.theText = "## Chapter One\n" + theToken.tokenizeText() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD2, 1, "Chapter One", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + assert theToken.theMarkdown == "## Chapter One\n\n" + + # Header 3 + theToken.theText = "### Scene One\n" + theToken.tokenizeText() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD3, 1, "Scene One", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + assert theToken.theMarkdown == "### Scene One\n\n" + + # Header 4 + theToken.theText = "#### A Section\n" + theToken.tokenizeText() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD4, 1, "A Section", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + assert theToken.theMarkdown == "#### A Section\n\n" + + # Comment + theToken.theText = "% A comment\n" + theToken.tokenizeText() + assert theToken.theTokens == [ + (Tokenizer.T_COMMENT, 1, "A comment", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + assert theToken.theMarkdown == "\n" + + theToken.setComments(True) + theToken.tokenizeText() + assert theToken.theMarkdown == "% A comment\n\n" + + # Symopsis + theToken.theText = "%synopsis: The synopsis\n" + theToken.tokenizeText() + assert theToken.theTokens == [ + (Tokenizer.T_SYNOPSIS, 1, "The synopsis", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + theToken.theText = "% synopsis: The synopsis\n" + theToken.tokenizeText() + assert theToken.theTokens == [ + (Tokenizer.T_SYNOPSIS, 1, "The synopsis", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + assert theToken.theMarkdown == "\n" + + theToken.setSynopsis(True) + theToken.tokenizeText() + assert theToken.theMarkdown == "% synopsis: The synopsis\n\n" + + # Keyword + theToken.theText = "@char: Bod\n" + theToken.tokenizeText() + assert theToken.theTokens == [ + (Tokenizer.T_KEYWORD, 1, "char: Bod", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + assert theToken.theMarkdown == "\n" + + theToken.setKeywords(True) + theToken.tokenizeText() + assert theToken.theMarkdown == "@char: Bod\n\n" + + # Text + theToken.theText = "Some plain text\non two lines\n\n\n" + theToken.tokenizeText() + assert theToken.theTokens == [ + (Tokenizer.T_TEXT, 1, "Some plain text", [], Tokenizer.A_NONE), + (Tokenizer.T_TEXT, 2, "on two lines", [], Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), + ] + assert theToken.theMarkdown == "Some plain text\non two lines\n\n\n\n" + + theToken.setBodyText(False) + theToken.tokenizeText() + assert theToken.theTokens == [ + (Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE), + ] + assert theToken.theMarkdown == "\n\n\n" + theToken.setBodyText(True) + + # Text Emphasis + theToken.theText = "Some **bolded text** on this lines\n" + theToken.tokenizeText() + assert theToken.theTokens == [ + ( + Tokenizer.T_TEXT, 1, + "Some **bolded text** on this lines", + [ + [5, 2, Tokenizer.FMT_B_B], + [18, 2, Tokenizer.FMT_B_E], + ], + Tokenizer.A_NONE + ), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + assert theToken.theMarkdown == "Some **bolded text** on this lines\n\n" + + theToken.theText = "Some _italic text_ on this lines\n" + theToken.tokenizeText() + assert theToken.theTokens == [ + ( + Tokenizer.T_TEXT, 1, + "Some _italic text_ on this lines", + [ + [5, 1, Tokenizer.FMT_I_B], + [17, 1, Tokenizer.FMT_I_E], + ], + Tokenizer.A_NONE + ), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + assert theToken.theMarkdown == "Some _italic text_ on this lines\n\n" + + theToken.theText = "Some **_bold italic text_** on this lines\n" + theToken.tokenizeText() + assert theToken.theTokens == [ + ( + Tokenizer.T_TEXT, 1, + "Some **_bold italic text_** on this lines", + [ + [5, 2, Tokenizer.FMT_B_B], + [7, 1, Tokenizer.FMT_I_B], + [24, 1, Tokenizer.FMT_I_E], + [25, 2, Tokenizer.FMT_B_E], + ], + Tokenizer.A_NONE + ), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + assert theToken.theMarkdown == "Some **_bold italic text_** on this lines\n\n" + + theToken.theText = "Some ~~strikethrough text~~ on this lines\n" + theToken.tokenizeText() + assert theToken.theTokens == [ + ( + Tokenizer.T_TEXT, 1, + "Some ~~strikethrough text~~ on this lines", + [ + [5, 2, Tokenizer.FMT_D_B], + [25, 2, Tokenizer.FMT_D_E], + ], + Tokenizer.A_NONE + ), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + assert theToken.theMarkdown == "Some ~~strikethrough text~~ on this lines\n\n" + + theToken.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n" + theToken.tokenizeText() + assert theToken.theTokens == [ + ( + Tokenizer.T_TEXT, 1, + "Some **nested bold and _italic_ and ~~strikethrough~~ text** here", + [ + [5, 2, Tokenizer.FMT_B_B], + [23, 1, Tokenizer.FMT_I_B], + [30, 1, Tokenizer.FMT_I_E], + [36, 2, Tokenizer.FMT_D_B], + [51, 2, Tokenizer.FMT_D_E], + [58, 2, Tokenizer.FMT_B_E], + ], + Tokenizer.A_NONE + ), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + assert theToken.theMarkdown == ( + "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n" + ) + + # Check the markdown function as well + assert theToken.getFilteredMarkdown() == ( + "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n" + ) + +# END Test testCoreToken_Tokenize + +@pytest.mark.core +def testCoreToken_Headers(dummyGUI): + """Test the header and page parser of the Tokenizer class. + """ + theProject = NWProject(dummyGUI) + theToken = Tokenizer(theProject, dummyGUI) + + # Nothing + theToken.theText = "Some text ...\n" + assert theToken.doHeaders() is True + theToken.isNone = True + assert theToken.doHeaders() is False + theToken.isNone = False + assert theToken.doHeaders() is True + theToken.isNote = True + assert theToken.doHeaders() is False + theToken.isNote = False + + ## + # Novel + ## + + theToken.isNovel = True + + # Titles + # ====== + + # H1: Title + theToken.theText = "# Novel Title\n" + theToken.setTitleFormat(r"T: %title%") + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD1, 1, "T: Novel Title", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # Chapters + # ======== + + # H2: Chapter + theToken.theText = "## Chapter One\n" + theToken.setChapterFormat(r"C: %title%") + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD2, 1, "C: Chapter One", None, Tokenizer.A_PBB), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # H2: Unnumbered Chapter + theToken.theText = "## Chapter One\n" + theToken.setUnNumberedFormat(r"U: %title%") + theToken.isUnNum = True + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD2, 1, "U: Chapter One", None, Tokenizer.A_PBB), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # H2: Unnumbered Chapter with Star + theToken.theText = "## *Prologue\n" + theToken.setUnNumberedFormat(r"U: %title%") + theToken.isUnNum = False + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD2, 1, "U: Prologue", None, Tokenizer.A_PBB), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # H2: Chapter Word Number + theToken.theText = "## Chapter\n" + theToken.setChapterFormat(r"Chapter %chw%") + theToken.numChapter = 0 + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD2, 1, "Chapter One", None, Tokenizer.A_PBB), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # H2: Chapter Roman Number Upper Case + theToken.theText = "## Chapter\n" + theToken.setChapterFormat(r"Chapter %chI%") + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD2, 1, "Chapter II", None, Tokenizer.A_PBB), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # H2: Chapter Roman Number Lower Case + theToken.theText = "## Chapter\n" + theToken.setChapterFormat(r"Chapter %chi%") + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD2, 1, "Chapter iii", None, Tokenizer.A_PBB), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # Scenes + # ====== + + # H3: Scene w/Title + theToken.theText = "### Scene One\n" + theToken.setSceneFormat(r"S: %title%", False) + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD3, 1, "S: Scene One", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # H3: Scene Hidden wo/Format + theToken.theText = "### Scene One\n" + theToken.setSceneFormat(r"", True) + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # H3: Scene wo/Format, first + theToken.theText = "### Scene One\n" + theToken.setSceneFormat(r"", False) + theToken.firstScene = True + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # H3: Scene wo/Format, not first + theToken.theText = "### Scene One\n" + theToken.setSceneFormat(r"", False) + theToken.firstScene = False + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # H3: Scene Separator, first + theToken.theText = "### Scene One\n" + theToken.setSceneFormat(r"* * *", False) + theToken.firstScene = True + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # H3: Scene Separator, not first + theToken.theText = "### Scene One\n" + theToken.setSceneFormat(r"* * *", False) + theToken.firstScene = False + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_SEP, 1, "* * *", None, Tokenizer.A_CENTRE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # H3: Scene w/Absolute Number + theToken.theText = "### A Scene\n" + theToken.setSceneFormat(r"Scene %sca%", False) + theToken.numAbsScene = 0 + theToken.numChScene = 0 + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD3, 1, "Scene 1", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # H3: Scene w/Chapter Number + theToken.theText = "### A Scene\n" + theToken.setSceneFormat(r"Scene %ch%.%sc%", False) + theToken.numAbsScene = 0 + theToken.numChScene = 1 + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD3, 1, "Scene 3.2", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # Sections + # ======== + + # H4: Section Hidden wo/Format + theToken.theText = "#### A Section\n" + theToken.setSectionFormat(r"", True) + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # H4: Section Visible wo/Format + theToken.theText = "#### A Section\n" + theToken.setSectionFormat(r"", False) + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # H4: Section w/Format + theToken.theText = "#### A Section\n" + theToken.setSectionFormat(r"X: %title%", False) + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD4, 1, "X: A Section", None, Tokenizer.A_NONE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # H4: Section Separator + theToken.theText = "#### A Section\n" + theToken.setSectionFormat(r"* * *", False) + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_SEP, 1, "* * *", None, Tokenizer.A_CENTRE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE), + ] + + # Check the first scene detector + assert theToken.firstScene is False + theToken.firstScene = True + assert theToken.firstScene is True + theToken.theText = "Some text ...\n" + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.firstScene is False + + ## + # Title or Partition + ## + + theToken.isNovel = False + + # H1: Title + theToken.theText = "# Novel Title\n" + theToken.tokenizeText() + theToken.isTitle = True + theToken.isPart = False + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_TITLE, 1, "Novel Title", None, Tokenizer.A_PBB_NO | Tokenizer.A_CENTRE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_PBA | Tokenizer.A_CENTRE), + ] + + # H1: Partition + theToken.theText = "# Partition Title\n" + theToken.setTitleFormat(r"T: %title%") + theToken.tokenizeText() + theToken.isTitle = False + theToken.isPart = True + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_HEAD1, 1, "Partition Title", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE), + (Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_PBA | Tokenizer.A_CENTRE), + ] + + ## + # Page + ## + + theToken.isNovel = False + theToken.isTitle = False + theToken.isPart = False + theToken.isPage = True + + # Some Page Text + theToken.theText = "Page text\n\nMore text\n" + theToken.tokenizeText() + theToken.doHeaders() + assert theToken.theTokens == [ + (Tokenizer.T_TEXT, 1, "Page text", [], Tokenizer.A_PBB | Tokenizer.A_LEFT), + (Tokenizer.T_EMPTY, 2, "", None, Tokenizer.A_LEFT), + (Tokenizer.T_TEXT, 3, "More text", [], Tokenizer.A_LEFT), + (Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_LEFT), + ] + +# END Test testCoreToken_Headers diff --git a/tests/test_tools.py b/tests/test_core_tools.py similarity index 85% rename from tests/test_tools.py rename to tests/test_core_tools.py index 4832aadb..ceb146a4 100644 --- a/tests/test_tools.py +++ b/tests/test_core_tools.py @@ -7,7 +7,7 @@ import pytest from nw.core.tools import countWords, numberToRoman, numberToWord @pytest.mark.core -def testCountWords(): +def testCoreTools_CountWords(): """Test the word counter and the exclusion filers. """ testText = ( @@ -26,15 +26,46 @@ def testCountWords(): "\n" "\n" "The third paragraph.\n" + "\n" + "Dashes\u2013and even longer\u2014dashes." ) cC, wC, pC = countWords(testText) - assert cC == 108 - assert wC == 17 - assert pC == 3 + assert cC == 138 + assert wC == 22 + assert pC == 4 + +# END Test testCoreTools_CountWords @pytest.mark.core -def testNumberWords(): +def testCoreTools_RomanNumbers(): + """Test conversion of integers to Roman numbers. + """ + assert numberToRoman(None, False) == "NAN" + assert numberToRoman(0, False) == "OOR" + assert numberToRoman(1, False) == "I" + assert numberToRoman(2, False) == "II" + assert numberToRoman(3, False) == "III" + assert numberToRoman(4, False) == "IV" + assert numberToRoman(5, False) == "V" + assert numberToRoman(6, False) == "VI" + assert numberToRoman(7, False) == "VII" + assert numberToRoman(8, False) == "VIII" + assert numberToRoman(9, False) == "IX" + assert numberToRoman(10, False) == "X" + assert numberToRoman(14, False) == "XIV" + assert numberToRoman(42, False) == "XLII" + assert numberToRoman(99, False) == "XCIX" + assert numberToRoman(142, False) == "CXLII" + assert numberToRoman(542, False) == "DXLII" + assert numberToRoman(999, False) == "CMXCIX" + assert numberToRoman(2010, False) == "MMX" + assert numberToRoman(999, True) == "cmxcix" + +# END Test testCoreTools_RomanNumbers + +@pytest.mark.core +def testCoreTools_NumberWords(): """Test the conversion of integer to English words. """ assert numberToWord(0, "en") == "Zero" @@ -70,27 +101,9 @@ def testNumberWords(): assert numberToWord(2, "foo") == "Two" assert numberToWord(3, "foo") == "Three" -@pytest.mark.core -def testRomanNumbers(): - """Test conversion of integers to Roman numbers. - """ - assert numberToRoman(None, False) == "NAN" - assert numberToRoman(0, False) == "OOR" - assert numberToRoman(1, False) == "I" - assert numberToRoman(2, False) == "II" - assert numberToRoman(3, False) == "III" - assert numberToRoman(4, False) == "IV" - assert numberToRoman(5, False) == "V" - assert numberToRoman(6, False) == "VI" - assert numberToRoman(7, False) == "VII" - assert numberToRoman(8, False) == "VIII" - assert numberToRoman(9, False) == "IX" - assert numberToRoman(10, False) == "X" - assert numberToRoman(14, False) == "XIV" - assert numberToRoman(42, False) == "XLII" - assert numberToRoman(99, False) == "XCIX" - assert numberToRoman(142, False) == "CXLII" - assert numberToRoman(542, False) == "DXLII" - assert numberToRoman(999, False) == "CMXCIX" - assert numberToRoman(2010, False) == "MMX" - assert numberToRoman(999, True) == "cmxcix" + # Test out of range values + assert numberToWord(12345, "en") == "[Out of Range]" + assert numberToWord(-2345, "en") == "[Negative]" + assert numberToWord("234", "en") == "[NaN]" + +# END Test testCoreTools_NumberWords diff --git a/tests/test_core_tree.py b/tests/test_core_tree.py new file mode 100644 index 00000000..29c1ca95 --- /dev/null +++ b/tests/test_core_tree.py @@ -0,0 +1,433 @@ +# -*- coding: utf-8 -*- +"""novelWriter NWTree Class Tester +""" + +import os +import pytest + +from lxml import etree + +from nw.core.project import NWProject, NWItem, NWTree +from nw.constants import nwItemClass, nwItemType, nwItemLayout, nwFiles + +@pytest.fixture(scope="function") +def dummyItems(dummyGUI): + """Create a list of dummy items. + """ + theProject = NWProject(dummyGUI) + + itemA = NWItem(theProject) + itemA.itemName = "Novel" + itemA.itemType = nwItemType.ROOT + itemA.itemClass = nwItemClass.NOVEL + itemA.isExpanded = True + + itemB = NWItem(theProject) + itemB.itemName = "Act One" + itemB.itemType = nwItemType.FOLDER + itemB.itemClass = nwItemClass.NOVEL + itemB.isExpanded = True + + itemC = NWItem(theProject) + itemC.itemName = "Chapter One" + itemC.itemType = nwItemType.FILE + itemC.itemClass = nwItemClass.NOVEL + itemC.itemLayout = nwItemLayout.CHAPTER + itemC.charCount = 300 + itemC.wordCount = 50 + itemC.paraCount = 2 + + itemD = NWItem(theProject) + itemD.itemName = "Scene One" + itemD.itemType = nwItemType.FILE + itemD.itemClass = nwItemClass.NOVEL + itemD.itemLayout = nwItemLayout.SCENE + itemD.charCount = 3000 + itemD.wordCount = 500 + itemD.paraCount = 20 + + itemE = NWItem(theProject) + itemE.itemName = "Outtakes" + itemE.itemType = nwItemType.ROOT + itemE.itemClass = nwItemClass.ARCHIVE + itemE.isExpanded = False + + itemF = NWItem(theProject) + itemF.itemName = "Trash" + itemF.itemType = nwItemType.TRASH + itemF.itemClass = nwItemClass.TRASH + itemF.isExpanded = False + + itemG = NWItem(theProject) + itemG.itemName = "Characters" + itemG.itemType = nwItemType.ROOT + itemG.itemClass = nwItemClass.CHARACTER + itemG.isExpanded = True + + itemH = NWItem(theProject) + itemH.itemName = "Jane Doe" + itemH.itemType = nwItemType.FILE + itemH.itemClass = nwItemClass.CHARACTER + itemH.itemLayout = nwItemLayout.NOTE + itemH.charCount = 2000 + itemH.wordCount = 400 + itemH.paraCount = 16 + + theItems = [ + ("a000000000001", None, itemA), + ("b000000000001", "a000000000001", itemB), + ("c000000000001", "b000000000001", itemC), + ("c000000000002", "b000000000001", itemD), + ("a000000000002", None, itemE), + ("a000000000003", None, itemF), + ("a000000000004", None, itemG), + ("b000000000002", "a000000000002", itemH), + ] + + return theItems + +@pytest.mark.core +def testCoreTree_BuildTree(dummyGUI, dummyItems): + """Test building a project tree from a list of items. + """ + theProject = NWProject(dummyGUI) + theTree = NWTree(theProject) + + theTree.setSeed(42) + assert theTree._handleSeed == 42 + + # Check that tree is empty (calls NWTree.__bool__) + assert not theTree + + # Check for archive and trash folders + assert theTree.trashRoot() is None + assert theTree.archiveRoot() is None + assert not theTree.isTrashRoot("a000000000003") + + aHandles = [] + for tHandle, pHande, nwItem in dummyItems: + aHandles.append(tHandle) + assert theTree.append(tHandle, pHande, nwItem) + + assert theTree._treeChanged + + # Check that tree is not empty (calls __bool__) + assert theTree + + # Check the number of elements (calls __len__) + assert len(theTree) == len(dummyItems) + + # Check that we have the correct handles + assert theTree.handles() == aHandles + + # Check by iterator (calls __iter__, __next__ and __getitem__) + for theItem, theHandle in zip(theTree, aHandles): + assert theItem.itemHandle == theHandle + + # Check that we have the correct archive and trash folders + assert theTree.trashRoot() == "a000000000003" + assert theTree.archiveRoot() == "a000000000002" + assert theTree.isTrashRoot("a000000000003") + + # Try to add another trash folder + itemT = NWItem(theProject) + itemT.itemName = "Trash" + itemT.itemType = nwItemType.TRASH + itemT.itemClass = nwItemClass.TRASH + itemT.isExpanded = False + + assert not theTree.append("1234567890abc", None, itemT) + assert len(theTree) == len(dummyItems) + + # Generate handle automatically + itemT = NWItem(theProject) + itemT.itemName = "New File" + itemT.itemType = nwItemType.FILE + itemT.itemClass = nwItemClass.NOVEL + itemT.itemLayout = nwItemLayout.SCENE + + assert theTree.append(None, None, itemT) + assert len(theTree) == len(dummyItems) + 1 + + theList = theTree.handles() + assert theList[-1] == "73475cb40a568" + + # Try to add existing handle + assert not theTree.append("73475cb40a568", None, itemT) + assert len(theTree) == len(dummyItems) + 1 + + # Delete a non-existing item + del theTree["dummy"] + assert len(theTree) == len(dummyItems) + 1 + + # Delete the last item + del theTree["73475cb40a568"] + assert len(theTree) == len(dummyItems) + assert "73475cb40a568" not in theTree + + # Delete the Novel, Archive and Trash folders + del theTree["a000000000001"] + assert len(theTree) == len(dummyItems) - 1 + assert "a000000000001" not in theTree + + del theTree["a000000000002"] + assert len(theTree) == len(dummyItems) - 2 + assert "a000000000002" not in theTree + assert theTree.archiveRoot() is None + + del theTree["a000000000003"] + assert len(theTree) == len(dummyItems) - 3 + assert "a000000000003" not in theTree + assert theTree.trashRoot() is None + +# END Test testCoreTree_BuildTree + +@pytest.mark.core +def testCoreTree_Methods(dummyGUI, dummyItems): + """Test building a project tree from a list of items. + """ + theProject = NWProject(dummyGUI) + theTree = NWTree(theProject) + + for tHandle, pHande, nwItem in dummyItems: + theTree.append(tHandle, pHande, nwItem) + + assert len(theTree) == len(dummyItems) + + # Root item lookup + theTree._treeRoots.append("dummy") + assert theTree.findRoot(nwItemClass.WORLD) is None + assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001" + assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004" + + # Check for root uniqueness + assert theTree.checkRootUnique(nwItemClass.CUSTOM) + assert theTree.checkRootUnique(nwItemClass.WORLD) + assert not theTree.checkRootUnique(nwItemClass.NOVEL) + assert not theTree.checkRootUnique(nwItemClass.CHARACTER) + + # Find root item of child item + assert theTree.getRootItem("b000000000001").itemHandle == "a000000000001" + assert theTree.getRootItem("c000000000001").itemHandle == "a000000000001" + assert theTree.getRootItem("c000000000002").itemHandle == "a000000000001" + assert theTree.getRootItem("dummy") is None + + # Get item path + assert theTree.getItemPath("dummy") == [] + assert theTree.getItemPath("c000000000001") == [ + "c000000000001", "b000000000001", "a000000000001" + ] + + # Break the folder parent handle + theTree["b000000000001"].itemParent = "dummy" + assert theTree.getItemPath("c000000000001") == [ + "c000000000001", "b000000000001" + ] + + theTree["b000000000001"].itemParent = "a000000000001" + assert theTree.getItemPath("c000000000001") == [ + "c000000000001", "b000000000001", "a000000000001" + ] + + # Change file layout + assert not theTree.setFileItemLayout("dummy", nwItemLayout.UNNUMBERED) + assert not theTree.setFileItemLayout("b000000000001", nwItemLayout.UNNUMBERED) + assert not theTree.setFileItemLayout("c000000000001", "stuff") + assert theTree.setFileItemLayout("c000000000001", nwItemLayout.UNNUMBERED) + assert theTree["c000000000001"].itemLayout == nwItemLayout.UNNUMBERED + +# END Test testCoreTree_Methods + +@pytest.mark.core +def testCoreTree_MakeHandles(monkeypatch, dummyGUI): + """Test generating item handles. + """ + theProject = NWProject(dummyGUI) + theTree = NWTree(theProject) + + theTree.setSeed(42) + + tHandle = theTree._makeHandle() + assert tHandle == "73475cb40a568" + + # Add the next in line to the project to foprce duplicate + theTree._projTree["44cb730c42048"] = None + tHandle = theTree._makeHandle() + assert tHandle == "71ee45a3c0db9" + + # Fix the time() function and force a handle collission + theTree.setSeed(None) + monkeypatch.setattr("nw.core.tree.time", lambda: 123.4) + + tHandle = theTree._makeHandle() + theTree._projTree[tHandle] = None + assert tHandle == "5f466d7afa48b" + + tHandle = theTree._makeHandle() + theTree._projTree[tHandle] = None + assert tHandle == "a79acf4c634a7" + + monkeypatch.undo() + +# END Test testCoreTree_MakeHandles + +@pytest.mark.core +def testCoreTree_Stats(dummyGUI, dummyItems): + """Test project stats methods. + """ + theProject = NWProject(dummyGUI) + theTree = NWTree(theProject) + + for tHandle, pHande, nwItem in dummyItems: + theTree.append(tHandle, pHande, nwItem) + + assert len(theTree) == len(dummyItems) + theTree._treeOrder.append("dummy") + + # Count Words + novelWords, noteWords = theTree.sumWords() + assert novelWords == 550 + assert noteWords == 400 + + # Count types + nRoot, nFolder, nFile = theTree.countTypes() + assert nRoot == 3 + assert nFolder == 1 + assert nFile == 3 + +# END Test testCoreTree_Stats + +@pytest.mark.core +def testCoreTree_Reorder(dummyGUI, dummyItems): + """Test changing tree order. + """ + theProject = NWProject(dummyGUI) + theTree = NWTree(theProject) + + aHandle = [] + for tHandle, pHande, nwItem in dummyItems: + aHandle.append(tHandle) + theTree.append(tHandle, pHande, nwItem) + + assert len(theTree) == len(dummyItems) + + bHandle = aHandle.copy() + bHandle[2], bHandle[3] = bHandle[3], bHandle[2] + assert aHandle != bHandle + + assert theTree.handles() == aHandle + theTree.setOrder(bHandle) + assert theTree.handles() == bHandle + + theTree.setOrder(bHandle + ["dummy"]) + assert theTree.handles() == bHandle + + theTree._treeOrder.append("dummy") + theTree.setOrder(bHandle) + assert theTree.handles() == bHandle + +# END Test testCoreTree_Reorder + +@pytest.mark.core +def testCoreTree_XMLPackUnpack(dummyGUI, dummyItems): + """Test changing tree order. + """ + theProject = NWProject(dummyGUI) + theTree = NWTree(theProject) + + for tHandle, pHande, nwItem in dummyItems: + theTree.append(tHandle, pHande, nwItem) + + assert len(theTree) == len(dummyItems) + + nwXML = etree.Element("novelWriterXML") + theTree.packXML(nwXML) + assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == ( + b"" + b"" + b"" + b"NovelROOTNOVELNone" + b"True" + b"" + b"Act OneFOLDERNOVELNone" + b"True" + b"" + b"Chapter OneFILENOVELNone" + b"TrueCHAPTER300" + b"5020" + b"" + b"Scene OneFILENOVELNone" + b"TrueSCENE3000" + b"500200" + b"" + b"OuttakesROOTARCHIVENone" + b"False" + b"" + b"TrashTRASHTRASHNone" + b"False" + b"" + b"CharactersROOTCHARACTERNone" + b"True" + b"" + b"Jane DoeFILECHARACTERNone" + b"TrueNOTE2000" + b"400160" + b"" + ) + + theTree.clear() + assert len(theTree) == 0 + assert not theTree.unpackXML(nwXML) + assert theTree.unpackXML(nwXML[0]) + assert len(theTree) == len(dummyItems) + +# END Test testCoreTree_XMLPackUnpack + +@pytest.mark.core +def testCoreTree_ToCFile(monkeypatch, dummyGUI, dummyItems, tmpDir): + """Test writing the ToC.txt file. + """ + theProject = NWProject(dummyGUI) + theTree = NWTree(theProject) + + for tHandle, pHande, nwItem in dummyItems: + theTree.append(tHandle, pHande, nwItem) + + assert len(theTree) == len(dummyItems) + theTree._treeOrder.append("dummy") + + def dummyIsFile(fileName): + """Return True for items that are files in novelWriter and + should thus also be files in the project folder structure. + """ + dItem = theTree[fileName[8:21]] + assert dItem is not None + return dItem.itemType == nwItemType.FILE + + monkeypatch.setattr("os.path.isfile", dummyIsFile) + + theProject.projContent = "content" + theProject.projPath = None + assert not theTree.writeToCFile() + + theProject.projPath = tmpDir + assert theTree.writeToCFile() + + pathA = os.path.join("content", "c000000000001.nwd") + pathB = os.path.join("content", "c000000000002.nwd") + pathC = os.path.join("content", "b000000000002.nwd") + + with open(os.path.join(tmpDir, nwFiles.TOC_TXT), mode="r", encoding="utf8") as inFile: + assert inFile.read() == ( + "\n" + "Table of Contents\n" + "=================\n" + "\n" + "File Name Class Layout Document Label\n" + "-------------------------------------------------------------\n" + f"{pathA} NOVEL CHAPTER Chapter One\n" + f"{pathB} NOVEL SCENE Scene One\n" + f"{pathC} CHARACTER NOTE Jane Doe\n" + ) + +# END Test testCoreTree_ToCFile diff --git a/tests/test_error.py b/tests/test_error.py index 179efa68..192fb6cb 100644 --- a/tests/test_error.py +++ b/tests/test_error.py @@ -10,9 +10,9 @@ from PyQt5.QtWidgets import qApp from nw.error import NWErrorMessage, exceptionHandler @pytest.mark.error -def testErrorDialog(qtbot, nwFuncTemp, nwTemp): +def testErrorDialog(qtbot, fncDir, tmpDir): qApp.closeAllWindows() - nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) diff --git a/tests/test_dialogs.py b/tests/test_gui_dialogs.py similarity index 91% rename from tests/test_dialogs.py rename to tests/test_gui_dialogs.py index 4c1f1885..96f64da4 100644 --- a/tests/test_dialogs.py +++ b/tests/test_gui_dialogs.py @@ -9,7 +9,7 @@ import os import sys from shutil import copyfile -from nwtools import cmpFiles, getGuiItem +from tools import cmpFiles, getGuiItem from PyQt5.QtCore import Qt, QItemSelectionModel from PyQt5.QtWidgets import ( @@ -30,8 +30,8 @@ typeDelay = 1 stepDelay = 20 @pytest.mark.gui -def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): - nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp]) +def testProjectSettings(qtbot, monkeypatch, yesToAll, fncDir, nwTempGUI, refDir, tmpDir): + nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -43,8 +43,8 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR # Create new project nwGUI.theProject.projTree.setSeed(42) - assert nwGUI.newProject({"projPath": nwFuncTemp}) - nwGUI.mainConf.backupPath = nwFuncTemp + assert nwGUI.newProject({"projPath": fncDir}) + nwGUI.mainConf.backupPath = fncDir # Get the dialog object monkeypatch.setattr(GuiProjectSettings, "exec_", lambda *args: None) @@ -135,9 +135,9 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR qtbot.wait(stepDelay) # Check the files - projFile = os.path.join(nwFuncTemp, "nwProject.nwx") + projFile = os.path.join(fncDir, "nwProject.nwx") testFile = os.path.join(nwTempGUI, "2_nwProject.nwx") - refFile = os.path.join(nwRef, "gui", "2_nwProject.nwx") + refFile = os.path.join(refDir, "gui", "2_nwProject.nwx") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [2, 8, 9, 10]) @@ -145,8 +145,8 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR nwGUI.closeMain() @pytest.mark.gui -def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, nwTemp): - nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp]) +def testItemEditor(qtbot, yesToAll, monkeypatch, fncDir, nwTempGUI, refDir, tmpDir): + nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -154,7 +154,7 @@ def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, n # Create new, save, open project nwGUI.theProject.projTree.setSeed(42) - assert nwGUI.newProject({"projPath": nwFuncTemp}) + assert nwGUI.newProject({"projPath": fncDir}) assert nwGUI.openDocument("0e17daca5f3e1") assert nwGUI.treeView.setSelectedHandle("0e17daca5f3e1", doScroll=True) @@ -208,9 +208,9 @@ def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, n qtbot.wait(stepDelay) # Check the files - projFile = os.path.join(nwFuncTemp, "nwProject.nwx") + projFile = os.path.join(fncDir, "nwProject.nwx") testFile = os.path.join(nwTempGUI, "3_nwProject.nwx") - refFile = os.path.join(nwRef, "gui", "3_nwProject.nwx") + refFile = os.path.join(refDir, "gui", "3_nwProject.nwx") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) @@ -218,8 +218,8 @@ def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, n nwGUI.closeMain() @pytest.mark.gui -def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): - nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp]) +def testWritingStatsExport(qtbot, monkeypatch, yesToAll, fncDir, tmpDir): + nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -227,13 +227,13 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): # Create new, save, close project nwGUI.theProject.projTree.setSeed(42) - assert nwGUI.newProject({"projPath": nwFuncTemp}) + assert nwGUI.newProject({"projPath": fncDir}) qtbot.wait(200) assert nwGUI.saveProject() assert nwGUI.closeProject() qtbot.wait(stepDelay) - sessFile = os.path.join(nwFuncTemp, "meta", nwFiles.SESS_STATS) + sessFile = os.path.join(fncDir, "meta", nwFiles.SESS_STATS) with open(sessFile, mode="w+", encoding="utf-8") as outFile: outFile.write( "# Start Time End Time Novel Notes\n" @@ -244,10 +244,10 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): ) # Open again, and check the stats - assert nwGUI.openProject(nwFuncTemp) + assert nwGUI.openProject(fncDir) qtbot.wait(stepDelay) - nwGUI.mainConf.lastPath = nwFuncTemp + nwGUI.mainConf.lastPath = fncDir nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger) qtbot.waitUntil(lambda: getGuiItem("GuiWritingStats") is not None, timeout=1000) @@ -264,7 +264,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert sessLog._saveData(sessLog.FMT_JSON) qtbot.wait(100) - jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") + jsonStats = os.path.join(fncDir, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.load(inFile) @@ -282,7 +282,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert sessLog._saveData(sessLog.FMT_JSON) qtbot.wait(stepDelay) - jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") + jsonStats = os.path.join(fncDir, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) @@ -299,7 +299,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert sessLog._saveData(sessLog.FMT_JSON) qtbot.wait(stepDelay) - jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") + jsonStats = os.path.join(fncDir, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.load(inFile) @@ -316,7 +316,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert sessLog._saveData(sessLog.FMT_JSON) qtbot.wait(stepDelay) - jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") + jsonStats = os.path.join(fncDir, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.load(inFile) @@ -329,7 +329,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert sessLog._saveData(sessLog.FMT_JSON) qtbot.wait(stepDelay) - jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") + jsonStats = os.path.join(fncDir, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.load(inFile) @@ -341,7 +341,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): assert sessLog._saveData(sessLog.FMT_JSON) qtbot.wait(stepDelay) - jsonStats = os.path.join(nwFuncTemp, "sessionStats.json") + jsonStats = os.path.join(fncDir, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.load(inFile) @@ -357,8 +357,8 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp): nwGUI.closeMain() @pytest.mark.gui -def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp): - nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]) +def testAboutBox(qtbot, monkeypatch, fncDir, tmpDir): + nwGUI = nw.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -397,9 +397,9 @@ def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp): nwGUI.closeMain() @pytest.mark.gui -def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp): +def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, refDir, tmpDir): - nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -428,13 +428,13 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp): projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd") testFile = os.path.join(nwTempBuild, "1_LoremIpsum.nwd") - refFile = os.path.join(nwRef, "build", "1_LoremIpsum.nwd") + refFile = os.path.join(refDir, "build", "1_LoremIpsum.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm") testFile = os.path.join(nwTempBuild, "1_LoremIpsum.htm") - refFile = os.path.join(nwRef, "build", "1_LoremIpsum.htm") + refFile = os.path.join(refDir, "build", "1_LoremIpsum.htm") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) @@ -467,13 +467,13 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp): projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd") testFile = os.path.join(nwTempBuild, "2_LoremIpsum.nwd") - refFile = os.path.join(nwRef, "build", "2_LoremIpsum.nwd") + refFile = os.path.join(refDir, "build", "2_LoremIpsum.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm") testFile = os.path.join(nwTempBuild, "2_LoremIpsum.htm") - refFile = os.path.join(nwRef, "build", "2_LoremIpsum.htm") + refFile = os.path.join(refDir, "build", "2_LoremIpsum.htm") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) @@ -487,14 +487,14 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp): assert nwBuild._saveDocument(nwBuild.FMT_NWD) projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd") testFile = os.path.join(nwTempBuild, "3_LoremIpsum.nwd") - refFile = os.path.join(nwRef, "build", "3_LoremIpsum.nwd") + refFile = os.path.join(refDir, "build", "3_LoremIpsum.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) assert nwBuild._saveDocument(nwBuild.FMT_HTM) projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm") testFile = os.path.join(nwTempBuild, "3_LoremIpsum.htm") - refFile = os.path.join(nwRef, "build", "3_LoremIpsum.htm") + refFile = os.path.join(refDir, "build", "3_LoremIpsum.htm") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) @@ -521,14 +521,14 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp): assert nwBuild._saveDocument(nwBuild.FMT_NWD) projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd") testFile = os.path.join(nwTempBuild, "4_LoremIpsum.nwd") - refFile = os.path.join(nwRef, "build", "4_LoremIpsum.nwd") + refFile = os.path.join(refDir, "build", "4_LoremIpsum.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) assert nwBuild._saveDocument(nwBuild.FMT_HTM) projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm") testFile = os.path.join(nwTempBuild, "4_LoremIpsum.htm") - refFile = os.path.join(nwRef, "build", "4_LoremIpsum.htm") + refFile = os.path.join(refDir, "build", "4_LoremIpsum.htm") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) @@ -536,14 +536,14 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp): assert nwBuild._saveDocument(nwBuild.FMT_JSON_H) projFile = os.path.join(nwLipsum, "Lorem Ipsum.json") testFile = os.path.join(nwTempBuild, "4H_LoremIpsum.json") - refFile = os.path.join(nwRef, "build", "4H_LoremIpsum.json") + refFile = os.path.join(refDir, "build", "4H_LoremIpsum.json") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [8]) assert nwBuild._saveDocument(nwBuild.FMT_JSON_M) projFile = os.path.join(nwLipsum, "Lorem Ipsum.json") testFile = os.path.join(nwTempBuild, "4M_LoremIpsum.json") - refFile = os.path.join(nwRef, "build", "4M_LoremIpsum.json") + refFile = os.path.join(refDir, "build", "4M_LoremIpsum.json") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [8]) @@ -589,9 +589,9 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp): nwGUI.closeMain() @pytest.mark.gui -def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef, nwTemp): +def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, refDir, tmpDir): - nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -620,7 +620,7 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef projFile = os.path.join(nwLipsum, "content", "73475cb40a568.nwd") testFile = os.path.join(nwTempGUI, "4_73475cb40a568.nwd") - refFile = os.path.join(nwRef, "gui", "4_73475cb40a568.nwd") + refFile = os.path.join(refDir, "gui", "4_73475cb40a568.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) @@ -646,7 +646,7 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef # This should give us back the file as it was before projFile = os.path.join(nwLipsum, "content", "71ee45a3c0db9.nwd") testFile = os.path.join(nwTempGUI, "4_71ee45a3c0db9.nwd") - refFile = os.path.join(nwRef, "gui", "4_73475cb40a568.nwd") + refFile = os.path.join(refDir, "gui", "4_73475cb40a568.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [1, 2, 3]) @@ -670,19 +670,19 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef projFile = os.path.join(nwLipsum, "content", "25fc0e7096fc6.nwd") testFile = os.path.join(nwTempGUI, "5_25fc0e7096fc6.nwd") - refFile = os.path.join(nwRef, "gui", "5_25fc0e7096fc6.nwd") + refFile = os.path.join(refDir, "gui", "5_25fc0e7096fc6.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) projFile = os.path.join(nwLipsum, "content", "31489056e0916.nwd") testFile = os.path.join(nwTempGUI, "5_31489056e0916.nwd") - refFile = os.path.join(nwRef, "gui", "5_31489056e0916.nwd") + refFile = os.path.join(refDir, "gui", "5_31489056e0916.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) projFile = os.path.join(nwLipsum, "content", "98010bd9270f9.nwd") testFile = os.path.join(nwTempGUI, "5_98010bd9270f9.nwd") - refFile = os.path.join(nwRef, "gui", "5_98010bd9270f9.nwd") + refFile = os.path.join(refDir, "gui", "5_98010bd9270f9.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) @@ -708,31 +708,31 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef projFile = os.path.join(nwLipsum, "content", "1a6562590ef19.nwd") testFile = os.path.join(nwTempGUI, "5_25fc0e7096fc6.nwd") - refFile = os.path.join(nwRef, "gui", "5_25fc0e7096fc6.nwd") + refFile = os.path.join(refDir, "gui", "5_25fc0e7096fc6.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [1, 2, 3]) projFile = os.path.join(nwLipsum, "content", "031b4af5197ec.nwd") testFile = os.path.join(nwTempGUI, "5_031b4af5197ec.nwd") - refFile = os.path.join(nwRef, "gui", "5_031b4af5197ec.nwd") + refFile = os.path.join(refDir, "gui", "5_031b4af5197ec.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) projFile = os.path.join(nwLipsum, "content", "41cfc0d1f2d12.nwd") testFile = os.path.join(nwTempGUI, "5_41cfc0d1f2d12.nwd") - refFile = os.path.join(nwRef, "gui", "5_41cfc0d1f2d12.nwd") + refFile = os.path.join(refDir, "gui", "5_41cfc0d1f2d12.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) projFile = os.path.join(nwLipsum, "content", "2858dcd1057d3.nwd") testFile = os.path.join(nwTempGUI, "5_2858dcd1057d3.nwd") - refFile = os.path.join(nwRef, "gui", "5_2858dcd1057d3.nwd") + refFile = os.path.join(refDir, "gui", "5_2858dcd1057d3.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) projFile = os.path.join(nwLipsum, "content", "2fca346db6561.nwd") testFile = os.path.join(nwTempGUI, "5_2fca346db6561.nwd") - refFile = os.path.join(nwRef, "gui", "5_2fca346db6561.nwd") + refFile = os.path.join(refDir, "gui", "5_2fca346db6561.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) @@ -740,7 +740,7 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef nwGUI.closeMain() @pytest.mark.gui -def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): +def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir): if sys.platform.startswith("darwin"): # Disable for macOS because the test segfaults on QWizard.show() @@ -752,7 +752,7 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): ProjWizardCustomPage, ProjWizardFinalPage ) - nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -937,8 +937,8 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): nwGUI.close() @pytest.mark.gui -def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): - nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp]) +def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir): + nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -1017,8 +1017,8 @@ def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): nwGUI.close() @pytest.mark.gui -def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpConf): - nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp]) +def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir, refDir, tmpConf): + nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -1203,9 +1203,9 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC # qtbot.stopForInteraction() nwGUI.closeMain() - refConf = os.path.join(nwRef, "novelwriter_prefs.conf") + refConf = os.path.join(refDir, "novelwriter_prefs.conf") projConf = os.path.join(nwGUI.mainConf.confPath, "novelwriter.conf") - testConf = os.path.join(nwTemp, "novelwriter_prefs.conf") + testConf = os.path.join(tmpDir, "novelwriter_prefs.conf") copyfile(projConf, testConf) ignoreLines = [ 2, # Timestamp @@ -1216,8 +1216,8 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC assert cmpFiles(testConf, refConf, ignoreLines) @pytest.mark.gui -def testQuotesDialog(qtbot, yesToAll, nwMinimal, nwTemp): - nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) +def testQuotesDialog(qtbot, yesToAll, nwMinimal, tmpDir): + nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -1246,15 +1246,15 @@ def testQuotesDialog(qtbot, yesToAll, nwMinimal, nwTemp): nwGUI.close() @pytest.mark.gui -def testDialogsOpenClose(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp): - nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) +def testDialogsOpenClose(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir): + nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) qtbot.wait(stepDelay) - monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: nwTemp) - assert nwGUI.selectProjectPath() == nwTemp + monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: tmpDir) + assert nwGUI.selectProjectPath() == tmpDir # qtbot.stopForInteraction() nwGUI.closeMain() diff --git a/tests/test_gui.py b/tests/test_gui_main.py similarity index 95% rename from tests/test_gui.py rename to tests/test_gui_main.py index fcf96c06..a10c0582 100644 --- a/tests/test_gui.py +++ b/tests/test_gui_main.py @@ -9,7 +9,7 @@ import os import sys from shutil import copyfile -from nwtools import cmpFiles +from tools import cmpFiles from PyQt5.QtCore import Qt, QUrl, QPoint, QItemSelectionModel from PyQt5.QtGui import QTextCursor, QColor, QPixmap, QIcon, QTextBlock @@ -27,11 +27,11 @@ typeDelay = 1 stepDelay = 20 @pytest.mark.gui -def testLaunch(qtbot, monkeypatch, nwFuncTemp, nwTemp): +def testLaunch(qtbot, monkeypatch, fncDir, tmpDir): # Defaults nwGUI = nw.main( - ["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp, "--style=Fusion"] + ["--testmode", "--config=%s" % fncDir, "--data=%s" % tmpDir, "--style=Fusion"] ) assert nw.logger.getEffectiveLevel() == logging.WARNING nwGUI.closeMain() @@ -39,21 +39,21 @@ def testLaunch(qtbot, monkeypatch, nwFuncTemp, nwTemp): # Log Levels nwGUI = nw.main( - ["--testmode", "--info", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ["--testmode", "--info", "--config=%s" % fncDir, "--data=%s" % tmpDir] ) assert nw.logger.getEffectiveLevel() == logging.INFO nwGUI.closeMain() nwGUI.close() nwGUI = nw.main( - ["--testmode", "--debug", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ["--testmode", "--debug", "--config=%s" % fncDir, "--data=%s" % tmpDir] ) assert nw.logger.getEffectiveLevel() == logging.DEBUG nwGUI.closeMain() nwGUI.close() nwGUI = nw.main( - ["--testmode", "--verbose", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ["--testmode", "--verbose", "--config=%s" % fncDir, "--data=%s" % tmpDir] ) assert nw.logger.getEffectiveLevel() == 5 nwGUI.closeMain() @@ -62,7 +62,7 @@ def testLaunch(qtbot, monkeypatch, nwFuncTemp, nwTemp): # Help and Version with pytest.raises(SystemExit) as ex: nwGUI = nw.main( - ["--testmode", "--help", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ["--testmode", "--help", "--config=%s" % fncDir, "--data=%s" % tmpDir] ) nwGUI.closeMain() nwGUI.close() @@ -70,7 +70,7 @@ def testLaunch(qtbot, monkeypatch, nwFuncTemp, nwTemp): with pytest.raises(SystemExit) as ex: nwGUI = nw.main( - ["--testmode", "--version", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ["--testmode", "--version", "--config=%s" % fncDir, "--data=%s" % tmpDir] ) nwGUI.closeMain() nwGUI.close() @@ -79,7 +79,7 @@ def testLaunch(qtbot, monkeypatch, nwFuncTemp, nwTemp): # Invalid options with pytest.raises(SystemExit) as ex: nwGUI = nw.main( - ["--testmode", "--invalid", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ["--testmode", "--invalid", "--config=%s" % fncDir, "--data=%s" % tmpDir] ) nwGUI.closeMain() nwGUI.close() @@ -92,7 +92,7 @@ def testLaunch(qtbot, monkeypatch, nwFuncTemp, nwTemp): monkeypatch.setattr("nw.CONFIG.verPyQtValue", 50000) with pytest.raises(SystemExit) as ex: nwGUI = nw.main( - ["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp] + ["--testmode", "--config=%s" % fncDir, "--data=%s" % tmpDir] ) nwGUI.closeMain() nwGUI.close() @@ -103,9 +103,9 @@ def testLaunch(qtbot, monkeypatch, nwFuncTemp, nwTemp): monkeypatch.undo() @pytest.mark.gui -def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): +def testDocEditor(qtbot, yesToAll, fncDir, nwTempGUI, refDir, tmpDir): - nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -113,7 +113,7 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): # Create new, save, close project nwGUI.theProject.projTree.setSeed(42) - assert nwGUI.newProject({"projPath": nwFuncTemp}) + assert nwGUI.newProject({"projPath": fncDir}) assert nwGUI.saveProject() assert nwGUI.closeProject() @@ -130,9 +130,9 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): assert not nwGUI.theProject.spellCheck # Check the files - projFile = os.path.join(nwFuncTemp, "nwProject.nwx") + projFile = os.path.join(fncDir, "nwProject.nwx") testFile = os.path.join(nwTempGUI, "0_nwProject.nwx") - refFile = os.path.join(nwRef, "gui", "0_nwProject.nwx") + refFile = os.path.join(refDir, "gui", "0_nwProject.nwx") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) qtbot.wait(stepDelay) @@ -140,7 +140,7 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): # qtbot.stopForInteraction() # Re-open project - assert nwGUI.openProject(nwFuncTemp) + assert nwGUI.openProject(fncDir) qtbot.wait(stepDelay) # Check that we loaded the data @@ -148,8 +148,8 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): assert len(nwGUI.theProject.projTree._treeOrder) == 8 assert len(nwGUI.theProject.projTree._treeRoots) == 4 assert nwGUI.theProject.projTree.trashRoot() is None - assert nwGUI.theProject.projPath == nwFuncTemp - assert nwGUI.theProject.projMeta == os.path.join(nwFuncTemp, "meta") + assert nwGUI.theProject.projPath == fncDir + assert nwGUI.theProject.projMeta == os.path.join(fncDir, "meta") assert nwGUI.theProject.projFile == "nwProject.nwx" assert nwGUI.theProject.projName == "New Project" assert nwGUI.theProject.bookTitle == "" @@ -379,33 +379,33 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): assert nwGUI.saveProject() # Check the files - projFile = os.path.join(nwFuncTemp, "nwProject.nwx") + projFile = os.path.join(fncDir, "nwProject.nwx") testFile = os.path.join(nwTempGUI, "1_nwProject.nwx") - refFile = os.path.join(nwRef, "gui", "1_nwProject.nwx") + refFile = os.path.join(refDir, "gui", "1_nwProject.nwx") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) - projFile = os.path.join(nwFuncTemp, "content", "031b4af5197ec.nwd") + projFile = os.path.join(fncDir, "content", "031b4af5197ec.nwd") testFile = os.path.join(nwTempGUI, "1_031b4af5197ec.nwd") - refFile = os.path.join(nwRef, "gui", "1_031b4af5197ec.nwd") + refFile = os.path.join(refDir, "gui", "1_031b4af5197ec.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) - projFile = os.path.join(nwFuncTemp, "content", "1a6562590ef19.nwd") + projFile = os.path.join(fncDir, "content", "1a6562590ef19.nwd") testFile = os.path.join(nwTempGUI, "1_1a6562590ef19.nwd") - refFile = os.path.join(nwRef, "gui", "1_1a6562590ef19.nwd") + refFile = os.path.join(refDir, "gui", "1_1a6562590ef19.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) - projFile = os.path.join(nwFuncTemp, "content", "0e17daca5f3e1.nwd") + projFile = os.path.join(fncDir, "content", "0e17daca5f3e1.nwd") testFile = os.path.join(nwTempGUI, "1_0e17daca5f3e1.nwd") - refFile = os.path.join(nwRef, "gui", "1_0e17daca5f3e1.nwd") + refFile = os.path.join(refDir, "gui", "1_0e17daca5f3e1.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) - projFile = os.path.join(nwFuncTemp, "content", "41cfc0d1f2d12.nwd") + projFile = os.path.join(fncDir, "content", "41cfc0d1f2d12.nwd") testFile = os.path.join(nwTempGUI, "1_41cfc0d1f2d12.nwd") - refFile = os.path.join(nwRef, "gui", "1_41cfc0d1f2d12.nwd") + refFile = os.path.join(refDir, "gui", "1_41cfc0d1f2d12.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, refFile) @@ -414,9 +414,9 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp): nwGUI.close() @pytest.mark.gui -def testDocViewer(qtbot, yesToAll, nwLipsum, nwTemp): +def testDocViewer(qtbot, yesToAll, nwLipsum, tmpDir): - nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -566,9 +566,9 @@ def testDocViewer(qtbot, yesToAll, nwLipsum, nwTemp): nwGUI.close() @pytest.mark.gui -def testProjectTree(qtbot, yesToAll, nwMinimal, nwTemp): +def testProjectTree(qtbot, yesToAll, nwMinimal, tmpDir): - nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -690,9 +690,9 @@ def testProjectTree(qtbot, yesToAll, nwMinimal, nwTemp): nwGUI.close() @pytest.mark.gui -def testEditFormatMenu(qtbot, yesToAll, nwLipsum, nwTemp): +def testEditFormatMenu(qtbot, yesToAll, nwLipsum, tmpDir): - nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -927,9 +927,9 @@ def testEditFormatMenu(qtbot, yesToAll, nwLipsum, nwTemp): nwGUI.close() @pytest.mark.gui -def testContextMenu(qtbot, yesToAll, nwLipsum, nwTemp): +def testContextMenu(qtbot, yesToAll, nwLipsum, tmpDir): - nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -1013,15 +1013,15 @@ def testContextMenu(qtbot, yesToAll, nwLipsum, nwTemp): nwGUI.close() @pytest.mark.gui -def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp): - nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp]) +def testInsertMenu(qtbot, monkeypatch, fncDir, tmpDir): + nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) qtbot.wait(stepDelay) nwGUI.theProject.projTree.setSeed(42) - assert nwGUI.newProject({"projPath": nwFuncTemp}) + assert nwGUI.newProject({"projPath": fncDir}) assert nwGUI.treeView._getTreeItem("0e17daca5f3e1") is not None @@ -1162,7 +1162,7 @@ def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp): assert not nwGUI.importDocument() # Then a valid path, but bot a file that exists - theFile = os.path.join(nwTemp, "import.txt") + theFile = os.path.join(tmpDir, "import.txt") monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwards: (theFile, "")) assert not nwGUI.importDocument() @@ -1205,16 +1205,16 @@ def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp): assert len(theBits) == 3 assert theBits[0] == "File details for the currently open file" assert theBits[1] == "Handle: 0e17daca5f3e1" - assert theBits[2] == "Location: %s" % os.path.join(nwFuncTemp, "content", "0e17daca5f3e1.nwd") + assert theBits[2] == "Location: %s" % os.path.join(fncDir, "content", "0e17daca5f3e1.nwd") # qtbot.stopForInteraction() nwGUI.closeMain() nwGUI.close() @pytest.mark.gui -def testTextSearch(qtbot, monkeypatch, yesToAll, nwLipsum, nwTemp): +def testTextSearch(qtbot, monkeypatch, yesToAll, nwLipsum, tmpDir): - nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -1392,9 +1392,9 @@ def testTextSearch(qtbot, monkeypatch, yesToAll, nwLipsum, nwTemp): nwGUI.close() @pytest.mark.gui -def testOutline(qtbot, yesToAll, nwLipsum, nwTemp): +def testOutline(qtbot, yesToAll, nwLipsum, tmpDir): - nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -1453,9 +1453,9 @@ def testOutline(qtbot, yesToAll, nwLipsum, nwTemp): nwGUI.close() @pytest.mark.gui -def testThemes(qtbot, yesToAll, nwMinimal, nwTemp): +def testThemes(qtbot, yesToAll, nwMinimal, tmpDir): - nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.waitForWindowShown(nwGUI) @@ -1478,7 +1478,7 @@ def testThemes(qtbot, yesToAll, nwMinimal, nwTemp): # Re-open assert nw.CONFIG.confPath == nwMinimal - nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal]) + nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal]) assert nwGUI.mainConf.confPath == nwMinimal qtbot.addWidget(nwGUI) nwGUI.show() diff --git a/tests/test_project.py b/tests/test_project.py deleted file mode 100644 index 5ed9d261..00000000 --- a/tests/test_project.py +++ /dev/null @@ -1,679 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter Project Class Tester -""" - -import pytest -import os - -from shutil import copyfile -from zipfile import ZipFile - -from nwtools import cmpFiles - -from nw.core.project import NWProject -from nw.core.document import NWDoc -from nw.core.spellcheck import NWSpellEnchant, NWSpellSimple -from nw.constants import nwConst, nwItemClass, nwItemType, nwItemLayout, nwFiles - -@pytest.mark.project -def testProjectNewOpenSave(nwFuncTemp, nwTempProj, nwRef, nwTemp, nwDummy): - """Test that a basic project can be created, and opened and saved. - """ - projFile = os.path.join(nwFuncTemp, "nwProject.nwx") - testFile = os.path.join(nwTempProj, "1_nwProject.nwx") - refFile = os.path.join(nwRef, "proj", "1_nwProject.nwx") - - theProject = NWProject(nwDummy) - theProject.projTree.setSeed(42) - - # Setting no data should fail - assert not theProject.newProject({}) - - # Try again with a proper path - assert theProject.newProject({"projPath": nwFuncTemp}) - assert theProject.setProjectPath(nwFuncTemp) - assert theProject.saveProject() - assert theProject.closeProject() - - # Creating the project once more should fail - assert not theProject.newProject({"projPath": nwFuncTemp}) - - # Check the new project - copyfile(projFile, testFile) - assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) - - # Open again - assert theProject.openProject(projFile) - - # Save and close - assert theProject.saveProject() - assert theProject.closeProject() - copyfile(projFile, testFile) - assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) - assert not theProject.projChanged - - # Open a second time - assert theProject.openProject(projFile) - assert not theProject.openProject(projFile) - assert theProject.openProject(projFile, overrideLock=True) - assert theProject.saveProject() - assert theProject.closeProject() - copyfile(projFile, testFile) - assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) - -@pytest.mark.project -def testProjectNewRoot(nwFuncTemp, nwTempProj, nwRef, nwDummy): - """Check that new root folders can be added to the project. - """ - projFile = os.path.join(nwFuncTemp, "nwProject.nwx") - testFile = os.path.join(nwTempProj, "2_nwProject.nwx") - refFile = os.path.join(nwRef, "proj", "2_nwProject.nwx") - - theProject = NWProject(nwDummy) - theProject.projTree.setSeed(42) - - assert theProject.newProject({"projPath": nwFuncTemp}) - assert theProject.setProjectPath(nwFuncTemp) - assert theProject.saveProject() - assert theProject.closeProject() - assert theProject.openProject(projFile) - - assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), type(None)) - assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), type(None)) - assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), type(None)) - assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), type(None)) - assert isinstance(theProject.newRoot("Timeline", nwItemClass.TIMELINE), str) - assert isinstance(theProject.newRoot("Object", nwItemClass.OBJECT), str) - assert isinstance(theProject.newRoot("Custom1", nwItemClass.CUSTOM), str) - assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str) - - assert theProject.projChanged - assert theProject.saveProject() - assert theProject.closeProject() - - copyfile(projFile, testFile) - assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) - assert not theProject.projChanged - -@pytest.mark.project -def testProjectNewFile(nwFuncTemp, nwTempProj, nwRef, nwDummy): - """Check that new files can be added to the project. - """ - projFile = os.path.join(nwFuncTemp, "nwProject.nwx") - testFile = os.path.join(nwTempProj, "3_nwProject.nwx") - refFile = os.path.join(nwRef, "proj", "3_nwProject.nwx") - - theProject = NWProject(nwDummy) - theProject.projTree.setSeed(42) - - assert theProject.newProject({"projPath": nwFuncTemp}) - assert theProject.setProjectPath(nwFuncTemp) - assert theProject.saveProject() - assert theProject.closeProject() - assert theProject.openProject(projFile) - - assert isinstance(theProject.newFile("Hello", nwItemClass.NOVEL, "31489056e0916"), str) - assert isinstance(theProject.newFile("Jane", nwItemClass.CHARACTER, "71ee45a3c0db9"), str) - assert theProject.projChanged - assert theProject.saveProject() - assert theProject.closeProject() - - copyfile(projFile, testFile) - assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) - assert not theProject.projChanged - -@pytest.mark.project -def testProjectNewCustomA(nwFuncTemp, nwTempProj, nwRef, nwDummy): - """Create a new project from a project wizard dictionary. - Custom type with chapters and scenes. - """ - projFile = os.path.join(nwFuncTemp, "nwProject.nwx") - testFile = os.path.join(nwTempProj, "4_nwProject.nwx") - refFile = os.path.join(nwRef, "proj", "4_nwProject.nwx") - - projData = { - "projName": "Test Custom", - "projTitle": "Test Novel", - "projAuthors": "Jane Doe\nJohn Doh\n", - "projPath": nwFuncTemp, - "popSample": False, - "popMinimal": False, - "popCustom": True, - "addRoots": [ - nwItemClass.PLOT, - nwItemClass.CHARACTER, - nwItemClass.WORLD, - nwItemClass.TIMELINE, - nwItemClass.OBJECT, - nwItemClass.ENTITY, - ], - "numChapters": 3, - "numScenes": 3, - "chFolders": True, - } - theProject = NWProject(nwDummy) - theProject.projTree.setSeed(42) - - assert theProject.newProject(projData) - assert theProject.saveProject() - assert theProject.closeProject() - - copyfile(projFile, testFile) - assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) - -@pytest.mark.project -def testProjectNewCustomB(nwFuncTemp, nwTempProj, nwRef, nwDummy): - """Create a new project from a project wizard dictionary. - Custom type without chapters, but with scenes. - """ - projFile = os.path.join(nwFuncTemp, "nwProject.nwx") - testFile = os.path.join(nwTempProj, "5_nwProject.nwx") - refFile = os.path.join(nwRef, "proj", "5_nwProject.nwx") - - projData = { - "projName": "Test Custom", - "projTitle": "Test Novel", - "projAuthors": "Jane Doe\nJohn Doh\n", - "projPath": nwFuncTemp, - "popSample": False, - "popMinimal": False, - "popCustom": True, - "addRoots": [ - nwItemClass.PLOT, - nwItemClass.CHARACTER, - nwItemClass.WORLD, - nwItemClass.TIMELINE, - nwItemClass.OBJECT, - nwItemClass.ENTITY, - ], - "numChapters": 0, - "numScenes": 6, - "chFolders": True, - } - theProject = NWProject(nwDummy) - theProject.projTree.setSeed(42) - - assert theProject.newProject(projData) - assert theProject.saveProject() - assert theProject.closeProject() - - copyfile(projFile, testFile) - assert cmpFiles(testFile, refFile, [2, 6, 7, 8]) - -@pytest.mark.project -def testProjectNewSampleA(nwFuncTemp, nwConf, nwDummy, nwTemp): - """Check that we can create a new project can be created from the - provided sample project via a zip file. - """ - projData = { - "projName": "Test Sample", - "projTitle": "Test Novel", - "projAuthors": "Jane Doe\nJohn Doh\n", - "projPath": nwFuncTemp, - "popSample": True, - "popMinimal": False, - "popCustom": False, - } - theProject = NWProject(nwDummy) - theProject.projTree.setSeed(42) - theProject.mainConf = nwConf - - # Sample set, but no path - assert not theProject.newProject({"popSample": True}) - - # Force the lookup path for assets to our temp folder - srcSample = os.path.abspath(os.path.join(nwConf.appRoot, "sample")) - dstSample = os.path.join(nwTemp, "sample.zip") - nwConf.assetPath = nwTemp - - # Create and open a defective zip file - with open(dstSample, mode="w+") as outFile: - outFile.write("foo") - - assert not theProject.newProject(projData) - os.unlink(dstSample) - - # Create a real zip file, and unpack it - with ZipFile(dstSample, "w") as zipObj: - zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx") - for docFile in os.listdir(os.path.join(srcSample, "content")): - srcDoc = os.path.join(srcSample, "content", docFile) - zipObj.write(srcDoc, "content/"+docFile) - - assert theProject.newProject(projData) - assert theProject.openProject(nwFuncTemp) - assert theProject.projName == "Sample Project" - assert theProject.saveProject() - assert theProject.closeProject() - os.unlink(dstSample) - -@pytest.mark.project -def testProjectNewSampleB(monkeypatch, nwFuncTemp, nwConf, nwDummy, nwTemp): - """Check that we can create a new project can be created from the - provided sample project folder. - """ - projData = { - "projName": "Test Sample", - "projTitle": "Test Novel", - "projAuthors": "Jane Doe\nJohn Doh\n", - "projPath": nwFuncTemp, - "popSample": True, - "popMinimal": False, - "popCustom": False, - } - theProject = NWProject(nwDummy) - theProject.projTree.setSeed(42) - theProject.mainConf = nwConf - - # Make sure we do not pick up the nw/assets/sample.zip file - nwConf.assetPath = nwTemp - - # Set a fake project file name - monkeypatch.setattr(nwFiles, "PROJ_FILE", "nothing.nwx") - assert not theProject.newProject(projData) - - monkeypatch.setattr(nwFiles, "PROJ_FILE", "nwProject.nwx") - assert theProject.newProject(projData) - assert theProject.openProject(nwFuncTemp) - assert theProject.projName == "Sample Project" - assert theProject.saveProject() - assert theProject.closeProject() - - # Misdirect the appRoot path so neither is possible - nwConf.appRoot = nwTemp - assert not theProject.newProject(projData) - -@pytest.mark.project -def testProjectMethods(monkeypatch, nwMinimal, nwDummy): - """Test other project class methods and functions. - """ - theProject = NWProject(nwDummy) - theProject.projTree.setSeed(42) - assert theProject.openProject(nwMinimal) - assert theProject.projPath == nwMinimal - - # Setting project path - assert theProject.setProjectPath(None) - assert theProject.projPath is None - assert theProject.setProjectPath("") - assert theProject.projPath is None - assert theProject.setProjectPath("~") - assert theProject.projPath == os.path.expanduser("~") - - # Create a new folder and populate it - projPath = os.path.join(nwMinimal, "dummy1") - assert theProject.setProjectPath(projPath, newProject=True) - - # Make os.mkdir fail - def altMkdir(*args): - raise Exception("Oops!") - - monkeypatch.setattr("os.mkdir", altMkdir) - projPath = os.path.join(nwMinimal, "dummy2") - assert not theProject.setProjectPath(projPath, newProject=True) - - # Project Name - assert theProject.setProjectName(" A Name ") - assert theProject.projName == "A Name" - - # Project Title - assert theProject.setBookTitle(" A Title ") - assert theProject.bookTitle == "A Title" - - # Project Authors - assert not theProject.setBookAuthors([]) - assert theProject.setBookAuthors(" Jane Doe \n John Doh \n ") - assert theProject.bookAuthors == ["Jane Doe", "John Doh"] - -@pytest.mark.project -def testDocMeta(nwDummy, nwLipsum): - """Check that the document meta data string is parsed correctly. - """ - theProject = NWProject(nwDummy) - theProject.projTree.setSeed(42) - assert theProject.openProject(nwLipsum) - - aDoc = NWDoc(theProject, nwDummy) - assert aDoc.openDocument("47666c91c7ccf") - theName, theParent, theClass, theLayout = aDoc.getMeta() - - assert theName == "Scene Five" - assert theParent == "6bd935d2490cd" - assert theClass == nwItemClass.NOVEL - assert theLayout == nwItemLayout.SCENE - - aDoc._docMeta = {"stuff": None} - theName, theParent, theClass, theLayout = aDoc.getMeta() - assert theName == "" - assert theParent is None - assert theClass is None - assert theLayout is None - -@pytest.mark.project -def testSpellEnchant(nwTemp, nwConf): - wList = os.path.join(nwTemp, "wordlist.txt") - with open(wList, mode="w") as wFile: - wFile.write("a_word\nb_word\nc_word\n") - - spChk = NWSpellEnchant() - spChk.mainConf = nwConf - 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 != "" - -@pytest.mark.project -def testSpellSimple(nwTemp, nwConf): - wList = os.path.join(nwTemp, "wordlist.txt") - with open(wList, mode="w") as wFile: - wFile.write("a_word\nb_word\nc_word\n") - - spChk = NWSpellSimple() - spChk.mainConf = nwConf - 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 == nwConst.SP_INTERNAL - -@pytest.mark.project -def testProjectOptions(nwDummy, nwLipsum): - """Test the class that holds all the GUI state user options that are - tied to the current open project. Non-project related GUI options - are handled by the Config class. - """ - theProject = NWProject(nwDummy) - assert theProject.projMeta is None - - theOpts = theProject.optState - assert not theOpts.loadSettings() - assert not theOpts.saveSettings() - - # No Settings - assert theProject.openProject(nwLipsum) - assert theOpts.loadSettings() - assert theOpts.saveSettings() - assert str(theOpts.theState) == r"{}" - - # Read Invalid Settings and Filter - stateFile = os.path.join(theProject.projMeta, nwFiles.OPTS_FILE) - with open(stateFile, mode="w", encoding="utf8") as outFile: - outFile.write( - r'{"GuiProjectSettings": {"winWidth": 100, "winHeight": 50}, "NoGroup": {"NoName": 0}}' - ) - assert theOpts.loadSettings() - assert str(theOpts.theState) == r"{'GuiProjectSettings': {'winWidth': 100, 'winHeight': 50}}" - - # Set New Settings - assert not theOpts.setValue("NoGroup", "NoName", None) - assert not theOpts.setValue("GuiProjectSettings", "NoName", None) - assert theOpts.setValue("GuiProjectSettings", "winWidth", 200) - assert theOpts.setValue("GuiProjectSettings", "winHeight", 80) - assert str(theOpts.theState) == r"{'GuiProjectSettings': {'winWidth': 200, 'winHeight': 80}}" - - # Check Read/Write Types - - ## String - assert theOpts.setValue("GuiWritingStats", "winWidth", "123") - assert isinstance(theOpts.getString("GuiWritingStats", "winWidth", "456"), str) - assert theOpts.getString("GuiWritingStats", "NoName", "456") == "456" - - ## Int - assert theOpts.setValue("GuiWritingStats", "winWidth", "123") - assert isinstance(theOpts.getInt("GuiWritingStats", "winWidth", 456), int) - assert theOpts.getInt("GuiWritingStats", "NoName", 456) == 456 - assert theOpts.setValue("GuiWritingStats", "winWidth", "True") - assert theOpts.getInt("GuiWritingStats", "NoName", 456) == 456 - - ## Float - assert theOpts.setValue("GuiWritingStats", "winWidth", "123") - assert isinstance(theOpts.getFloat("GuiWritingStats", "winWidth", 456.0), float) - assert theOpts.getFloat("GuiWritingStats", "NoName", 456.0) == 456.0 - assert theOpts.setValue("GuiWritingStats", "winWidth", "True") - assert theOpts.getFloat("GuiWritingStats", "winWidth", 456.0) == 456.0 - - ## Bool - assert theOpts.setValue("GuiWritingStats", "winWidth", True) - assert isinstance(theOpts.getBool("GuiWritingStats", "winWidth", False), bool) - assert theOpts.getFloat("GuiWritingStats", "NoName", False) is False - assert theOpts.setValue("GuiWritingStats", "winWidth", "True") - assert theOpts.getFloat("GuiWritingStats", "winWidth", False) is False - -@pytest.mark.project -def testProjectOrphanedFiles(nwDummy, nwLipsum): - """Check that files in the content folder that are not tracked in - the project XML file are handled correctly by the orphaned files - function. It should also restore as much meta data as possible from - the meta line at the top of the document file. - """ - theProject = NWProject(nwDummy) - assert theProject.openProject(nwLipsum) - assert theProject.projTree["636b6aa9b697b"] is None - assert theProject.closeProject() - - # First Item with Meta Data - orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd") - with open(orphPath, mode="w", encoding="utf8") as outFile: - outFile.write("%%~name:Mars\n") - outFile.write("%%~path:5eaea4e8cdee8/636b6aa9b697b\n") - outFile.write("%%~kind:WORLD/NOTE\n") - outFile.write("%%~invalid\n") - outFile.write("\n") - - # Second Item without Meta Data - orphPath = os.path.join(nwLipsum, "content", "736b6aa9b697b.nwd") - with open(orphPath, mode="w", encoding="utf8") as outFile: - outFile.write("\n") - - # Invalid File Name - dummyPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.txt") - with open(dummyPath, mode="w", encoding="utf8") as outFile: - outFile.write("\n") - - # Invalid File Name - dummyPath = os.path.join(nwLipsum, "content", "636b6aa9b697bb.nwd") - with open(dummyPath, mode="w", encoding="utf8") as outFile: - outFile.write("\n") - - # Invalid File Name - dummyPath = os.path.join(nwLipsum, "content", "abcdefghijklm.nwd") - with open(dummyPath, mode="w", encoding="utf8") as outFile: - outFile.write("\n") - - assert theProject.openProject(nwLipsum) - assert theProject.projPath is not None - assert theProject.projTree["636b6aa9b697bb"] is None - assert theProject.projTree["abcdefghijklm"] is None - - # First Item with Meta Data - oItem = theProject.projTree["636b6aa9b697b"] - assert oItem is not None - assert oItem.itemName == "Mars" - assert oItem.itemHandle == "636b6aa9b697b" - assert oItem.itemParent is None - assert oItem.itemClass == nwItemClass.WORLD - assert oItem.itemType == nwItemType.FILE - assert oItem.itemLayout == nwItemLayout.NOTE - - # Second Item without Meta Data - oItem = theProject.projTree["736b6aa9b697b"] - assert oItem is not None - assert oItem.itemName == "Orphaned File 1" - assert oItem.itemHandle == "736b6aa9b697b" - assert oItem.itemParent is None - assert oItem.itemClass == nwItemClass.NO_CLASS - assert oItem.itemType == nwItemType.FILE - assert oItem.itemLayout == nwItemLayout.NO_LAYOUT - - assert theProject.saveProject(nwLipsum) - assert theProject.closeProject() - -@pytest.mark.project -def testProjectOldFormat(nwDummy, nwOldProj): - """Test that a project folder structure of version 1.0 can be - converted to the latest folder structure. Version 1.0 split the - documents into 'data_0' ... 'data_f' folders, which are now all - contained in a single 'content' folder. - """ - theProject = NWProject(nwDummy) - theProject.mainConf.showGUI = False - - # Create dummy files for known legacy files - deleteFiles = [ - os.path.join(nwOldProj, "cache", "nwProject.nwx.0"), - os.path.join(nwOldProj, "cache", "nwProject.nwx.1"), - os.path.join(nwOldProj, "cache", "nwProject.nwx.2"), - os.path.join(nwOldProj, "cache", "nwProject.nwx.3"), - os.path.join(nwOldProj, "cache", "nwProject.nwx.4"), - os.path.join(nwOldProj, "cache", "nwProject.nwx.5"), - os.path.join(nwOldProj, "cache", "nwProject.nwx.6"), - os.path.join(nwOldProj, "cache", "nwProject.nwx.7"), - os.path.join(nwOldProj, "cache", "nwProject.nwx.8"), - os.path.join(nwOldProj, "cache", "nwProject.nwx.9"), - os.path.join(nwOldProj, "meta", "mainOptions.json"), - os.path.join(nwOldProj, "meta", "exportOptions.json"), - os.path.join(nwOldProj, "meta", "outlineOptions.json"), - os.path.join(nwOldProj, "meta", "timelineOptions.json"), - os.path.join(nwOldProj, "meta", "docMergeOptions.json"), - os.path.join(nwOldProj, "meta", "sessionLogOptions.json"), - ] - - # Add some files that shouldn't be there - deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.nwd")) - deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.txt")) - - # Add some folders that shouldn't be there - os.mkdir(os.path.join(nwOldProj, "stuff")) - os.mkdir(os.path.join(nwOldProj, "data_1", "stuff")) - - # Create dummy files - os.mkdir(os.path.join(nwOldProj, "cache")) - for aFile in deleteFiles: - with open(aFile, mode="w+", encoding="utf8") as outFile: - outFile.write("Hi") - for aFile in deleteFiles: - assert os.path.isfile(aFile) - - # Open project and check that files that are not supposed to be - # there have been removed - assert theProject.openProject(nwOldProj) - for aFile in deleteFiles: - assert not os.path.isfile(aFile) - - assert not os.path.isdir(os.path.join(nwOldProj, "data_1", "stuff")) - assert not os.path.isdir(os.path.join(nwOldProj, "data_1")) - assert not os.path.isdir(os.path.join(nwOldProj, "data_7")) - assert not os.path.isdir(os.path.join(nwOldProj, "data_8")) - assert not os.path.isdir(os.path.join(nwOldProj, "data_9")) - assert not os.path.isdir(os.path.join(nwOldProj, "data_a")) - assert not os.path.isdir(os.path.join(nwOldProj, "data_f")) - - # Check stuff that has been moved - assert os.path.isdir(os.path.join(nwOldProj, "junk")) - assert os.path.isdir(os.path.join(nwOldProj, "junk", "stuff")) - assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.nwd")) - assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.txt")) - - # Check that files we want to keep are in the right place - assert os.path.isdir(os.path.join(nwOldProj, "cache")) - assert os.path.isdir(os.path.join(nwOldProj, "content")) - assert os.path.isdir(os.path.join(nwOldProj, "meta")) - - assert os.path.isfile(os.path.join(nwOldProj, "content", "f528d831f5b24.nwd")) - assert os.path.isfile(os.path.join(nwOldProj, "content", "88124a4292d8b.nwd")) - assert os.path.isfile(os.path.join(nwOldProj, "content", "91239bf2f8b69.nwd")) - assert os.path.isfile(os.path.join(nwOldProj, "content", "19752e7f9d8af.nwd")) - assert os.path.isfile(os.path.join(nwOldProj, "content", "a764d5acf5a21.nwd")) - assert os.path.isfile(os.path.join(nwOldProj, "content", "9058ae29f0dfd.nwd")) - assert os.path.isfile(os.path.join(nwOldProj, "content", "7ff63b8afc4cd.nwd")) - - assert os.path.isfile(os.path.join(nwOldProj, "meta", "tagsIndex.json")) - assert os.path.isfile(os.path.join(nwOldProj, "meta", "sessionInfo.log")) - - # Close the project - theProject.closeProject() - - # Check that new files have been created - assert os.path.isfile(os.path.join(nwOldProj, "meta", "guiOptions.json")) - assert os.path.isfile(os.path.join(nwOldProj, "meta", "sessionStats.log")) - assert os.path.isfile(os.path.join(nwOldProj, "ToC.txt")) - -@pytest.mark.project -def testProjectBackup(nwDummy, nwMinimal, nwTemp): - """Test the automated backup feature of the project class. The test - creates a backup of the Minimal test project, and then unzips the - backupd file and checks that the project XML file is identical to - the original file. - """ - theProject = NWProject(nwDummy) - assert theProject.openProject(nwMinimal) - - # Test faulty settings - # Invalid path - theProject.mainConf.backupPath = None - assert not theProject.zipIt(doNotify=False) - - # Missing project name - theProject.mainConf.backupPath = nwTemp - theProject.projName = "" - assert not theProject.zipIt(doNotify=False) - - # Non-existent folder - theProject.mainConf.backupPath = os.path.join(nwTemp, "nonexistent") - theProject.projName = "Test Minimal" - assert not theProject.zipIt(doNotify=False) - - # Same folder as project (causes infinite loop in zipping) - theProject.mainConf.backupPath = nwMinimal - assert not theProject.zipIt(doNotify=False) - - # Test correct settings - theProject.mainConf.backupPath = nwTemp - assert theProject.zipIt(doNotify=False) - - theFiles = os.listdir(os.path.join(nwTemp, "Test Minimal")) - assert len(theFiles) == 1 - - theZip = theFiles[0] - assert theZip[:12] == "Backup from " - assert theZip[-4:] == ".zip" - - # Extract the archive - with ZipFile(os.path.join(nwTemp, "Test Minimal", theZip), "r") as inZip: - inZip.extractall(os.path.join(nwTemp, "extract")) - - # Check that the main project file was restored - assert cmpFiles( - os.path.join(nwMinimal, "nwProject.nwx"), os.path.join(nwTemp, "extract", "nwProject.nwx") - ) diff --git a/tests/nwtools.py b/tests/tools.py similarity index 83% rename from tests/nwtools.py rename to tests/tools.py index e35d9e08..185a9fa0 100644 --- a/tests/nwtools.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)