diff --git a/novelwriter/config.py b/novelwriter/config.py index af523dc5..8c21dc99 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -74,8 +74,8 @@ class Config: self._appPath = self._appRoot # Runtime Settings and Variables - self.hasError = False # True if the config class encountered an error - self.errData = [] # List of error messages + self._hasError = False # True if the config class encountered an error + self._errData = [] # List of error messages self.confChanged = False # True whenever the config has chenged, false after save self.cmdOpen = None # Path from command line for project to be opened on launch @@ -92,6 +92,8 @@ class Config: # User Settings # ============= + self._recentProj = RecentProjects(self._dataPath) + # General GUI Settings self.guiLang = self._qLocal.name() self.guiTheme = "" # GUI theme @@ -245,6 +247,18 @@ class Config: return + ## + # Properties + ## + + @property + def hasError(self): + return self._hasError + + @property + def recentProjects(self): + return self._recentProj + ## # Methods ## @@ -282,6 +296,15 @@ class Config: return self._lastPath return Path.home().absolute() + def errorText(self): + """Compile and return error messages from the initialisation of + the Config class, and clear the error buffer. + """ + errMessage = "
".join(self._errData) + self._hasError = False + self._errData = [] + return errMessage + ## # Config Actions ## @@ -321,12 +344,12 @@ class Config: else: self.saveConfig() - self.loadRecentCache() + self._recentProj.loadCache() self._checkOptionalPackages() logger.debug("Config initialisation complete") - return True + return def initLocalisation(self, nwApp): """Initialise the localisation of the GUI. @@ -345,7 +368,7 @@ class Config: qTrans = QTranslator() lngFile = "%s_%s" % (lngBase, lngCode.replace("-", "_")) if lngFile not in self._qtTrans: - if qTrans.load(lngFile, lngPath): + if qTrans.load(lngFile, str(lngPath)): logger.debug("Loaded: %s", os.path.join(lngPath, lngFile)) nwApp.installTranslator(qTrans) self._qtTrans[lngFile] = qTrans @@ -367,12 +390,12 @@ class Config: else: return [] - for qmFile in os.listdir(self._nwLangPath): - if not os.path.isfile(os.path.join(self._nwLangPath, qmFile)): + for qmFile in Path(self._nwLangPath).iterdir(): + qmName = qmFile.name + if not (qmFile.is_file() and qmName.startswith(fPre) and qmName.endswith(fExt)): continue - if not qmFile.startswith(fPre) or not qmFile.endswith(fExt): - continue - qmLang = qmFile[len(fPre):-len(fExt)] + + qmLang = qmName[len(fPre):-len(fExt)] qmName = QLocale(qmLang).nativeLanguageName().title() if qmLang and qmName and qmLang != "en_GB": langList[qmLang] = qmName @@ -392,9 +415,9 @@ class Config: except Exception as exc: logger.error("Could not load config file") logException() - self.hasError = True - self.errData.append("Could not load config file") - self.errData.append(formatException(exc)) + self._hasError = True + self._errData.append("Could not load config file") + self._errData.append(formatException(exc)) return False # Main @@ -606,80 +629,13 @@ class Config: except Exception as exc: logger.error("Could not save config file") logException() - self.hasError = True - self.errData.append("Could not save config file") - self.errData.append(formatException(exc)) + self._hasError = True + self._errData.append("Could not save config file") + self._errData.append(formatException(exc)) return False return True - def loadRecentCache(self): - """Load the cache file for recent projects. - """ - self.recentProj = {} - - cacheFile = self._dataPath / nwFiles.RECENT_FILE - if not os.path.isfile(cacheFile): - return True - - try: - with open(cacheFile, mode="r", encoding="utf-8") as inFile: - theData = json.load(inFile) - - for projPath, theEntry in theData.items(): - self.recentProj[projPath] = { - "title": theEntry.get("title", ""), - "time": theEntry.get("time", 0), - "words": theEntry.get("words", 0), - } - - except Exception as exc: - self.hasError = True - self.errData.append("Could not load recent project cache") - self.errData.append(formatException(exc)) - return False - - return True - - def saveRecentCache(self): - """Save the cache dictionary of recent projects. - """ - cacheFile = self._dataPath / nwFiles.RECENT_FILE - cacheTemp = cacheFile.with_suffix(".tmp") - try: - with open(cacheTemp, mode="w+", encoding="utf-8") as outFile: - json.dump(self.recentProj, outFile, indent=2) - cacheTemp.replace(cacheFile) - except Exception as exc: - self.hasError = True - self.errData.append("Could not save recent project cache") - self.errData.append(formatException(exc)) - return False - - return True - - def updateRecentCache(self, projPath, projTitle, wordCount, saveTime): - """Add or update recent cache information on a given project. - """ - self.recentProj[os.path.abspath(projPath)] = { - "title": projTitle, - "time": int(saveTime), - "words": int(wordCount), - } - return True - - def removeFromRecentCache(self, thePath): - """Trying to remove a path from the recent projects cache. - """ - if thePath in self.recentProj: - del self.recentProj[thePath] - logger.debug("Removed recent: %s", thePath) - self.saveRecentCache() - else: - logger.error("Unknown recent: %s", thePath) - return False - return True - ## # Setters ## @@ -828,15 +784,6 @@ class Config: def getTabWidth(self): return self.pxInt(max(self.tabWidth, 0)) - def getErrData(self): - """Compile and return error messages from the initialisation of - the Config class, and clear the error buffer. - """ - errMessage = "
".join(self.errData) - self.hasError = False - self.errData = [] - return errMessage - ## # Internal Functions ## @@ -872,3 +819,78 @@ class Config: return # END Class Config + + +class RecentProjects: + + def __init__(self, dataPath): + self._dataPath = dataPath + self._data = {} + return + + def loadCache(self): + """Load the cache file for recent projects. + """ + self._data = {} + + cacheFile = self._dataPath / nwFiles.RECENT_FILE + if not cacheFile.is_file(): + return True + + try: + with open(cacheFile, mode="r", encoding="utf-8") as inFile: + theData = json.load(inFile) + for projPath, theEntry in theData.items(): + self._data[projPath] = { + "title": theEntry.get("title", ""), + "words": theEntry.get("words", 0), + "time": theEntry.get("time", 0), + } + except Exception: + logger.error("Could not load recent project cache") + logException() + return False + + return True + + def saveCache(self): + """Save the cache dictionary of recent projects. + """ + cacheFile = self._dataPath / nwFiles.RECENT_FILE + cacheTemp = cacheFile.with_suffix(".tmp") + try: + with open(cacheTemp, mode="w+", encoding="utf-8") as outFile: + json.dump(self._data, outFile, indent=2) + cacheTemp.replace(cacheFile) + except Exception: + logger.error("Could not save recent project cache") + logException() + return False + + return True + + def listEntries(self): + """List all items in the cache. + """ + return [(k, e["title"], e["words"], e["time"]) for k, e in self._data.items()] + + def update(self, projPath, projTitle, wordCount, saveTime): + """Add or update recent cache information on a given project. + """ + self._data[str(projPath)] = { + "title": projTitle, + "words": int(wordCount), + "time": int(saveTime), + } + self.saveCache() + return + + def remove(self, projPath): + """Try to remove a path from the recent projects cache. + """ + if self._data.pop(str(projPath), None) is not None: + logger.debug("Removed recent: %s", projPath) + self.saveCache() + return + +# END Class RecentProjects diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 49df331a..ae4f7bab 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -354,10 +354,9 @@ class NWProject(QObject): self._loadProjectLocalisation() # Update recent projects - self.mainConf.updateRecentCache( + self.mainConf.recentProjects.update( self._storage.storagePath, self._data.name, sum(self._data.initCounts), time() ) - self.mainConf.saveRecentCache() # Check the project tree consistency for tItem in self._tree: @@ -424,10 +423,9 @@ class NWProject(QObject): self._storage.runPostSaveTasks(autoSave=autoSave) # Update recent projects - self.mainConf.updateRecentCache( + self.mainConf.recentProjects.update( self._storage.storagePath, self._data.name, sum(self._data.currCounts), saveTime ) - self.mainConf.saveRecentCache() self._storage.writeLockFile() self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self._data.name)) diff --git a/novelwriter/dialogs/projload.py b/novelwriter/dialogs/projload.py index c5568f62..ecd2f9f4 100644 --- a/novelwriter/dialogs/projload.py +++ b/novelwriter/dialogs/projload.py @@ -229,7 +229,7 @@ class GuiProjectLoad(QDialog): ).format(projName) ) if msgYes: - self.mainConf.removeFromRecentCache( + self.mainConf.recentProjects.remove( selList[0].data(self.C_NAME, Qt.UserRole) ) self._populateList() @@ -264,23 +264,17 @@ class GuiProjectLoad(QDialog): def _populateList(self): """Populate the list box with recent project data. """ - dataList = [] - for projPath in self.mainConf.recentProj: - theEntry = self.mainConf.recentProj[projPath] - theTitle = theEntry.get("title", "") - theTime = theEntry.get("time", 0) - theWords = theEntry.get("words", 0) - dataList.append([theTitle, theTime, theWords, projPath]) - self.listBox.clear() - sortList = sorted(dataList, key=lambda x: x[1], reverse=True) - for theTitle, theTime, theWords, projPath in sortList: + dataList = self.mainConf.recentProjects.listEntries() + sortList = sorted(dataList, key=lambda x: x[3], reverse=True) + nwxIcon = self.mainGui.mainTheme.getIcon("proj_nwx") + for path, title, words, time in sortList: newItem = QTreeWidgetItem([""]*4) - newItem.setIcon(self.C_NAME, self.mainGui.mainTheme.getIcon("proj_nwx")) - newItem.setText(self.C_NAME, theTitle) - newItem.setData(self.C_NAME, Qt.UserRole, projPath) - newItem.setText(self.C_COUNT, formatInt(theWords)) - newItem.setText(self.C_TIME, datetime.fromtimestamp(theTime).strftime("%x %X")) + newItem.setIcon(self.C_NAME, nwxIcon) + newItem.setText(self.C_NAME, title) + newItem.setData(self.C_NAME, Qt.UserRole, path) + newItem.setText(self.C_COUNT, formatInt(words)) + newItem.setText(self.C_TIME, datetime.fromtimestamp(time).strftime("%x %X")) newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter) newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter) newItem.setTextAlignment(self.C_TIME, Qt.AlignRight | Qt.AlignVCenter) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index fb77ca8a..09df46fb 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1144,7 +1144,7 @@ class GuiMain(QMainWindow): errors since it is initialised before the GUI itself. """ if self.mainConf.hasError: - self.makeAlert(self.mainConf.getErrData(), nwAlert.ERROR) + self.makeAlert(self.mainConf.errorText(), nwAlert.ERROR) return True return False diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index fdca0b8b..bac296ce 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -19,16 +19,16 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import sys import pytest from shutil import copyfile +from pathlib import Path from mock import causeOSError, MockApp from tools import cmpFiles, writeFile -from novelwriter.config import Config +from novelwriter.config import Config, RecentProjects from novelwriter.constants import nwFiles @@ -37,173 +37,140 @@ def testBaseConfig_Constructor(monkeypatch): """Test config contructor. """ # Linux - monkeypatch.setattr("sys.platform", "linux") - tstConf = Config() - assert tstConf.osLinux is True - assert tstConf.osDarwin is False - assert tstConf.osWindows is False - assert tstConf.osUnknown is False + with monkeypatch.context() as mp: + mp.setattr("sys.platform", "linux") + tstConf = Config() + assert tstConf.osLinux is True + assert tstConf.osDarwin is False + assert tstConf.osWindows is False + assert tstConf.osUnknown is False # macOS - monkeypatch.setattr("sys.platform", "darwin") - tstConf = Config() - assert tstConf.osLinux is False - assert tstConf.osDarwin is True - assert tstConf.osWindows is False - assert tstConf.osUnknown is False + with monkeypatch.context() as mp: + mp.setattr("sys.platform", "darwin") + tstConf = Config() + assert tstConf.osLinux is False + assert tstConf.osDarwin is True + assert tstConf.osWindows is False + assert tstConf.osUnknown is False # Windows - monkeypatch.setattr("sys.platform", "win32") - tstConf = Config() - assert tstConf.osLinux is False - assert tstConf.osDarwin is False - assert tstConf.osWindows is True - assert tstConf.osUnknown is False + with monkeypatch.context() as mp: + mp.setattr("sys.platform", "win32") + tstConf = Config() + assert tstConf.osLinux is False + assert tstConf.osDarwin is False + assert tstConf.osWindows is True + assert tstConf.osUnknown is False # Cygwin - monkeypatch.setattr("sys.platform", "cygwin") - tstConf = Config() - assert tstConf.osLinux is False - assert tstConf.osDarwin is False - assert tstConf.osWindows is True - assert tstConf.osUnknown is False + with monkeypatch.context() as mp: + mp.setattr("sys.platform", "cygwin") + tstConf = Config() + assert tstConf.osLinux is False + assert tstConf.osDarwin is False + assert tstConf.osWindows is True + assert tstConf.osUnknown is False # Other - monkeypatch.setattr("sys.platform", "some_other_os") - tstConf = Config() - assert tstConf.osLinux is False - assert tstConf.osDarwin is False - assert tstConf.osWindows is False - assert tstConf.osUnknown is True + with monkeypatch.context() as mp: + mp.setattr("sys.platform", "some_other_os") + tstConf = Config() + assert tstConf.osLinux is False + assert tstConf.osDarwin is False + assert tstConf.osWindows is False + assert tstConf.osUnknown is True + + # App is single file + with monkeypatch.context() as mp: + mp.setattr("pathlib.Path.is_file", lambda *a: True) + tstConf = Config() + assert tstConf._appPath == tstConf._appRoot # END Test testBaseConfig_Constructor @pytest.mark.base -@pytest.mark.skip -def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): +def testBaseConfig_InitLoadSave(monkeypatch, fncPath, tstPaths): """Test config intialisation. """ tstConf = Config() - confFile = os.path.join(tmpDir, "novelwriter.conf") - testFile = os.path.join(outDir, "baseConfig_novelwriter.conf") - compFile = os.path.join(refDir, "baseConfig_novelwriter.conf") + confFile = fncPath / nwFiles.CONF_FILE + testFile = tstPaths.outDir / "baseConfig_novelwriter.conf" + compFile = tstPaths.refDir / "baseConfig_novelwriter.conf" # Make sure we don't have any old conf file - if os.path.isfile(confFile): - os.unlink(confFile) + if confFile.is_file(): + confFile.unlink() - # Let the config class figure out the path - with monkeypatch.context() as mp: - mp.setattr("PyQt5.QtCore.QStandardPaths.writableLocation", lambda *a: fncDir) - tstConf.initConfig() - assert tstConf._confPath == os.path.join(fncDir, tstConf.appHandle) - assert tstConf._dataPath == os.path.join(fncDir, tstConf.appHandle) - assert not os.path.isfile(confFile) + # Running init against a new oath should write a new config file + tstConf.initConfig(confPath=fncPath, dataPath=fncPath) + assert tstConf._confPath == fncPath + assert tstConf._dataPath == fncPath + assert confFile.exists() - # Fail to make folders - with monkeypatch.context() as mp: - mp.setattr("os.mkdir", causeOSError) + # Check that we have a default file + copyfile(confFile, testFile) + ignore = ("timestamp", "lastnotes", "guilang", "lastpath") + assert cmpFiles(testFile, compFile, ignoreStart=ignore) + tstConf.errorText() # This clears the error cache - tstConfDir = os.path.join(fncDir, "test_conf") - tstConf.initConfig(confPath=tstConfDir, dataPath=tmpDir) - assert tstConf._confPath is None - assert tstConf._dataPath == tmpDir - assert not os.path.isfile(confFile) - - tstDataDir = os.path.join(fncDir, "test_data") - tstConf.initConfig(confPath=tmpDir, dataPath=tstDataDir) - assert tstConf._confPath == tmpDir - assert tstConf._dataPath is None - assert os.path.isfile(confFile) - os.unlink(confFile) - - # Test load/save with no path - tstConf._confPath = None - assert tstConf.loadConfig() is False - assert tstConf.saveConfig() is False - - # Run again and set the paths directly and correctly - # This should create a config file as well - with monkeypatch.context() as mp: - mp.setattr("os.path.expanduser", lambda *a: "") - tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) - assert tstConf._confPath == tmpDir - assert tstConf._dataPath == tmpDir - assert os.path.isfile(confFile) - - copyfile(confFile, testFile) - assert cmpFiles(testFile, compFile, ignoreStart=("timestamp", "lastnotes", "guilang")) - - # Load and save with OSError + # Block saving the file with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) - - assert not tstConf.loadConfig() + assert tstConf.saveConfig() is False assert tstConf.hasError is True - assert tstConf.errData != [] - assert tstConf.getErrData().startswith("Could not") - assert tstConf.hasError is False - assert tstConf.errData == [] + assert tstConf.errorText().startswith("Could not save config file") - assert not tstConf.saveConfig() - assert tstConf.hasError is True - assert tstConf.errData != [] - assert tstConf.getErrData().startswith("Could not") - assert tstConf.hasError is False - assert tstConf.errData == [] - - # Check handling of novelWriter as a package + # Block loading the file with monkeypatch.context() as mp: - tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) - assert tstConf._confPath == tmpDir - assert tstConf._dataPath == tmpDir - appRoot = tstConf._appRoot + mp.setattr("builtins.open", causeOSError) + assert tstConf.loadConfig() is False + assert tstConf.hasError is True + assert tstConf.errorText().startswith("Could not load config file") - mp.setattr("os.path.isfile", lambda *a: True) - tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) - assert tstConf._confPath == tmpDir - assert tstConf._dataPath == tmpDir - assert tstConf._appRoot == os.path.dirname(appRoot) - assert tstConf._appPath == os.path.dirname(appRoot) - - assert tstConf.loadConfig() is True + # Change a few settings, save, reset, and reload + tstConf.guiTheme = "foo" + tstConf.guiSyntax = "bar" assert tstConf.saveConfig() is True - # Test Correcting Quote Settings - origDbl = tstConf.fmtDoubleQuotes - origSng = tstConf.fmtSingleQuotes - orDoDbl = tstConf.doReplaceDQuote - orDoSng = tstConf.doReplaceSQuote + newConf = Config() + newConf.initConfig(confPath=fncPath, dataPath=fncPath) + assert newConf.guiTheme == "foo" + assert newConf.guiSyntax == "bar" + # Test Correcting Quote Settings tstConf.fmtDoubleQuotes = ["\"", "\""] tstConf.fmtSingleQuotes = ["'", "'"] tstConf.doReplaceDQuote = True tstConf.doReplaceSQuote = True assert tstConf.saveConfig() is True - assert tstConf.loadConfig() is True - assert tstConf.doReplaceDQuote is False - assert tstConf.doReplaceSQuote is False + assert newConf.loadConfig() is True + assert newConf.doReplaceDQuote is False + assert newConf.doReplaceSQuote is False - tstConf.fmtDoubleQuotes = origDbl - tstConf.fmtSingleQuotes = origSng - tstConf.doReplaceDQuote = orDoDbl - tstConf.doReplaceSQuote = orDoSng - assert tstConf.saveConfig() is True +# END Test testBaseConfig_InitLoadSave + + +@pytest.mark.base +def testBaseConfig_Localisation(fncPath, tstPaths): + """Test localisation. + """ + tstConf = Config() + tstConf.initConfig(confPath=fncPath, dataPath=fncPath) # Localisation # ============ - i18nDir = os.path.join(fncDir, "i18n") - os.mkdir(i18nDir) - os.mkdir(os.path.join(i18nDir, "stuff")) + i18nDir = fncPath / "i18n" + i18nDir.mkdir() tstConf._nwLangPath = i18nDir - copyfile(os.path.join(filesDir, "nw_en_GB.qm"), os.path.join(i18nDir, "nw_en_GB.qm")) - writeFile(os.path.join(i18nDir, "nw_en_GB.ts"), "") - writeFile(os.path.join(i18nDir, "nw_abcd.qm"), "") + copyfile(tstPaths.filesDir / "nw_en_GB.qm", i18nDir / "nw_en_GB.qm") + writeFile(i18nDir / "nw_en_GB.ts", "") + writeFile(i18nDir / "nw_abcd.qm", "") tstApp = MockApp() tstConf.initLocalisation(tstApp) @@ -217,82 +184,55 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): assert theList == [] # Add Language - copyfile(os.path.join(filesDir, "nw_en_GB.qm"), os.path.join(i18nDir, "nw_fr.qm")) - writeFile(os.path.join(i18nDir, "nw_fr.ts"), "") + copyfile(tstPaths.filesDir / "nw_en_GB.qm", i18nDir / "nw_fr.qm") + writeFile(i18nDir / "nw_fr.ts", "") theList = tstConf.listLanguages(tstConf.LANG_NW) assert theList == [("en_GB", "British English"), ("fr", "Français")] - copyfile(confFile, testFile) - assert cmpFiles(testFile, compFile, ignoreStart=("timestamp", "lastnotes", "guilang")) - -# END Test testBaseConfig_Init +# END Test testBaseConfig_Localisation @pytest.mark.base -def testBaseConfig_RecentCache(monkeypatch, tmpConf, tmpDir, fncDir): - """Test recent cache file. +def testBaseConfig_Methods(tmpConf, tmpPath): + """Check class methods. """ - # Add a couple of values - pathOne = os.path.join(fncDir, "projPathOne", nwFiles.PROJ_FILE) - pathTwo = os.path.join(fncDir, "projPathTwo", nwFiles.PROJ_FILE) - assert tmpConf.updateRecentCache(pathOne, "Proj One", 100, 1600002000) - assert tmpConf.updateRecentCache(pathTwo, "Proj Two", 200, 1600005600) - assert tmpConf.recentProj == { - pathOne: {"time": 1600002000, "title": "Proj One", "words": 100}, - pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200}, - } + # Data Path + assert tmpConf.dataPath() == tmpPath + assert tmpConf.dataPath("stuff") == tmpPath / "stuff" - # Fail to Save - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert not tmpConf.saveRecentCache() + # Assets Path + appPath = tmpConf._appPath + assert tmpConf.assetPath() == appPath / "assets" + assert tmpConf.assetPath("stuff") == appPath / "assets" / "stuff" - # Save Proper - cacheFile = os.path.join(tmpDir, nwFiles.RECENT_FILE) - assert tmpConf.saveRecentCache() - assert tmpConf.saveRecentCache() - assert os.path.isfile(cacheFile) + # Last Path + assert tmpConf.lastPath() == tmpPath - # Fail to Load - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - tmpConf.recentProj = {} - assert not tmpConf.loadRecentCache() - assert tmpConf.recentProj == {} + tmpStuff = tmpPath / "stuff" + tmpStuff.mkdir() + tmpConf.setLastPath(tmpStuff) + assert tmpConf.lastPath() == tmpStuff - # Load Proper - tmpConf.recentProj = {} - assert tmpConf.loadRecentCache() - assert tmpConf.recentProj == { - pathOne: {"time": 1600002000, "title": "Proj One", "words": 100}, - pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200}, - } + fileStuff = tmpStuff / "more_stuff.txt" + fileStuff.write_text("Stuff") + tmpConf.setLastPath(fileStuff) + assert tmpConf.lastPath() == tmpStuff - # Remove Non-Existent Entry - assert not tmpConf.removeFromRecentCache("stuff") - assert tmpConf.recentProj == { - pathOne: {"time": 1600002000, "title": "Proj One", "words": 100}, - pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200}, - } + fileStuff.unlink() + tmpStuff.rmdir() + assert tmpConf.lastPath() == Path.home().absolute() - # Remove Second Entry - assert tmpConf.removeFromRecentCache(pathTwo) - assert tmpConf.recentProj == { - pathOne: {"time": 1600002000, "title": "Proj One", "words": 100}, - } + # Recent Projects + assert isinstance(tmpConf.recentProjects, RecentProjects) -# END Test testBaseConfig_RecentCache +# END Test testBaseConfig_Methods @pytest.mark.base -def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): +def testBaseConfig_SettersGetters(tmpConf): """Set various sizes and positions """ - confFile = os.path.join(tmpDir, "novelwriter.conf") - testFile = os.path.join(outDir, "baseConfig_novelwriter.conf") - compFile = os.path.join(refDir, "baseConfig_novelwriter.conf") - # GUI Scaling # =========== @@ -439,17 +379,6 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): tmpConf.setViewSynopsis(True) assert tmpConf.viewSynopsis is True - # Check Final File - # ================ - - assert tmpConf.confChanged is True - assert tmpConf.saveConfig() is True - assert tmpConf.confChanged is False - - copyfile(confFile, testFile) - ignore = ("timestamp", "lastnotes", "guilang", "lastpath") - assert cmpFiles(testFile, compFile, ignoreStart=ignore) - # END Test testBaseConfig_SettersGetters @@ -479,3 +408,68 @@ def testBaseConfig_Internal(monkeypatch, tmpConf): assert tmpConf.hasEnchant is False # END Test testBaseConfig_Internal + + +@pytest.mark.base +def testBaseConfig_RecentCache(monkeypatch, fncPath): + """Test recent cache file. + """ + cacheFile = fncPath / nwFiles.RECENT_FILE + recent = RecentProjects(fncPath) + + # Load when there is no file should pass, but load nothing + assert not cacheFile.exists() + assert recent.loadCache() is True + assert recent.listEntries() == [] + + # Add a couple of values + pathOne = fncPath / "projPathOne" / nwFiles.PROJ_FILE + pathTwo = fncPath / "projPathTwo" / nwFiles.PROJ_FILE + + recent.update(pathOne, "Proj One", 100, 1600002000) + recent.update(pathTwo, "Proj Two", 200, 1600005600) + assert recent.listEntries() == [ + (str(pathOne), "Proj One", 100, 1600002000), + (str(pathTwo), "Proj Two", 200, 1600005600), + ] + assert cacheFile.exists() + cacheFile.unlink() + assert not cacheFile.exists() + + # Fail to Save + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + assert recent.saveCache() is False + assert not cacheFile.exists() + + # Save Proper + assert recent.saveCache() is True + assert cacheFile.exists() + + # Fail to Load + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + assert recent.loadCache() is False + assert recent.listEntries() == [] + + # Load Proper + assert recent.loadCache() is True + assert recent.listEntries() == [ + (str(pathOne), "Proj One", 100, 1600002000), + (str(pathTwo), "Proj Two", 200, 1600005600), + ] + + # Remove Non-Existent Entry + recent.remove("stuff") + assert recent.listEntries() == [ + (str(pathOne), "Proj One", 100, 1600002000), + (str(pathTwo), "Proj Two", 200, 1600005600), + ] + + # Remove Second Entry + recent.remove(pathTwo) + assert recent.listEntries() == [ + (str(pathOne), "Proj One", 100, 1600002000), + ] + +# END Test testBaseConfig_RecentCache