""" novelWriter – Config Class Tester ================================= This file is a part of novelWriter Copyright (C) 2020 Veronica Berglyd Olsen and novelWriter contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ # noqa from __future__ import annotations import datetime import json import sys from pathlib import Path from shutil import copyfile import pytest from novelwriter import CONFIG from novelwriter.config import Config, RecentPaths, RecentProjects from novelwriter.constants import nwFiles from novelwriter.core.project import NWProject from tests.mocked import MockApp, causeOSError from tests.tools import cmpFiles, writeFile @pytest.mark.base def testBaseConfig_Constructor(monkeypatch): """Test config constructor.""" # Linux 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 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 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 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 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 @pytest.mark.base def testBaseConfig_InitLoadSave(monkeypatch, fncPath, tstPaths): """Test config initialisation.""" tstConf = Config() 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 confFile.is_file(): confFile.unlink() # Running init + load against a new path should write a new config file tstConf.initConfig(confPath=fncPath, dataPath=fncPath) assert tstConf._confPath == fncPath assert tstConf._dataPath == fncPath tstConf.loadConfig() assert confFile.exists() # Check that we have a default file copyfile(confFile, testFile) ignore = ( "timestamp", "lastnotes", "localisation", "lastpath", "backuppath", "font", "textfont" ) assert cmpFiles(testFile, compFile, ignoreStart=ignore) tstConf.errorText() # This clears the error cache # Block saving the file with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) assert tstConf.saveConfig() is False assert tstConf.hasError is True assert tstConf.errorText().startswith("Could not save config file") # Block loading the file with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) assert tstConf.loadConfig() is False assert tstConf.hasError is True assert tstConf.errorText().startswith("Could not load config file") # Change a few settings, save, reset, and reload tstConf.lightTheme = "foo" tstConf.darkTheme = "bar" assert tstConf.saveConfig() is True newConf = Config() newConf.initConfig(confPath=fncPath, dataPath=fncPath) newConf.loadConfig() assert newConf.lightTheme == "foo" assert newConf.darkTheme == "bar" # Test Correcting Quote Settings tstConf.fmtDQuoteOpen = '"' tstConf.fmtDQuoteClose = '"' tstConf.fmtSQuoteOpen = "'" tstConf.fmtSQuoteClose = "'" tstConf.doReplaceDQuote = True tstConf.doReplaceSQuote = True assert tstConf.saveConfig() is True assert newConf.loadConfig() is True assert newConf.doReplaceDQuote is False assert newConf.doReplaceSQuote is False @pytest.mark.base def testBaseConfig_Localisation(fncPath, tstPaths): """Test localisation.""" tstConf = Config() tstConf.initConfig(confPath=fncPath, dataPath=fncPath) # Localisation # ============ i18nDir = fncPath / "i18n" i18nDir.mkdir() tstConf._nwLangPath = i18nDir 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) # type: ignore # Check Lists languages = tstConf.listLanguages(tstConf.LANG_NW) assert languages == [("en_GB", "British English")] languages = tstConf.listLanguages(tstConf.LANG_PROJ) assert languages == [("en_GB", "British English")] languages = tstConf.listLanguages(None) # type: ignore assert languages == [] # Add Language copyfile(tstPaths.filesDir / "nw_en_GB.qm", i18nDir / "nw_fr.qm") writeFile(i18nDir / "nw_fr.ts", "") languages = tstConf.listLanguages(tstConf.LANG_NW) assert languages == [("en_GB", "British English"), ("fr", "Français")] # Date Formats # Checks for bug #2325 ts = datetime.datetime.fromtimestamp(1746370775) CONFIG._dShortDate = "dd/MM/yyyy" CONFIG._dShortDateTime = "dd/MM/yyyy HH:mm" assert CONFIG.localDate(ts) == ts.strftime("%d/%m/%Y") assert CONFIG.localDateTime(ts) == ts.strftime("%d/%m/%Y %H:%M") @pytest.mark.base def testBaseConfig_Methods(fncPath): """Check class methods.""" tstConf = Config() tstConf.initConfig(confPath=fncPath, dataPath=fncPath) # Home Path assert tstConf.homePath() == Path.home().absolute() # Data Path assert tstConf.dataPath() == fncPath assert tstConf.dataPath("stuff") == fncPath / "stuff" # Assets Path appPath = tstConf._appPath assert tstConf.assetPath() == appPath / "assets" assert tstConf.assetPath("stuff") == appPath / "assets" / "stuff" # Last Path assert tstConf.lastPath("project") == Path.home().absolute() tmpStuff = fncPath / "stuff" tmpStuff.mkdir() tstConf.setLastPath("project", tmpStuff) assert tstConf.lastPath("project") == tmpStuff fileStuff = tmpStuff / "more_stuff.txt" fileStuff.write_text("Stuff") tstConf.setLastPath("project", fileStuff) assert tstConf.lastPath("project") == tmpStuff fileStuff.unlink() tmpStuff.rmdir() assert tstConf.lastPath("project") == Path.home().absolute() # Backup Path assert tstConf.backupPath() == tstConf._backPath tstConf.setBackupPath(fncPath) assert tstConf.backupPath() == fncPath # Recent Projects assert isinstance(tstConf.recentProjects, RecentProjects) # Last Author assert CONFIG.lastAuthor == "" CONFIG.setLastAuthor(" Jane Doe ") assert CONFIG.lastAuthor == "Jane Doe" @pytest.mark.base def testBaseConfig_SettersGetters(fncPath): """Set various sizes and positions.""" tstConf = Config() tstConf.initConfig(confPath=fncPath, dataPath=fncPath) # Setter + Getter Combos # ====================== # Window Size tstConf.setMainWinSize(1205, 655) assert tstConf.mainWinSize == [1200, 650] tstConf.setMainWinSize(70, 70) assert tstConf.mainWinSize == [70, 70] assert tstConf.mainWinSize == [70, 70] tstConf.setMainWinSize(1200, 650) # Welcome Window Size tstConf.setWelcomeWinSize(70, 70) assert tstConf.welcomeWinSize == [70, 70] assert tstConf.welcomeWinSize == [70, 70] tstConf.setWelcomeWinSize(800, 500) # Preferences Size tstConf.setPreferencesWinSize(70, 70) assert tstConf.prefsWinSize == [70, 70] assert tstConf.prefsWinSize == [70, 70] tstConf.setPreferencesWinSize(700, 615) # Getters Only # ============ assert tstConf.getTextWidth(False) == 700 assert tstConf.getTextWidth(True) == 800 @pytest.mark.base def testBaseConfig_Internal(monkeypatch, fncPath): """Check internal functions.""" tstConf = Config() tstConf.initConfig(confPath=fncPath, dataPath=fncPath) # Function _packList assert tstConf._packList(["A", 1, 2.0, None, False]) == "A, 1, 2.0, None, False" # Function _checkOptionalPackages # (Assumes enchant package exists and is importable) tstConf._checkOptionalPackages() assert tstConf.hasEnchant is True with monkeypatch.context() as mp: mp.setitem(sys.modules, "enchant", None) tstConf._checkOptionalPackages() assert tstConf.hasEnchant is False @pytest.mark.base def testBaseConfig_RecentCache(monkeypatch, tstPaths, nwGUI): """Test recent cache file.""" cacheFile = tstPaths.cnfDir / nwFiles.RECENT_FILE recent = RecentProjects(CONFIG) # 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 = tstPaths.cnfDir / "projPathOne" / nwFiles.PROJ_FILE pathTwo = tstPaths.cnfDir / "projPathTwo" / nwFiles.PROJ_FILE prjOne = NWProject() prjTwo = NWProject() prjOne.data.setUuid(None) prjTwo.data.setUuid(None) prjOne.data.setName("Proj One") prjTwo.data.setName("Proj Two") prjOne.data.setCurrCounts(100, 0) prjTwo.data.setCurrCounts(200, 0) recent.update(pathOne, prjOne.data, 1600002000) recent.update(pathTwo, prjTwo.data, 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), ] # Pass Invalid recent.update(None, None, 0) # type: ignore recent.update(None, None, 0) # type: ignore 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), ] @pytest.mark.base def testBaseConfig_RecentPaths(monkeypatch, tstPaths): """Test recent paths file.""" cacheFile = tstPaths.cnfDir / nwFiles.RECENT_PATH recent = RecentPaths(CONFIG) # Load when there is no file should pass, but load nothing assert not cacheFile.exists() assert recent.loadCache() is True assert recent._data == {} # Set valid paths recent.setPath("default", tstPaths.cnfDir / "default") recent.setPath("project", tstPaths.cnfDir / "project") recent.setPath("import", tstPaths.cnfDir / "import") recent.setPath("outline", tstPaths.cnfDir / "outline") recent.setPath("stats", tstPaths.cnfDir / "stats") # Set invalid path recent.setPath("foobar", tstPaths.cnfDir / "foobar") # Check valid paths assert recent.getPath("default") == str(tstPaths.cnfDir / "default") assert recent.getPath("project") == str(tstPaths.cnfDir / "project") assert recent.getPath("import") == str(tstPaths.cnfDir / "import") assert recent.getPath("outline") == str(tstPaths.cnfDir / "outline") assert recent.getPath("stats") == str(tstPaths.cnfDir / "stats") # Check invalid path assert recent.getPath("foobar") is None # Check file expected = { "default": str(tstPaths.cnfDir / "default"), "project": str(tstPaths.cnfDir / "project"), "import": str(tstPaths.cnfDir / "import"), "outline": str(tstPaths.cnfDir / "outline"), "stats": str(tstPaths.cnfDir / "stats"), } assert cacheFile.exists() assert json.loads(cacheFile.read_text()) == expected # Clear and reload recent._data = {} recent.loadCache() assert recent._data == expected # Check error handling with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) assert recent.saveCache() is False assert recent.loadCache() is False