Add test coverage of build settings class

This commit is contained in:
Veronica Berglyd Olsen
2023-06-17 17:30:37 +02:00
parent a3f0d5b865
commit 73451faa31
3 changed files with 376 additions and 17 deletions
+9 -13
View File
@@ -39,7 +39,6 @@ from novelwriter.enum import nwBuildFmt
from novelwriter.error import logException
from novelwriter.common import checkUuid, isHandle, jsonEncode
from novelwriter.constants import nwFiles, nwHeadFmt
from novelwriter.core.item import NWItem
from novelwriter.core.project import NWProject
logger = logging.getLogger(__name__)
@@ -208,15 +207,15 @@ class BuildSettings:
def getInt(self, key: str) -> int:
"""Type safe value access for integers."""
value = self._settings.get(key, SETTINGS_TEMPLATE.get(key, (None, None)[1]))
if isinstance(value, int):
return value
if isinstance(value, (int, float)):
return int(value)
return 0
def getFloat(self, key: str) -> float:
"""Type safe value access for floats."""
value = self._settings.get(key, SETTINGS_TEMPLATE.get(key, (None, None)[1]))
if isinstance(value, float):
return value
if isinstance(value, (int, float)):
return float(value)
return 0.0
##
@@ -282,8 +281,8 @@ class BuildSettings:
self._changed = True
return
def setSkipRoot(self, tHandle: str, state: bool):
"""Set a specific root folder as skipped or not."""
def setAllowRoot(self, tHandle: str, state: bool):
"""Set a specific root folder as allowed or not."""
if state is True:
self._skipRoot.discard(tHandle)
self._changed = True
@@ -299,7 +298,7 @@ class BuildSettings:
definition = SETTINGS_TEMPLATE[key]
if not isinstance(value, definition[0]):
return False
if len(definition) == 4:
if len(definition) == 4 and isinstance(value, (int, float)):
value = min(max(value, definition[2]), definition[3])
self._changed = value != self._settings[key]
self._settings[key] = value
@@ -330,10 +329,7 @@ class BuildSettings:
for item in project.tree:
tHandle = item.itemHandle
if not tHandle:
continue
if not isinstance(item, NWItem):
result[tHandle] = (False, FilterMode.UNKNOWN)
if tHandle is None:
continue
if item.isInactiveClass() or (item.itemRoot in self._skipRoot):
result[tHandle] = (False, FilterMode.SKIPPED)
@@ -401,7 +397,7 @@ class BuildSettings:
self.setLastPath(data.get("path", None))
self.setLastBuildName(data.get("build", ""))
buildFmt = str(data.get("build", ""))
buildFmt = str(data.get("format", ""))
if buildFmt in nwBuildFmt.__members__:
self.setLastFormat(nwBuildFmt[buildFmt])
+4 -4
View File
@@ -423,7 +423,7 @@ class _FilterTab(QWidget):
self._build.setValue(key[4:], state)
self._setTreeItemMode()
elif key.startswith("root:"):
self._build.setSkipRoot(key[5:], state)
self._build.setAllowRoot(key[5:], state)
self._populateTree()
return
@@ -512,14 +512,14 @@ class _FilterTab(QWidget):
# Root Classes
self.filterOpt.addLabel(self.tr("Root Folders"))
for tHandle, nwItem in self.theProject.tree.iterRoots(None):
for nwItem in self.theProject.getProjectItems():
if not nwItem.isInactiveClass():
itemIcon = self.mainTheme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout
)
self.filterOpt.addItem(
itemIcon, nwItem.itemName, f"root:{tHandle}",
default=self._build.isRootAllowed(tHandle)
itemIcon, nwItem.itemName, f"root:{nwItem.itemHandle}",
default=self._build.isRootAllowed(nwItem.itemHandle)
)
return
+363
View File
@@ -0,0 +1,363 @@
"""
novelWriter Manuscript Build Settings Tester
==============================================
This file is a part of novelWriter
Copyright 20182023, 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 <https://www.gnu.org/licenses/>.
"""
import uuid
import pytest
from pathlib import Path
from novelwriter.core.item import NWItem
from tools import C, buildTestProject
from novelwriter.enum import nwBuildFmt, nwItemClass
from novelwriter.core.project import NWProject
from novelwriter.core.buildsettings import BuildSettings, FilterMode
def isUUID(value):
"""Checks if a value is a valid UUID object."""
try:
uuid.UUID(value)
return True
except Exception:
return False
@pytest.mark.core
def testCoreBuildSettings_ClassAttributes(fncPath: Path):
"""Test the BuildSettings class attributes."""
build = BuildSettings()
assert build.changed is False
# Name is always converted to string
build.setName(None) # type: ignore
assert build.name == "None"
build.setName("Test Build")
assert build.name == "Test Build"
# Only valid UUIDs are accpeted, anything else generates a new UUID
build.setBuildID("5cf45d24-f496-42c9-8733-529a9e52a62b")
assert build.buildID == "5cf45d24-f496-42c9-8733-529a9e52a62b"
build.setBuildID(None) # type: ignore
assert build.buildID != "5cf45d24-f496-42c9-8733-529a9e52a62b"
assert isUUID(build.buildID)
build.setBuildID("qcf45d24-f496-42c9-8733-529a9e52a62b")
assert build.buildID != "qcf45d24-f496-42c9-8733-529a9e52a62b"
assert isUUID(build.buildID)
# Last path must be valid, if not it defaults to $HOME
build.setLastPath("/path/to/nowhere")
assert build.lastPath == Path.home()
build.setLastPath(None)
assert build.lastPath == Path.home()
(fncPath / "test.txt").write_text("foobar")
build.setLastPath(fncPath / "test.txt") # Can't be a file
assert build.lastPath == Path.home()
build.setLastPath(fncPath)
assert build.lastPath == fncPath
build.setLastPath(str(fncPath)) # String paths are also ok
assert build.lastPath == fncPath
# Last path no longer exists -> fallback to $HOME
testDir = fncPath / "test_dir"
testDir.mkdir()
build.setLastPath(testDir)
assert build.lastPath == testDir
testDir.rmdir()
assert build.lastPath == Path.home()
# Last build name
build.setLastBuildName(None) # type: ignore
assert build.lastBuildName == "None"
build.setLastBuildName("\tBuild Name ")
assert build.lastBuildName == "Build Name"
# Last build format
build.setLastFormat(None) # type: ignore
assert build.lastFormat == nwBuildFmt.ODT # Default value
build.setLastFormat(nwBuildFmt.FODT)
assert build.lastFormat == nwBuildFmt.FODT
# Changes are recorded
assert build.changed is True
build.resetChangedState()
assert build.changed is False
# Set some sensible values
build.setName("Test Build")
build.setBuildID("5cf45d24-f496-42c9-8733-529a9e52a62b")
build.setLastPath(fncPath)
build.setLastBuildName("Build Name")
build.setLastFormat(nwBuildFmt.HTML)
# Pack the values
data = build.pack()
assert data["name"] == "Test Build"
assert data["uuid"] == "5cf45d24-f496-42c9-8733-529a9e52a62b"
assert data["path"] == str(fncPath)
assert data["build"] == "Build Name"
assert data["format"] == nwBuildFmt.HTML.name
# Unpack into new object
another = BuildSettings()
another.unpack(data)
more = another.pack()
assert more["name"] == "Test Build"
assert more["uuid"] == "5cf45d24-f496-42c9-8733-529a9e52a62b"
assert more["path"] == str(fncPath)
assert more["build"] == "Build Name"
assert more["format"] == nwBuildFmt.HTML.name
# END Test testCoreBuildSettings_ClassAttributes
@pytest.mark.core
def testCoreBuildSettings_BuildValues():
"""Test BuildSettings get/set of build values."""
build = BuildSettings()
strSetting = "headings.fmtTitle"
intSetting = "format.textSize"
boolSetting = "filter.includeNovel"
floatSetting = "format.lineHeight"
# Invalid setting
assert build.setValue("foo", "bar") is False
# Value must be correct type
assert build.setValue(strSetting, 15) is False
assert build.setValue(intSetting, 15.0) is False
assert build.setValue(boolSetting, "string") is False
assert build.setValue(floatSetting, 15) is False
# Check min/max range
assert build.setValue(floatSetting, 200.0) is True
assert build.getFloat(floatSetting) == 3.0
assert build.setValue(floatSetting, 0.0) is True
assert build.getFloat(floatSetting) == 0.75
# Check string values
assert build.setValue(strSetting, "foobar") is True
assert build.getStr(strSetting) == "foobar"
assert build.getInt(strSetting) == 0
assert build.getBool(strSetting) is True
assert build.getFloat(strSetting) == 0.0
# Check int values
assert build.setValue(intSetting, 42) is True
assert build.getStr(intSetting) == "42"
assert build.getInt(intSetting) == 42
assert build.getBool(intSetting) is True
assert build.getFloat(intSetting) == 42.0
# Check bool values
assert build.setValue(boolSetting, True) is True
assert build.getStr(boolSetting) == "True"
assert build.getInt(boolSetting) == 1
assert build.getBool(boolSetting) is True
assert build.getFloat(boolSetting) == 1.0
# Check float values
assert build.setValue(floatSetting, 2.5) is True
assert build.getStr(floatSetting) == "2.5"
assert build.getInt(floatSetting) == 2
assert build.getBool(floatSetting) is True
assert build.getFloat(floatSetting) == 2.5
# Check labels
assert build.getLabel(strSetting) == "Title Headings"
assert BuildSettings.getLabel(strSetting) == "Title Headings"
# Pack the values
data = build.pack()
assert data["settings"][strSetting] == "foobar"
assert data["settings"][intSetting] == 42
assert data["settings"][boolSetting] is True
assert data["settings"][floatSetting] == 2.5
# Unpack into new object
another = BuildSettings()
another.unpack(data)
more = another.pack()
assert more["settings"][strSetting] == "foobar"
assert more["settings"][intSetting] == 42
assert more["settings"][boolSetting] is True
assert more["settings"][floatSetting] == 2.5
# END Test testCoreBuildSettings_BuildValues
@pytest.mark.core
def testCoreBuildSettings_Filters(mockGUI, fncPath: Path, mockRnd):
"""Test filters for project items."""
project = NWProject(mockGUI)
buildTestProject(project, fncPath)
build = BuildSettings()
# Add some more items
hArchRoot = project.newRoot(nwItemClass.ARCHIVE, "Archive")
hCharDoc = project.newFile("Jane Doe", C.hCharRoot)
initLen = len(project.tree)
# With no changes
assert build.isRootAllowed(C.hNovelRoot) is True
assert build.isRootAllowed(C.hPlotRoot) is True
assert build.isRootAllowed(C.hCharRoot) is True
assert build.isRootAllowed(C.hWorldRoot) is True
assert build.buildItemFilter(project, withRoots=False) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hPlotRoot: (False, FilterMode.SKIPPED),
C.hCharRoot: (False, FilterMode.SKIPPED),
C.hWorldRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.FILTERED),
C.hSceneDoc: (True, FilterMode.FILTERED),
hArchRoot: (False, FilterMode.SKIPPED),
hCharDoc: (False, FilterMode.FILTERED),
}
# Enable notes and roots
build.setValue("filter.includeNotes", True)
assert build.buildItemFilter(project, withRoots=True) == {
C.hNovelRoot: (True, FilterMode.ROOT),
C.hPlotRoot: (True, FilterMode.ROOT),
C.hCharRoot: (True, FilterMode.ROOT),
C.hWorldRoot: (True, FilterMode.ROOT),
C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.FILTERED),
C.hSceneDoc: (True, FilterMode.FILTERED),
hArchRoot: (False, FilterMode.SKIPPED),
hCharDoc: (True, FilterMode.FILTERED),
}
# Skip plot and char roots
build.setValue("filter.includeNotes", True)
build.setAllowRoot(C.hPlotRoot, False)
build.setAllowRoot(C.hCharRoot, False)
assert build.isRootAllowed(C.hNovelRoot) is True
assert build.isRootAllowed(C.hPlotRoot) is False
assert build.isRootAllowed(C.hCharRoot) is False
assert build.isRootAllowed(C.hWorldRoot) is True
assert build.buildItemFilter(project, withRoots=True) == {
C.hNovelRoot: (True, FilterMode.ROOT),
C.hPlotRoot: (False, FilterMode.SKIPPED),
C.hCharRoot: (False, FilterMode.SKIPPED),
C.hWorldRoot: (True, FilterMode.ROOT),
C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.FILTERED),
C.hSceneDoc: (True, FilterMode.FILTERED),
hArchRoot: (False, FilterMode.SKIPPED),
hCharDoc: (False, FilterMode.SKIPPED), # Now also skipped since in char
}
# Enable char root again, but exclude char doc and scene doc
build.setValue("filter.includeNotes", True)
build.setAllowRoot(C.hPlotRoot, False)
build.setAllowRoot(C.hCharRoot, True)
build.setExcluded(C.hSceneDoc)
build.setExcluded(hCharDoc) # type: ignore
assert build.isRootAllowed(C.hNovelRoot) is True
assert build.isRootAllowed(C.hPlotRoot) is False
assert build.isRootAllowed(C.hCharRoot) is True
assert build.isRootAllowed(C.hWorldRoot) is True
assert build.buildItemFilter(project, withRoots=True) == {
C.hNovelRoot: (True, FilterMode.ROOT),
C.hPlotRoot: (False, FilterMode.SKIPPED),
C.hCharRoot: (True, FilterMode.ROOT),
C.hWorldRoot: (True, FilterMode.ROOT),
C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.FILTERED),
C.hSceneDoc: (False, FilterMode.EXCLUDED),
hArchRoot: (False, FilterMode.SKIPPED),
hCharDoc: (False, FilterMode.EXCLUDED),
}
# Disable notes, but include char doc
build.setValue("filter.includeNotes", False)
build.setAllowRoot(C.hPlotRoot, False)
build.setAllowRoot(C.hCharRoot, True)
build.setExcluded(C.hSceneDoc)
build.setIncluded(hCharDoc) # type: ignore
assert build.isRootAllowed(C.hNovelRoot) is True
assert build.isRootAllowed(C.hPlotRoot) is False
assert build.isRootAllowed(C.hCharRoot) is True
assert build.isRootAllowed(C.hWorldRoot) is True
assert build.buildItemFilter(project, withRoots=True) == {
C.hNovelRoot: (True, FilterMode.ROOT),
C.hPlotRoot: (False, FilterMode.SKIPPED),
C.hCharRoot: (True, FilterMode.ROOT),
C.hWorldRoot: (True, FilterMode.ROOT),
C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.FILTERED),
C.hSceneDoc: (False, FilterMode.EXCLUDED),
hArchRoot: (False, FilterMode.SKIPPED),
hCharDoc: (True, FilterMode.INCLUDED),
}
# Set everything back to filered
build.setValue("filter.includeNotes", False)
build.setAllowRoot(C.hPlotRoot, False)
build.setAllowRoot(C.hCharRoot, True)
build.setFiltered(C.hSceneDoc)
build.setFiltered(hCharDoc) # type: ignore
assert build.isRootAllowed(C.hNovelRoot) is True
assert build.isRootAllowed(C.hPlotRoot) is False
assert build.isRootAllowed(C.hCharRoot) is True
assert build.isRootAllowed(C.hWorldRoot) is True
assert build.buildItemFilter(project, withRoots=True) == {
C.hNovelRoot: (True, FilterMode.ROOT),
C.hPlotRoot: (False, FilterMode.SKIPPED),
C.hCharRoot: (True, FilterMode.ROOT),
C.hWorldRoot: (True, FilterMode.ROOT),
C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.FILTERED),
C.hSceneDoc: (True, FilterMode.FILTERED),
hArchRoot: (False, FilterMode.SKIPPED),
hCharDoc: (False, FilterMode.FILTERED),
}
# Check error handling
project.tree._treeOrder.append("00000000000ff")
project.tree._projTree["00000000000ff"] = NWItem(project)
assert project.tree["00000000000ff"].itemHandle is None # type: ignore
filtered = build.buildItemFilter(project, withRoots=False)
assert len(filtered) == initLen
# No valid project provided
assert build.buildItemFilter(None) == {} # type: ignore
# END Test testCoreBuildSettings_Filters