Some checks failed
Syntax / Linting and Type Checking (push) Has been cancelled
Tests / Linux Python 3.11 (push) Has been cancelled
Tests / Linux Python 3.12 (push) Has been cancelled
Tests / Linux Python 3.13 (push) Has been cancelled
Tests / Linux Python 3.14 (push) Has been cancelled
Tests / Linux Python 3.15 (push) Has been cancelled
Tests / MacOS Python 3.14 (push) Has been cancelled
Tests / Windows Python 3.14 (push) Has been cancelled
Tests / PR Coverage Report (push) Has been cancelled
Tests / Action Coverage Report (push) Has been cancelled
Tests / Ubuntu 24.04 & Sys Packages (push) Has been cancelled
Tests / Fedora 44 & Sys Packages (push) Has been cancelled
- Rename test fixture files: config_novelwriter.toml -> config_umbraedit.toml - Rename test fixture files: old_novelwriter.conf -> old_umbraedit.conf - Rename test fixture files: nw_en_GB.qm -> ue_en_GB.qm - Rename asset files: novelwriter-text-*.svg -> umbraedit-text-*.svg - Rename asset files: novelwriter.svg/ico -> umbraedit.svg/ico - Rename asset files: x-novelwriter-project.* -> x-umbraedit-project.* - Fix test references to _nwLangPath -> _ueLangPath - Bump version to 1.0.0 (first UmbraEdit release) - All 735 tests pass, 2 skipped
741 lines
24 KiB
Python
741 lines
24 KiB
Python
"""
|
|
UmbraEdit - Config Tests
|
|
==========================
|
|
|
|
This file is a part of UmbraEdit.
|
|
|
|
Copyright (C) 2020 Veronica Berglyd Olsen and UmbraEdit 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 <https://www.gnu.org/licenses/>.
|
|
""" # noqa
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
import json
|
|
import sys
|
|
import tomllib
|
|
|
|
from pathlib import Path
|
|
from shutil import copyfile
|
|
|
|
import pytest
|
|
|
|
from PyQt6.QtCore import QRect, Qt
|
|
from PyQt6.QtGui import QFont, QFontDatabase
|
|
|
|
from umbraedit import CONFIG
|
|
from umbraedit.config import MOVED_CONFIGS, Config, RecentPaths, RecentProjects
|
|
from umbraedit.constants import nwConst, nwFiles
|
|
from umbraedit.core.project import NWProject
|
|
from umbraedit.enum import nwTheme
|
|
from umbraedit.formats.configparser import NConfigParser
|
|
from umbraedit.formats.tomlparser import NTomlParser, T_TomlConfig
|
|
from umbraedit.guimain import GuiMain
|
|
|
|
from tests.helpers import TstPaths, cmpFiles, writeFile
|
|
from tests.mocked import MockApp, causeOSError
|
|
|
|
|
|
@pytest.mark.base
|
|
def testConfig_Constructor(monkeypatch: pytest.MonkeyPatch):
|
|
"""Test config constructor."""
|
|
# Linux
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr("sys.platform", "linux")
|
|
conf = Config()
|
|
assert conf.osLinux is True
|
|
assert conf.osDarwin is False
|
|
assert conf.osWindows is False
|
|
assert conf.osUnknown is False
|
|
|
|
# MacOS
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr("sys.platform", "darwin")
|
|
conf = Config()
|
|
assert conf.osLinux is False
|
|
assert conf.osDarwin is True
|
|
assert conf.osWindows is False
|
|
assert conf.osUnknown is False
|
|
|
|
# Windows
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr("sys.platform", "win32")
|
|
conf = Config()
|
|
assert conf.osLinux is False
|
|
assert conf.osDarwin is False
|
|
assert conf.osWindows is True
|
|
assert conf.osUnknown is False
|
|
|
|
# Cygwin
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr("sys.platform", "cygwin")
|
|
conf = Config()
|
|
assert conf.osLinux is False
|
|
assert conf.osDarwin is False
|
|
assert conf.osWindows is True
|
|
assert conf.osUnknown is False
|
|
|
|
# Other
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr("sys.platform", "some_other_os")
|
|
conf = Config()
|
|
assert conf.osLinux is False
|
|
assert conf.osDarwin is False
|
|
assert conf.osWindows is False
|
|
assert conf.osUnknown is True
|
|
|
|
|
|
@pytest.mark.base
|
|
def testConfig_BuildMeta(monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, tstPaths: TstPaths):
|
|
"""Test parsing of the build meta.toml file, both as the checked-in
|
|
placeholder and as stamped by the build scripts for a release.
|
|
"""
|
|
metaFile = tstPaths.tmpDir / "meta.toml"
|
|
monkeypatch.setattr("umbraedit.config.Config.assetPath", lambda *a: metaFile)
|
|
|
|
# Placeholder, as checked into the repo
|
|
writeFile(
|
|
metaFile, ('[Build]\ntimestamp = ""\ntype = "testing"\nformat = "source"\ninstall_source = "repository"\n')
|
|
)
|
|
conf = Config()
|
|
assert conf.buildTime == ""
|
|
assert conf.buildType == "testing"
|
|
assert conf.buildFormat == "source"
|
|
assert conf.installSource == "repository"
|
|
|
|
# Stamped by the build scripts ahead of packaging
|
|
writeFile(
|
|
metaFile,
|
|
(
|
|
"[Build]\n"
|
|
'timestamp = "2026-09-26T19:33:58+02:00"\n'
|
|
'type = "stable"\n'
|
|
'format = "debian"\n'
|
|
'install_source = "cloudsmith"\n'
|
|
),
|
|
)
|
|
conf._parseBuildMeta()
|
|
assert conf.buildTime == "2026-09-26T19:33:58+02:00"
|
|
assert conf.buildType == "stable"
|
|
assert conf.buildFormat == "debian"
|
|
assert conf.installSource == "cloudsmith"
|
|
|
|
# An error while reading the file must be caught and logged, and
|
|
# must leave the previously parsed values untouched
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr("builtins.open", causeOSError)
|
|
caplog.clear()
|
|
conf._parseBuildMeta()
|
|
assert "OSError" in caplog.text
|
|
|
|
assert conf.buildTime == "2026-09-26T19:33:58+02:00"
|
|
assert conf.buildType == "stable"
|
|
assert conf.buildFormat == "debian"
|
|
assert conf.installSource == "cloudsmith"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testConfig_InitLoadSave(monkeypatch: pytest.MonkeyPatch, fncPath: Path, tstPaths: TstPaths):
|
|
"""Test config initialisation."""
|
|
conf = Config()
|
|
|
|
confFile = fncPath / nwFiles.CONF_FILE
|
|
testFile = tstPaths.outDir / "config_umbraedit.toml"
|
|
compFile = tstPaths.refDir / "config_umbraedit.toml"
|
|
|
|
# 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
|
|
conf.initConfig(confPath=fncPath, dataPath=fncPath)
|
|
assert conf._confPath == fncPath
|
|
assert conf._dataPath == fncPath
|
|
conf.loadConfig()
|
|
assert confFile.exists()
|
|
|
|
# Check that we have a default file
|
|
copyfile(confFile, testFile)
|
|
ignore = (
|
|
"timeStamp",
|
|
"font",
|
|
"localisation",
|
|
"lastNotes",
|
|
"textFont",
|
|
"backupPath",
|
|
)
|
|
assert cmpFiles(testFile, compFile, ignStart=ignore)
|
|
conf.errorText() # This clears the error cache
|
|
|
|
# Block saving the file
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr("builtins.open", causeOSError)
|
|
assert conf.saveConfig() is False
|
|
assert conf.hasError is True
|
|
assert conf.errorText().startswith("Could not save config file")
|
|
|
|
# Block loading the file
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr("builtins.open", causeOSError)
|
|
assert conf.loadConfig() is False
|
|
assert conf.hasError is True
|
|
assert conf.errorText().startswith("Could not load config file")
|
|
|
|
# Change a few settings, save, reset, and reload
|
|
conf.lightTheme = "foo"
|
|
conf.darkTheme = "bar"
|
|
assert conf.saveConfig() is True
|
|
|
|
newConf = Config()
|
|
newConf.initConfig(confPath=fncPath, dataPath=fncPath)
|
|
newConf.loadConfig()
|
|
assert newConf.lightTheme == "foo"
|
|
assert newConf.darkTheme == "bar"
|
|
|
|
# If the data path doesn't materialise as a folder, sub-folders are skipped
|
|
with monkeypatch.context() as mp:
|
|
freshDataPath = fncPath / "freshdata"
|
|
origIsDir = Path.is_dir
|
|
mp.setattr(Path, "is_dir", lambda self: False if self == freshDataPath else origIsDir(self))
|
|
conf.initConfig(confPath=fncPath, dataPath=freshDataPath)
|
|
assert not (freshDataPath / "cache").exists()
|
|
|
|
# Test Correcting Quote Settings
|
|
conf.fmtDQuoteOpen = '"'
|
|
conf.fmtDQuoteClose = '"'
|
|
conf.fmtSQuoteOpen = "'"
|
|
conf.fmtSQuoteClose = "'"
|
|
conf.doReplaceDQuote = True
|
|
conf.doReplaceSQuote = True
|
|
assert conf.saveConfig() is True
|
|
|
|
assert newConf.loadConfig() is True
|
|
assert newConf.doReplaceDQuote is False
|
|
assert newConf.doReplaceSQuote is False
|
|
|
|
|
|
@pytest.mark.base
|
|
def testConfig_Localisation(fncPath: Path, tstPaths: TstPaths):
|
|
"""Test localisation."""
|
|
conf = Config()
|
|
conf.initConfig(confPath=fncPath, dataPath=fncPath)
|
|
|
|
# Localisation
|
|
# ============
|
|
|
|
i18nDir = fncPath / "i18n"
|
|
i18nDir.mkdir()
|
|
conf._ueLangPath = i18nDir
|
|
|
|
copyfile(tstPaths.filesDir / "ue_en_GB.qm", i18nDir / "ue_en_GB.qm")
|
|
writeFile(i18nDir / "ue_en_GB.ts", "")
|
|
writeFile(i18nDir / "ue_abcd.qm", "")
|
|
|
|
tstApp = MockApp()
|
|
conf.initLocalisation(tstApp) # type: ignore
|
|
assert conf.rtlMode is False
|
|
|
|
# Forcing RTL sets the app layout direction
|
|
conf.guiForceRTL = True
|
|
conf.initLocalisation(tstApp) # type: ignore
|
|
assert tstApp.layoutDirection() == Qt.LayoutDirection.RightToLeft
|
|
assert conf.rtlMode is True
|
|
|
|
# Check Lists
|
|
languages = conf.listLanguages(conf.LANG_NW)
|
|
assert languages == [("en_GB", "British English")]
|
|
languages = conf.listLanguages(conf.LANG_PROJ)
|
|
assert languages == [("en_GB", "British English")]
|
|
languages = conf.listLanguages(None) # type: ignore
|
|
assert languages == []
|
|
|
|
# Add Language
|
|
copyfile(tstPaths.filesDir / "ue_en_GB.qm", i18nDir / "ue_fr.qm")
|
|
writeFile(i18nDir / "ue_fr.ts", "")
|
|
|
|
languages = conf.listLanguages(conf.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 testConfig_Methods(fncPath: Path):
|
|
"""Check class methods."""
|
|
conf = Config()
|
|
conf.initConfig(confPath=fncPath, dataPath=fncPath)
|
|
|
|
# Home Path
|
|
assert conf.homePath() == Path.home().absolute()
|
|
|
|
# Data Path
|
|
assert conf.dataPath() == fncPath
|
|
assert conf.dataPath("stuff") == fncPath / "stuff"
|
|
|
|
# Assets Path
|
|
appPath = conf._appPath
|
|
assert conf.assetPath() == appPath / "assets"
|
|
assert conf.assetPath("stuff") == appPath / "assets" / "stuff"
|
|
|
|
# Last Path
|
|
assert conf.lastPath("project") == Path.home().absolute()
|
|
|
|
tmpStuff = fncPath / "stuff"
|
|
tmpStuff.mkdir()
|
|
conf.setLastPath("project", tmpStuff)
|
|
assert conf.lastPath("project") == tmpStuff
|
|
|
|
fileStuff = tmpStuff / "more_stuff.txt"
|
|
fileStuff.write_text("Stuff")
|
|
conf.setLastPath("project", fileStuff)
|
|
assert conf.lastPath("project") == tmpStuff
|
|
|
|
fileStuff.unlink()
|
|
tmpStuff.rmdir()
|
|
assert conf.lastPath("project") == Path.home().absolute()
|
|
|
|
# Invalid type does nothing
|
|
conf.setLastPath("project", None) # type: ignore
|
|
assert conf.lastPath("project") == Path.home().absolute()
|
|
|
|
# A path where neither it nor its parent exist does nothing
|
|
conf.setLastPath("project", fncPath / "missing" / "deeper" / "file.txt")
|
|
assert conf.lastPath("project") == Path.home().absolute()
|
|
|
|
# Backup Path
|
|
assert conf.backupPath() == conf._backPath
|
|
conf.setBackupPath(fncPath)
|
|
assert conf.backupPath() == fncPath
|
|
|
|
# Recent Projects
|
|
assert isinstance(conf.recentProjects, RecentProjects)
|
|
|
|
# Last Author
|
|
assert CONFIG.lastAuthor == ""
|
|
CONFIG.setLastAuthor(" Jane Doe ")
|
|
assert CONFIG.lastAuthor == "Jane Doe"
|
|
|
|
|
|
@pytest.mark.base
|
|
def testConfig_Fonts(monkeypatch: pytest.MonkeyPatch, fncPath: Path):
|
|
"""Check the OS-specific default font selection."""
|
|
conf = Config()
|
|
conf.initConfig(confPath=fncPath, dataPath=fncPath)
|
|
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(conf, "osWindows", True)
|
|
mp.setattr(QFontDatabase, "families", lambda *a: ["Segoe UI"])
|
|
conf.setGuiFont(None)
|
|
assert conf.guiFont.family() == "Segoe UI"
|
|
|
|
conf.setTextFont(None)
|
|
assert conf.textFont.family() == "Segoe UI"
|
|
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(conf, "osDarwin", True)
|
|
mp.setattr(QFontDatabase, "families", lambda *a: ["Helvetica"])
|
|
conf.setTextFont(None)
|
|
assert conf.textFont.family() == "Helvetica"
|
|
|
|
# Text font size is capped to the zoom range, without changing the font passed in
|
|
font = QFont(conf.textFont)
|
|
font.setPointSizeF(2.0)
|
|
conf.setTextFont(font)
|
|
assert conf.textFont.pointSizeF() == nwConst.TEXT_FONT_MIN_SIZE
|
|
assert font.pointSizeF() == 2.0
|
|
|
|
font.setPointSizeF(100.0)
|
|
conf.setTextFont(font.toString())
|
|
assert conf.textFont.pointSizeF() == nwConst.TEXT_FONT_MAX_SIZE
|
|
|
|
font.setPointSizeF(12.0)
|
|
conf.setTextFont(font)
|
|
assert conf.textFont.pointSizeF() == 12.0
|
|
|
|
# Pixel sized fonts are left as is
|
|
font.setPixelSize(4)
|
|
conf.setTextFont(font)
|
|
assert conf.textFont.pixelSize() == 4
|
|
|
|
|
|
@pytest.mark.base
|
|
def testConfig_SettersGetters(fncPath: Path):
|
|
"""Set various sizes and positions."""
|
|
conf = Config()
|
|
conf.initConfig(confPath=fncPath, dataPath=fncPath)
|
|
|
|
# Setter + Getter Combos
|
|
# ======================
|
|
|
|
# Window Size
|
|
conf.setMainWinSize(QRect(0, 0, 1205, 655))
|
|
assert conf.mainWinSize == [1200, 650]
|
|
|
|
conf.setMainWinSize(QRect(0, 0, 70, 70))
|
|
assert conf.mainWinSize == [70, 70]
|
|
|
|
conf.setMainWinSize(QRect(0, 0, 1200, 650))
|
|
|
|
# Welcome Window Size
|
|
conf.setWelcomeWinSize(QRect(0, 0, 70, 70))
|
|
assert conf.welcomeWinSize == [70, 70]
|
|
|
|
conf.setWelcomeWinSize(QRect(0, 0, 800, 500))
|
|
|
|
# Preferences Size
|
|
conf.setPreferencesWinSize(QRect(0, 0, 70, 70))
|
|
assert conf.prefsWinSize == [70, 70]
|
|
|
|
conf.setPreferencesWinSize(QRect(0, 0, 700, 615))
|
|
|
|
# Font Dialog Size
|
|
conf.setFontWinSize(QRect(0, 0, 70, 70))
|
|
assert conf.fontWinSize == [70, 70]
|
|
|
|
conf.setFontWinSize(QRect(0, 0, 700, 550))
|
|
|
|
# Getters Only
|
|
# ============
|
|
|
|
assert conf.getTextWidth(False) == 700
|
|
assert conf.getTextWidth(True) == 800
|
|
|
|
|
|
@pytest.mark.base
|
|
def testConfig_Internal(monkeypatch: pytest.MonkeyPatch, fncPath: Path):
|
|
"""Check internal functions."""
|
|
conf = Config()
|
|
conf.initConfig(confPath=fncPath, dataPath=fncPath)
|
|
|
|
# Function _packList
|
|
assert conf._packList(["A", 1, 2.0, None, False]) == "A, 1, 2.0, None, False"
|
|
|
|
# Function _checkOptionalPackages
|
|
# (Assumes enchant package exists and is importable)
|
|
conf._checkOptionalPackages()
|
|
assert conf.hasEnchant is True
|
|
|
|
with monkeypatch.context() as mp:
|
|
mp.setitem(sys.modules, "enchant", None)
|
|
conf._checkOptionalPackages()
|
|
assert conf.hasEnchant is False
|
|
|
|
|
|
@pytest.mark.base
|
|
def testConfig_RecentCache(monkeypatch: pytest.MonkeyPatch, tstPaths: TstPaths, fncPath: Path, nwGUI: GuiMain):
|
|
"""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),
|
|
]
|
|
|
|
# Entries missing a path or a title are skipped
|
|
cacheFile.write_text(
|
|
json.dumps({
|
|
"": {"uuid": "abc", "title": "No Path", "words": 1, "chars": 1, "time": 1},
|
|
str(pathTwo): {"uuid": "abc", "title": "", "words": 1, "chars": 1, "time": 1},
|
|
}),
|
|
encoding="utf-8",
|
|
)
|
|
assert recent.loadCache() is True
|
|
assert recent.listEntries() == []
|
|
|
|
# Entries without a UUID are not mapped
|
|
recent._setEntry("", str(pathOne), "No UUID", 10, 0, 0)
|
|
assert recent._map.get("") is None
|
|
recent.remove(pathOne)
|
|
|
|
# A moved project replaces the old entry
|
|
pathOld = fncPath / "projOld"
|
|
pathNew = fncPath / "projNew"
|
|
recent.update(pathOld, prjTwo.data, 1600002000)
|
|
assert recent.findCopy(pathNew, prjTwo.data.uuid) is None
|
|
recent.update(pathNew, prjTwo.data, 1600005600)
|
|
assert recent.listEntries() == [
|
|
(str(pathNew), "Proj Two", 200, 1600005600),
|
|
]
|
|
recent.remove(pathNew)
|
|
|
|
# A copied project keeps both entries
|
|
pathOld.mkdir()
|
|
(pathOld / nwFiles.PROJ_FILE).touch()
|
|
oldUuid = prjOne.data.uuid
|
|
recent.update(pathOld, prjOne.data, 1600002000)
|
|
assert recent.findCopy(pathOld, oldUuid) is None
|
|
assert recent.findCopy(pathNew, oldUuid) == str(pathOld)
|
|
recent.update(pathNew, prjOne.data, 1600005600)
|
|
assert recent.listEntries() == [
|
|
(str(pathOld), "Proj One", 100, 1600002000),
|
|
(str(pathNew), "Proj One", 100, 1600005600),
|
|
]
|
|
|
|
# Once the copy has a new UUID, the original is no longer a copy
|
|
prjOne.data.setUuid("")
|
|
recent.update(pathNew, prjOne.data, 1600005600)
|
|
assert recent.findCopy(pathOld, oldUuid) is None
|
|
assert len(recent.listEntries()) == 2
|
|
|
|
|
|
@pytest.mark.base
|
|
def testConfig_RecentPaths(monkeypatch: pytest.MonkeyPatch, tstPaths: 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("story", tstPaths.cnfDir / "story")
|
|
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("story") == str(tstPaths.cnfDir / "story")
|
|
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"),
|
|
"story": str(tstPaths.cnfDir / "story"),
|
|
"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
|
|
|
|
# A non-dict cache file is ignored
|
|
cacheFile.write_text(json.dumps(["not", "a", "dict"]), encoding="utf-8")
|
|
recent._data = {}
|
|
assert recent.loadCache() is True
|
|
assert recent._data == {}
|
|
|
|
# Invalid keys and non-string values are skipped
|
|
cacheFile.write_text(
|
|
json.dumps({
|
|
"default": str(tstPaths.cnfDir / "default"),
|
|
"foobar": str(tstPaths.cnfDir / "foobar"),
|
|
"project": 1234,
|
|
}),
|
|
encoding="utf-8",
|
|
)
|
|
recent._data = {}
|
|
assert recent.loadCache() is True
|
|
assert recent._data == {"default": str(tstPaths.cnfDir / "default")}
|
|
|
|
# Check error handling
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr("builtins.open", causeOSError)
|
|
assert recent.saveCache() is False
|
|
assert recent.loadCache() is False
|
|
|
|
|
|
@pytest.mark.base
|
|
def testConfig_IOError(monkeypatch: pytest.MonkeyPatch):
|
|
"""Test handling of I/O errors when using pathlib."""
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(Path, "iterdir", causeOSError)
|
|
config = Config() # Loading manuals will fail if not handled
|
|
assert config.listLanguages(config.LANG_NW) == [("en_GB", "British English")]
|
|
|
|
# No assets folder found
|
|
with monkeypatch.context() as mp:
|
|
origIsDir = Path.is_dir
|
|
mp.setattr(Path, "is_dir", lambda self: False if self.name == "assets" else origIsDir(self))
|
|
config = Config()
|
|
assert config._manuals == {}
|
|
|
|
with monkeypatch.context() as mp:
|
|
config = Config()
|
|
mp.setattr(Path, "is_dir", causeOSError)
|
|
config.initConfig()
|
|
|
|
|
|
@pytest.mark.base
|
|
def testConfig_ConvertOldConfig(fncPath: Path, tstPaths: TstPaths):
|
|
"""Test that an old ini-format config file is converted to the new
|
|
TOML format when loaded.
|
|
"""
|
|
oldFile = fncPath / nwFiles.CONF_FILE_OLD
|
|
newFile = fncPath / nwFiles.CONF_FILE
|
|
copyfile(tstPaths.filesDir / "old_umbraedit.conf", oldFile)
|
|
|
|
assert oldFile.is_file()
|
|
assert not newFile.exists()
|
|
|
|
conf = Config()
|
|
conf.initConfig(confPath=fncPath, dataPath=fncPath)
|
|
assert conf.loadConfig() is True
|
|
|
|
# The old file should have triggered a conversion to the new format
|
|
assert newFile.exists()
|
|
|
|
# A selection of values should have been picked up from the old file
|
|
assert conf.lightTheme == "default_light"
|
|
assert conf.darkTheme == "default_dark"
|
|
assert conf.themeMode == nwTheme.AUTO
|
|
assert conf.mainWinSize == [1200, 650]
|
|
assert conf.fontWinSize == [700, 550]
|
|
assert conf.autoSaveProj == 60
|
|
assert conf.autoSaveDoc == 30
|
|
assert conf.backupInterval == "session"
|
|
assert conf.textWidth == 700
|
|
assert conf.tabSize == 2.0
|
|
assert conf.dialogStyle == 2
|
|
assert conf.spellLanguage == "en"
|
|
assert conf.userIdleTime == 300
|
|
assert conf.showSessionTime is True
|
|
assert conf.searchRegEx is False
|
|
|
|
# Loading the newly converted file should produce the same values
|
|
newConf = Config()
|
|
newConf.initConfig(confPath=fncPath, dataPath=fncPath)
|
|
assert newConf.loadConfig() is True
|
|
assert newConf.mainWinSize == conf.mainWinSize
|
|
assert newConf.dialogStyle == conf.dialogStyle
|
|
assert newConf.spellLanguage == conf.spellLanguage
|
|
|
|
|
|
@pytest.mark.base
|
|
def testConfig_MovedConfigs(fncPath: Path):
|
|
"""Test that moved config options are saved to their new location
|
|
and read from their old location as a fallback.
|
|
"""
|
|
conf = Config()
|
|
conf.initConfig(confPath=fncPath, dataPath=fncPath)
|
|
assert conf.saveConfig() is True
|
|
saved = tomllib.loads((fncPath / nwFiles.CONF_FILE).read_text(encoding="utf-8"))
|
|
|
|
for (newSec, newOpt), (oldSec, oldOpt) in MOVED_CONFIGS.items():
|
|
assert newSec is not None
|
|
assert newOpt in saved[newSec]
|
|
assert oldOpt not in saved.get(oldSec, {})
|
|
|
|
data: T_TomlConfig = {oldSec: {oldOpt: "old"}}
|
|
|
|
toml = NTomlParser(confMap=MOVED_CONFIGS)
|
|
toml.readString(toml.writeString(data))
|
|
assert toml.getStr(newSec, newOpt, "") == "old"
|
|
|
|
ini = NConfigParser(confMap=MOVED_CONFIGS)
|
|
ini.read_dict(data)
|
|
assert ini.getStr(newSec, newOpt, "") == "old"
|
|
|
|
# The new location takes precedence
|
|
data.setdefault(newSec, {})[newOpt] = "new"
|
|
|
|
toml.readString(toml.writeString(data))
|
|
assert toml.getStr(newSec, newOpt, "") == "new"
|
|
|
|
ini.read_dict(data)
|
|
assert ini.getStr(newSec, newOpt, "") == "new"
|