""" novelWriter – Config Class Tester ================================= This file is a part of novelWriter Copyright 2018–2021, Veronica Berglyd Olsen This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import os import sys import pytest from shutil import copyfile from mock import causeOSError, MockApp from tools import cmpFiles, writeFile from novelwriter.config import Config from novelwriter.constants import nwConst, nwFiles @pytest.mark.base 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 # 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 # 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 # 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 # 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 # END Test testBaseConfig_Constructor @pytest.mark.base def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): """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") # Make sure we don't have any old conf file if os.path.isfile(confFile): os.unlink(confFile) # Let the config class figure out the path with monkeypatch.context() as mp: mp.setattr("PyQt5.QtCore.QStandardPaths.writableLocation", lambda *a: fncDir) tstConf.verQtValue = 50600 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) tstConf.verQtValue = 50000 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) # Fail to make folders with monkeypatch.context() as mp: mp.setattr("os.mkdir", causeOSError) 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.spellTool = nwConst.SP_INTERNAL 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, [2, 9, 10]) # Load and save with OSError with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) assert not tstConf.loadConfig() assert tstConf.hasError is True assert tstConf.errData != [] assert tstConf.getErrData().startswith("Could not") assert tstConf.hasError is False assert tstConf.errData == [] 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 with monkeypatch.context() as mp: tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) assert tstConf.confPath == tmpDir assert tstConf.dataPath == tmpDir appRoot = tstConf.appRoot 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 assert tstConf.saveConfig() is True # Test Correcting Quote Settings origDbl = tstConf.fmtDoubleQuotes origSng = tstConf.fmtSingleQuotes orDoDbl = tstConf.doReplaceDQuote orDoSng = tstConf.doReplaceSQuote 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 tstConf.fmtDoubleQuotes = origDbl tstConf.fmtSingleQuotes = origSng tstConf.doReplaceDQuote = orDoDbl tstConf.doReplaceSQuote = orDoSng assert tstConf.saveConfig() is True # Test Correcting icon theme origIcons = tstConf.guiIcons tstConf.guiIcons = "typicons_colour_dark" assert tstConf.saveConfig() is True assert tstConf.loadConfig() is True assert tstConf.guiIcons == "typicons_dark" tstConf.guiIcons = "typicons_grey_dark" assert tstConf.saveConfig() is True assert tstConf.loadConfig() is True assert tstConf.guiIcons == "typicons_dark" tstConf.guiIcons = "typicons_colour_light" assert tstConf.saveConfig() is True assert tstConf.loadConfig() is True assert tstConf.guiIcons == "typicons_light" tstConf.guiIcons = "typicons_grey_light" assert tstConf.saveConfig() is True assert tstConf.loadConfig() is True assert tstConf.guiIcons == "typicons_light" tstConf.guiIcons = origIcons assert tstConf.saveConfig() # Localisation # ============ i18nDir = os.path.join(fncDir, "i18n") os.mkdir(i18nDir) os.mkdir(os.path.join(i18nDir, "stuff")) 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"), "") tstApp = MockApp() tstConf.initLocalisation(tstApp) # Check Lists theList = tstConf.listLanguages(tstConf.LANG_NW) assert theList == [("en_GB", "British English")] theList = tstConf.listLanguages(tstConf.LANG_PROJ) assert theList == [("en", "English")] theList = tstConf.listLanguages(None) 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"), "") theList = tstConf.listLanguages(tstConf.LANG_NW) assert theList == [("en_GB", "British English"), ("fr", "Français")] copyfile(confFile, testFile) assert cmpFiles(testFile, compFile, [2, 9, 10]) # END Test testBaseConfig_Init @pytest.mark.base def testBaseConfig_RecentCache(monkeypatch, tmpConf, tmpDir, fncDir): """Test recent cache file. """ # Check failing tmpConf.dataPath = None assert not tmpConf.loadRecentCache() assert not tmpConf.saveRecentCache() tmpConf.dataPath = tmpDir # 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}, } # Fail to Save with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) assert not tmpConf.saveRecentCache() # Save Proper cacheFile = os.path.join(tmpDir, nwFiles.RECENT_FILE) assert tmpConf.saveRecentCache() assert tmpConf.saveRecentCache() assert os.path.isfile(cacheFile) # Fail to Load with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) tmpConf.recentProj = {} assert not tmpConf.loadRecentCache() assert tmpConf.recentProj == {} # 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}, } # 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}, } # Remove Second Entry assert tmpConf.removeFromRecentCache(pathTwo) assert tmpConf.recentProj == { pathOne: {"time": 1600002000, "title": "Proj One", "words": 100}, } # END Test testBaseConfig_RecentCache @pytest.mark.base def testBaseConfig_SetPath(tmpConf, tmpDir): """Test path setters. """ # Conf Path assert tmpConf.setConfPath(None) assert not tmpConf.setConfPath(os.path.join("somewhere", "over", "the", "rainbow")) assert tmpConf.setConfPath(os.path.join(tmpDir, "novelwriter.conf")) assert tmpConf.confPath == tmpDir assert tmpConf.confFile == "novelwriter.conf" assert not tmpConf.confChanged # Data Path assert tmpConf.setDataPath(None) assert not tmpConf.setDataPath(os.path.join("somewhere", "over", "the", "rainbow")) assert tmpConf.setDataPath(tmpDir) assert tmpConf.dataPath == tmpDir assert not tmpConf.confChanged # Last Path assert tmpConf.setLastPath(None) assert tmpConf.lastPath == "" assert tmpConf.setLastPath(os.path.join(tmpDir, "file.tmp")) assert tmpConf.lastPath == tmpDir assert tmpConf.setLastPath("") assert tmpConf.lastPath == "" # END Test testBaseConfig_SetPath @pytest.mark.base def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): """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 # =========== tmpConf.guiScale = 1.0 assert tmpConf.pxInt(10) == 10 assert tmpConf.pxInt(13) == 13 assert tmpConf.rpxInt(10) == 10 assert tmpConf.rpxInt(13) == 13 tmpConf.guiScale = 2.0 assert tmpConf.pxInt(10) == 20 assert tmpConf.pxInt(13) == 26 assert tmpConf.rpxInt(10) == 5 assert tmpConf.rpxInt(13) == 6 # Setter + Getter Combos # ====================== # Window Size tmpConf.guiScale = 1.0 assert tmpConf.setWinSize(1205, 655) assert not tmpConf.confChanged tmpConf.guiScale = 2.0 assert tmpConf.setWinSize(70, 70) assert tmpConf.getWinSize() == [70, 70] assert tmpConf.winGeometry == [35, 35] tmpConf.guiScale = 1.0 assert tmpConf.setWinSize(70, 70) assert tmpConf.getWinSize() == [70, 70] assert tmpConf.winGeometry == [70, 70] assert tmpConf.setWinSize(1200, 650) # Preferences Size tmpConf.guiScale = 2.0 assert tmpConf.setPreferencesSize(70, 70) assert tmpConf.getPreferencesSize() == [70, 70] assert tmpConf.prefGeometry == [35, 35] tmpConf.guiScale = 1.0 assert tmpConf.setPreferencesSize(70, 70) assert tmpConf.getPreferencesSize() == [70, 70] assert tmpConf.prefGeometry == [70, 70] assert tmpConf.setPreferencesSize(700, 615) # Project Tree Columns tmpConf.guiScale = 2.0 assert tmpConf.setTreeColWidths([10, 20, 25]) assert tmpConf.getTreeColWidths() == [10, 20, 24] assert tmpConf.treeColWidth == [5, 10, 12] tmpConf.guiScale = 1.0 assert tmpConf.setTreeColWidths([10, 20, 25]) assert tmpConf.getTreeColWidths() == [10, 20, 25] assert tmpConf.treeColWidth == [10, 20, 25] assert tmpConf.setTreeColWidths([200, 50, 30]) # Novel Tree Columns tmpConf.guiScale = 2.0 assert tmpConf.setNovelColWidths([10, 20]) assert tmpConf.getNovelColWidths() == [10, 20] assert tmpConf.novelColWidth == [5, 10] tmpConf.guiScale = 1.0 assert tmpConf.setNovelColWidths([10, 20]) assert tmpConf.getNovelColWidths() == [10, 20] assert tmpConf.novelColWidth == [10, 20] assert tmpConf.setNovelColWidths([200, 50]) # Project Settings Tree Columns tmpConf.guiScale = 2.0 assert tmpConf.setProjColWidths([10, 20, 30]) assert tmpConf.getProjColWidths() == [10, 20, 30] assert tmpConf.projColWidth == [5, 10, 15] tmpConf.guiScale = 1.0 assert tmpConf.setProjColWidths([10, 20, 30]) assert tmpConf.getProjColWidths() == [10, 20, 30] assert tmpConf.projColWidth == [10, 20, 30] assert tmpConf.setProjColWidths([200, 60, 140]) # Main Pane Splitter tmpConf.guiScale = 2.0 assert tmpConf.setMainPanePos([200, 700]) assert tmpConf.getMainPanePos() == [200, 700] assert tmpConf.mainPanePos == [100, 350] tmpConf.guiScale = 1.0 assert tmpConf.setMainPanePos([200, 700]) assert tmpConf.getMainPanePos() == [200, 700] assert tmpConf.mainPanePos == [200, 700] assert tmpConf.setMainPanePos([300, 800]) # Doc Pane Splitter tmpConf.guiScale = 2.0 assert tmpConf.setDocPanePos([300, 300]) assert tmpConf.getDocPanePos() == [300, 300] assert tmpConf.docPanePos == [150, 150] tmpConf.guiScale = 1.0 assert tmpConf.setDocPanePos([300, 300]) assert tmpConf.getDocPanePos() == [300, 300] assert tmpConf.docPanePos == [300, 300] assert tmpConf.setDocPanePos([400, 400]) # View Pane Splitter tmpConf.guiScale = 2.0 assert tmpConf.setViewPanePos([400, 250]) assert tmpConf.getViewPanePos() == [400, 250] assert tmpConf.viewPanePos == [200, 125] tmpConf.guiScale = 1.0 assert tmpConf.setViewPanePos([400, 250]) assert tmpConf.getViewPanePos() == [400, 250] assert tmpConf.viewPanePos == [400, 250] assert tmpConf.setViewPanePos([500, 150]) # Outline Pane Splitter tmpConf.guiScale = 2.0 assert tmpConf.setOutlinePanePos([400, 250]) assert tmpConf.getOutlinePanePos() == [400, 250] assert tmpConf.outlnPanePos == [200, 125] tmpConf.guiScale = 1.0 assert tmpConf.setOutlinePanePos([400, 250]) assert tmpConf.getOutlinePanePos() == [400, 250] assert tmpConf.outlnPanePos == [400, 250] assert tmpConf.setOutlinePanePos([500, 150]) # Getters Only # ============ tmpConf.guiScale = 1.0 assert tmpConf.getTextWidth() == 600 assert tmpConf.getTextMargin() == 40 assert tmpConf.getTabWidth() == 40 assert tmpConf.getFocusWidth() == 800 tmpConf.guiScale = 2.0 assert tmpConf.getTextWidth() == 1200 assert tmpConf.getTextMargin() == 80 assert tmpConf.getTabWidth() == 80 assert tmpConf.getFocusWidth() == 1600 # Flag Setters # ============ assert not tmpConf.setShowRefPanel(False) assert not tmpConf.showRefPanel assert tmpConf.setShowRefPanel(True) assert not tmpConf.setViewComments(False) assert not tmpConf.viewComments assert tmpConf.setViewComments(True) assert not tmpConf.setViewSynopsis(False) assert not tmpConf.viewSynopsis assert tmpConf.setViewSynopsis(True) # Check Final File # ================ assert tmpConf.confChanged assert tmpConf.saveConfig() assert not tmpConf.confChanged copyfile(confFile, testFile) assert cmpFiles(testFile, compFile, [2, 9, 10]) # END Test testBaseConfig_SettersGetters @pytest.mark.base def testBaseConfig_Internal(monkeypatch, tmpConf): """Check internal functions. """ # Function _packList assert tmpConf._packList(["A", 1, 2.0, None, False]) == "A, 1, 2.0, None, False" # Function _checkNone assert tmpConf._checkNone(None) is None assert tmpConf._checkNone("None") is None assert tmpConf._checkNone("none") is None assert tmpConf._checkNone("NONE") is None assert tmpConf._checkNone("NoNe") is None assert tmpConf._checkNone(123456) == 123456 # Function _checkOptionalPackages # (Assumes enchant package exists and is importable) tmpConf._checkOptionalPackages() assert tmpConf.hasEnchant is True with monkeypatch.context() as mp: mp.setitem(sys.modules, "enchant", None) tmpConf._checkOptionalPackages() assert tmpConf.hasEnchant is False # END Test testBaseConfig_Internal