Add novel model test coverage

This commit is contained in:
Veronica Berglyd Olsen
2025-03-24 22:17:34 +01:00
parent 34a3a6e85c
commit 1090a160de
4 changed files with 274 additions and 34 deletions
+4 -4
View File
@@ -818,13 +818,13 @@ class TagsIndex:
}
return
def tagName(self, tagKey: str) -> str:
def tagName(self, tagKey: str, default: str = "") -> str:
"""Get the name of a given tag."""
return self._tags.get(tagKey.lower(), {}).get("name", "")
return self._tags.get(tagKey.lower(), {}).get("name", default)
def tagDisplay(self, tagKey: str) -> str:
def tagDisplay(self, tagKey: str, default: str = "") -> str:
"""Get the display name of a given tag."""
return self._tags.get(tagKey.lower(), {}).get("display", "")
return self._tags.get(tagKey.lower(), {}).get("display", default)
def tagHandle(self, tagKey: str) -> str | None:
"""Get the handle of a given tag."""
+19 -30
View File
@@ -32,7 +32,6 @@ from novelwriter import SHARED
from novelwriter.constants import nwKeyWords, nwLabels, nwStyles, trConst
from novelwriter.core.indexdata import IndexHeading, IndexNode
from novelwriter.enum import nwNovelExtra
from novelwriter.error import logException
from novelwriter.types import QtAlignRight
logger = logging.getLogger(__name__)
@@ -169,38 +168,28 @@ class NovelModel(QAbstractTableModel):
first = current[0]
last = current[-1]
if len(current) != last - first + 1:
logger.warning("Novel model entries for '%s' are not continuous", handle)
return False
remains = []
try:
for key, head in node.items():
if key != "T0000":
if current:
j = current.pop(0)
self._rows[j] = self._generateEntry(handle, key, head)
else:
remains.append((key, head))
for key, head in node.items():
if key != "T0000":
if current:
j = current.pop(0)
self._rows[j] = self._generateEntry(handle, key, head)
else:
remains.append((key, head))
self.dataChanged.emit(self.createIndex(first, 0), self.createIndex(last, cols))
self.dataChanged.emit(self.createIndex(first, 0), self.createIndex(last, cols))
if remains:
self.beginInsertRows(QModelIndex(), last, last + len(remains) - 1)
for k, (key, head) in enumerate(remains, last + 1):
self._rows.insert(k, self._generateEntry(handle, key, head))
self.endInsertRows()
elif current:
self.beginRemoveRows(QModelIndex(), current[0], current[-1])
del self._rows[current[0]:current[-1] + 1]
self.endRemoveRows()
except Exception:
# This is faster than to check for index boundaries.
# We definitely don't want to cause a crash.
logger.error("Novel model refresh error for '%s'", handle)
logException()
return False
if remains:
# Inserting is safe for out of bounds indices
self.beginInsertRows(QModelIndex(), last, last + len(remains) - 1)
for k, (key, head) in enumerate(remains, last + 1):
self._rows.insert(k, self._generateEntry(handle, key, head))
self.endInsertRows()
elif current:
# Deleting ranges are safe for out of bounds indices
self.beginRemoveRows(QModelIndex(), current[0], current[-1])
del self._rows[current[0]:current[-1] + 1]
self.endRemoveRows()
return True
+73
View File
@@ -22,6 +22,7 @@ from __future__ import annotations
import pytest
from novelwriter import CONFIG
from novelwriter.core.index import TagsIndex
from novelwriter.core.indexdata import IndexHeading, IndexNode
from novelwriter.core.item import NWItem
@@ -179,6 +180,7 @@ def testCoreIndexData_IndexHeading():
assert head.charCount == 0
assert head.wordCount == 0
assert head.paraCount == 0
assert head.mainCount == 0
assert head.synopsis == ""
assert head.tag == ""
assert head.references == {}
@@ -205,6 +207,11 @@ def testCoreIndexData_IndexHeading():
assert head.wordCount == 4
assert head.paraCount == 2
# Check Main Count
assert head.mainCount == 4
CONFIG.useCharCount = True
assert head.mainCount == 42
# Set Summary
head.setSynopsis("In the beginning ...")
assert head.synopsis == "In the beginning ..."
@@ -235,6 +242,72 @@ def testCoreIndexData_IndexHeading():
assert head.synopsis == "How it started ..."
@pytest.mark.core
def testCoreIndexData_IndexHeadingReferences():
"""Test the IndexHeading references handling."""
tags = TagsIndex()
head = IndexHeading(tags, "T0001")
# Add some references
head.addReference("Jane", "@pov")
head.addReference("Jane", "@char")
head.addReference("John", "@char")
head.addReference("Main", "@plot")
head.addReference("Gun", "@object")
# With no tagsIndex name set, these should be empty
assert head.getReferences() == {
"@entity": [],
"@plot": [],
"@object": [],
"@story": [],
"@tag": [],
"@focus": [],
"@custom": [],
"@time": [],
"@pov": [],
"@mention": [],
"@char": [],
"@location": [],
}
# Set names
tags.add("Jane", "Jane", "0000000000000", "T00001", "CHARACTER")
tags.add("John", "John", "0000000000000", "T00001", "CHARACTER")
tags.add("Main", "Main", "0000000000000", "T00001", "PLOT")
tags.add("Gun", "Gun", "0000000000000", "T00001", "OBJECT")
# Now they should be populated
assert head.getReferences() == {
"@entity": [],
"@plot": ["Main"],
"@object": ["Gun"],
"@story": [],
"@tag": [],
"@focus": [],
"@custom": [],
"@time": [],
"@pov": ["Jane"],
"@mention": [],
"@char": ["Jane", "John"],
"@location": [],
}
# Check them individually
assert head.getReferencesByKeyword("@entity") == []
assert head.getReferencesByKeyword("@plot") == ["Main"]
assert head.getReferencesByKeyword("@object") == ["Gun"]
assert head.getReferencesByKeyword("@story") == []
assert head.getReferencesByKeyword("@tag") == []
assert head.getReferencesByKeyword("@focus") == []
assert head.getReferencesByKeyword("@custom") == []
assert head.getReferencesByKeyword("@time") == []
assert head.getReferencesByKeyword("@pov") == ["Jane"]
assert head.getReferencesByKeyword("@mention") == []
assert head.getReferencesByKeyword("@char") == ["Jane", "John"]
assert head.getReferencesByKeyword("@location") == []
@pytest.mark.core
def testCoreIndexData_IndexHeadingUnpackMeta():
"""Test IndexHeading class meta unpacking."""
+178
View File
@@ -0,0 +1,178 @@
"""
novelWriter Novel Model Tester
================================
This file is a part of novelWriter
Copyright (C) 2025 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 <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import pytest
from PyQt6.QtCore import QModelIndex, Qt
from novelwriter.core.indexdata import IndexHeading
from novelwriter.core.novelmodel import NovelModel
from novelwriter.core.project import NWProject
from novelwriter.enum import nwNovelExtra
from tests.tools import C, buildTestProject
@pytest.mark.core
def testCoreNovelModel_Interface(nwGUI, fncPath, mockRnd):
"""Test the novel model interface."""
project = NWProject()
mockRnd.reset()
buildTestProject(project, fncPath)
model = project.index.getNovelModel(C.hNovelRoot)
assert isinstance(model, NovelModel)
root = QModelIndex()
assert root.row() == -1
# Initial structure
assert model.rowCount(root) == 3
assert model.columnCount(root) == 3
assert model.data(model.createIndex(0, 0), Qt.ItemDataRole.DisplayRole) == "New Novel"
assert model.handle(model.createIndex(0, 0)) == C.hTitlePage
assert model.key(model.createIndex(0, 0)) == "T0001"
# Clear the model and check error handling
model.clear()
assert model.data(root, Qt.ItemDataRole.DisplayRole) is None
assert model.handle(root) is None
assert model.key(root) is None
@pytest.mark.core
def testCoreNovelModel_Extra(nwGUI, fncPath, mockRnd):
"""Test the novel model extra column."""
project = NWProject()
mockRnd.reset()
buildTestProject(project, fncPath)
root = QModelIndex()
model = project.index.getNovelModel(C.hNovelRoot)
assert isinstance(model, NovelModel)
assert model.rowCount(root) == 3
project.index._tagsIndex.add("Jane", "Jane", "0000000000000", "T0001", "CHARACTER")
project.index._tagsIndex.add("John", "John", "0000000000000", "T0001", "CHARACTER")
project.index._tagsIndex.add("Main", "Main", "0000000000000", "T0001", "PLOT")
project.index._tagsIndex.add("Side", "Side", "0000000000000", "T0001", "PLOT")
scene = project.index._itemIndex[C.hSceneDoc]
assert scene is not None
scene.addHeadingRef("T0001", ["Jane"], "@pov")
scene.addHeadingRef("T0001", ["John"], "@focus")
scene.addHeadingRef("T0001", ["Main"], "@plot")
scene.addHeadingRef("T0001", ["Side"], "@plot")
# No extra by default
assert model.columns == 3
assert model._extraKey == ""
assert model._extraLabel == ""
# Point of view
model.setExtraColumn(nwNovelExtra.POV)
model.refresh(scene)
assert model.columns == 4
assert model._extraKey == "@pov"
assert model._extraLabel == "Point of View"
assert model.data(model.createIndex(2, 2), Qt.ItemDataRole.DisplayRole) == "Jane"
model.setExtraColumn(nwNovelExtra.HIDDEN)
assert model.columns == 3
# Focus
model.setExtraColumn(nwNovelExtra.FOCUS)
model.refresh(scene)
assert model.columns == 4
assert model._extraKey == "@focus"
assert model._extraLabel == "Focus"
assert model.data(model.createIndex(2, 2), Qt.ItemDataRole.DisplayRole) == "John"
model.setExtraColumn(nwNovelExtra.HIDDEN)
assert model.columns == 3
# Plot
model.setExtraColumn(nwNovelExtra.PLOT)
model.refresh(scene)
assert model.columns == 4
assert model._extraKey == "@plot"
assert model._extraLabel == "Plot"
assert model.data(model.createIndex(2, 2), Qt.ItemDataRole.DisplayRole) == "Main, Side"
model.setExtraColumn(nwNovelExtra.HIDDEN)
assert model.columns == 3
@pytest.mark.core
def testCoreNovelModel_Data(nwGUI, fncPath, mockRnd):
"""Test the novel model data methods."""
project = NWProject()
mockRnd.reset()
buildTestProject(project, fncPath)
root = QModelIndex()
model = project.index.getNovelModel(C.hNovelRoot)
assert isinstance(model, NovelModel)
assert model.rowCount(root) == 3
title = project.index._itemIndex[C.hTitlePage]
chapter = project.index._itemIndex[C.hChapterDoc]
scene = project.index._itemIndex[C.hSceneDoc]
assert title is not None
assert chapter is not None
assert scene is not None
# Clear the model and try to refresh
model.clear()
assert model.rowCount(root) == 0
# Cannot refresh an empty model
assert model.refresh(title) is False
# Add all back
model.append(title)
model.append(chapter)
model.append(scene)
# Add headings to scene
scene.addHeading(IndexHeading(scene._tags, "T0002", 10, "H4", "A Section"))
scene.addHeading(IndexHeading(scene._tags, "T0003", 10, "H4", "Another Section"))
assert model.refresh(scene) is True
assert [
model.data(model.createIndex(i, 0), Qt.ItemDataRole.DisplayRole)
for i in range(model.rowCount(root))
] == [
"New Novel", "New Chapter", "New Scene", "A Section", "Another Section",
]
# Remove new headings
del scene._headings["T0002"]
del scene._headings["T0003"]
assert model.refresh(scene) is True
assert [
model.data(model.createIndex(i, 0), Qt.ItemDataRole.DisplayRole)
for i in range(model.rowCount(root))
] == [
"New Novel", "New Chapter", "New Scene",
]