Add full test coverage of index class

This commit is contained in:
Veronica Berglyd Olsen
2022-06-05 13:17:24 +02:00
parent 22099c1b56
commit 63068f1018
3 changed files with 305 additions and 42 deletions
+5 -5
View File
@@ -34,7 +34,7 @@ def trConst(tString):
return QCoreApplication.translate("Constant", tString)
class nwConst():
class nwConst:
# Date and Time Formats
FMT_TSTAMP = "%Y-%m-%d %H:%M:%S" # Default format
@@ -48,7 +48,7 @@ class nwConst():
# END Class nwConst
class nwRegEx():
class nwRegEx:
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
@@ -57,7 +57,7 @@ class nwRegEx():
# END Class nwRegEx
class nwFiles():
class nwFiles:
PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt"
@@ -107,7 +107,7 @@ class nwKeyWords:
# END Class nwKeyWords
class nwLabels():
class nwLabels:
CLASS_NAME = {
nwItemClass.NO_CLASS: QT_TRANSLATE_NOOP("Constant", "None"),
@@ -185,7 +185,7 @@ class nwLabels():
# END Class nwLabels
class nwQuotes():
class nwQuotes:
"""Allowed quotation marks.
Source: https://en.wikipedia.org/wiki/Quotation_mark
"""
+29 -15
View File
@@ -67,12 +67,10 @@ class NWIndex:
self.theProject = theProject
# Internal
self._indexBroken = False
# Indices
# Storage and State
self._tagsIndex = TagsIndex()
self._itemIndex = ItemIndex(theProject)
self._indexBroken = False
# TimeStamps
self._timeNovel = 0
@@ -81,6 +79,9 @@ class NWIndex:
return
def __repr__(self):
return f"<NWIndex project='{self.theProject.projName}'>"
##
# Properties
##
@@ -124,8 +125,7 @@ class NWIndex:
logger.debug("Re-indexing item '%s'", tHandle)
theDoc = NWDoc(self.theProject, tHandle)
theText = theDoc.readDocument()
self.scanText(tHandle, theText if theText is not None else "")
self.scanText(tHandle, theDoc.readDocument() or "")
return True
@@ -316,7 +316,7 @@ class NWIndex:
return True
##
# Internal Indexers
# Internal Indexer Helpers
##
def _indexTitle(self, tHandle, aLine, nTitle):
@@ -613,11 +613,13 @@ class NWIndex:
# =============================================================================================== #
# Indexer Objects
# The Tags Index Object
# =============================================================================================== #
class TagsIndex:
"""A wrapper class that holds the reverse lookup tags index.
"""A wrapper class that holds the reverse lookup tags index. This is
just a simple wrapper around a single dictionary to keep tighter
control of the keys.
"""
def __init__(self):
@@ -719,8 +721,16 @@ class TagsIndex:
# END Class TagsIndex
# =============================================================================================== #
# The Item Index Objects
# =============================================================================================== #
class ItemIndex:
"""A wrapper object holding the indexed items.
"""A wrapper object holding the indexed items. This is a warapper
class around a single storage dictionary with a set of utility
functions for setting and accessing the index data. Each indexed
item is stored in an IndexItem object, which again holds an
IndexHeading object for each header of the text.
"""
def __init__(self, theProject):
@@ -896,7 +906,8 @@ class IndexItem:
"""This object represents the index data of a project item (NWItem).
It holds a record of all the headings in the text, and the meta data
associated with each heading. It also holds a pointer to the project
item.
item. The main heading level of the item is also held here since it
must be reset each time the item is re-indexed.
"""
def __init__(self, tHandle, tItem):
@@ -912,7 +923,7 @@ class IndexItem:
return
def __repr__(self):
return f"<IndexItem handle={self._handle}>"
return f"<IndexItem handle='{self._handle}'>"
##
# Properties
@@ -1062,7 +1073,7 @@ class IndexHeading:
return
def __repr__(self):
return f"<IndexHeading key={self._key}>"
return f"<IndexHeading key='{self._key}'>"
##
# Properties
@@ -1165,8 +1176,11 @@ class IndexHeading:
def packReferences(self):
"""Pack references into a dictionary for saving to cache.
Multiple types are packed into a sorted, comma separated string.
It is sorted to prevent creating unnecessary diffs as the order
of a set is not guaranteed.
"""
return {key: ",".join(value) for key, value in self._refs.items()}
return {key: ",".join(sorted(list(value))) for key, value in self._refs.items()}
def unpackData(self, data):
"""Unpack a heading entry from a dictionary.
@@ -1189,7 +1203,7 @@ class IndexHeading:
if not isinstance(tagKey, str):
raise ValueError("itemIndex reference key must be a string")
if not isinstance(refTypes, str):
raise ValueError("itemIndex reference types must be a string")
raise ValueError("itemIndex reference type must be a string")
for refType in refTypes.split(","):
if refType in nwKeyWords.VALID_KEYS:
self.addReference(tagKey, refType)
+271 -22
View File
@@ -29,7 +29,7 @@ from mock import causeException
from tools import buildTestProject, cmpFiles, writeFile
from novelwriter.core.project import NWProject
from novelwriter.core.index import NWIndex, countWords
from novelwriter.core.index import NWIndex, countWords, TagsIndex
from novelwriter.enum import nwItemClass, nwItemLayout
@@ -46,6 +46,8 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir):
assert theProject.openProject(nwLipsum)
theIndex = NWIndex(theProject)
assert repr(theIndex) == "<NWIndex project='Lorem Ipsum'>"
notIndexable = {
"b3643d0f92e32": False, # Novel ROOT
"45e6b01ca35c1": False, # Chapter One FOLDER
@@ -761,6 +763,166 @@ def testCoreIndex_ExtractData(mockGUI, fncDir, mockRnd):
# END Test testCoreIndex_ExtractData
@pytest.mark.core
def testCoreIndex_TagsIndex():
"""Check the TagsIndex class.
"""
tagsIndex = TagsIndex()
assert tagsIndex._tags == {}
# Expected data
content = {
"Tag1": {
"handle": "0000000000001",
"heading": "T000001",
"class": nwItemClass.NOVEL.name,
},
"Tag2": {
"handle": "0000000000002",
"heading": "T000002",
"class": nwItemClass.CHARACTER.name,
},
"Tag3": {
"handle": "0000000000003",
"heading": "T000003",
"class": nwItemClass.PLOT.name,
},
}
# Add data
tagsIndex.add("Tag1", "0000000000001", "T000001", nwItemClass.NOVEL)
tagsIndex.add("Tag2", "0000000000002", "T000002", nwItemClass.CHARACTER)
tagsIndex.add("Tag3", "0000000000003", "T000003", nwItemClass.PLOT)
assert tagsIndex._tags == content
# Get items
assert tagsIndex["Tag1"] == content["Tag1"]
assert tagsIndex["Tag2"] == content["Tag2"]
assert tagsIndex["Tag3"] == content["Tag3"]
assert tagsIndex["Tag4"] is None
# Contains
assert "Tag1" in tagsIndex
assert "Tag2" in tagsIndex
assert "Tag3" in tagsIndex
assert "Tag4" not in tagsIndex
# Read back handles
assert tagsIndex.tagHandle("Tag1") == "0000000000001"
assert tagsIndex.tagHandle("Tag2") == "0000000000002"
assert tagsIndex.tagHandle("Tag3") == "0000000000003"
assert tagsIndex.tagHandle("Tag4") is None
# Read back headings
assert tagsIndex.tagHeading("Tag1") == "T000001"
assert tagsIndex.tagHeading("Tag2") == "T000002"
assert tagsIndex.tagHeading("Tag3") == "T000003"
assert tagsIndex.tagHeading("Tag4") == "T000000"
# Read back classes
assert tagsIndex.tagClass("Tag1") == nwItemClass.NOVEL.name
assert tagsIndex.tagClass("Tag2") == nwItemClass.CHARACTER.name
assert tagsIndex.tagClass("Tag3") == nwItemClass.PLOT.name
assert tagsIndex.tagClass("Tag4") is None
# Pack Data
assert tagsIndex.packData() == content
# Delete the second key and a nomn-existant key
del tagsIndex["Tag2"]
del tagsIndex["Tag4"]
assert "Tag1" in tagsIndex
assert "Tag2" not in tagsIndex
assert "Tag3" in tagsIndex
assert "Tag4" not in tagsIndex
# Clear and reload
tagsIndex.clear()
assert tagsIndex._tags == {}
assert tagsIndex.packData() == {}
tagsIndex.unpackData(content)
assert tagsIndex._tags == content
assert tagsIndex.packData() == content
# Unpack Errors
# =============
tagsIndex.clear()
# Invalid data type
with pytest.raises(ValueError):
tagsIndex.unpackData([])
# Invalid key
with pytest.raises(ValueError):
tagsIndex.unpackData({
1234: {
"handle": "0000000000001",
"heading": "T000001",
"class": "NOVEL",
}
})
# Missing handle
with pytest.raises(KeyError):
tagsIndex.unpackData({
"Tag1": {
"heading": "T000001",
"class": "NOVEL",
}
})
# Missing heading
with pytest.raises(KeyError):
tagsIndex.unpackData({
"Tag1": {
"handle": "0000000000001",
"class": "NOVEL",
}
})
# Missing class
with pytest.raises(KeyError):
tagsIndex.unpackData({
"Tag1": {
"handle": "0000000000001",
"heading": "T000001",
}
})
# Invalid handle
with pytest.raises(ValueError):
tagsIndex.unpackData({
"Tag1": {
"handle": "blablabla",
"heading": "T000001",
"class": "NOVEL",
}
})
# Invalid heading
with pytest.raises(ValueError):
tagsIndex.unpackData({
"Tag1": {
"handle": "0000000000001",
"heading": "blabla",
"class": "NOVEL",
}
})
# Invalid class
with pytest.raises(ValueError):
tagsIndex.unpackData({
"Tag1": {
"handle": "0000000000001",
"heading": "T000001",
"class": "blabla",
}
})
# END Test testCoreIndex_TagsIndex
@pytest.mark.core
def testCoreIndex_ItemIndex(mockGUI, fncDir, mockRnd):
"""Check the ItemIndex class.
@@ -780,26 +942,6 @@ def testCoreIndex_ItemIndex(mockGUI, fncDir, mockRnd):
assert cHandle not in itemIndex
assert sHandle not in itemIndex
# Unpack Data
# ===========
# Data must be dictionary
with pytest.raises(ValueError):
itemIndex.unpackData("stuff")
# Keys must be valid handles
with pytest.raises(ValueError):
itemIndex.unpackData({"stuff": "more stuff"})
# Unknown keys should be skipped
itemIndex.unpackData({"0000000000000": {}})
assert itemIndex._items == {}
# Known keys can be added, even witout data
itemIndex.unpackData({nHandle: {}})
assert nHandle in itemIndex
itemIndex.clear()
# Add Items
# =========
assert cHandle not in itemIndex
@@ -820,7 +962,7 @@ def testCoreIndex_ItemIndex(mockGUI, fncDir, mockRnd):
# Set the remainig data values
itemIndex.setHeadingCounts(cHandle, "T000001", 60, 10, 2)
itemIndex.setHeadingSynopsis(cHandle, "T000001", "In the beginning ...")
itemIndex.setHeadingTag(cHandle, "T000001", "One") # Although it isn't allowed to have a tag
itemIndex.setHeadingTag(cHandle, "T000001", "One")
itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane"], "@pov")
itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane"], "@focus")
itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane", "John"], "@char")
@@ -842,6 +984,37 @@ def testCoreIndex_ItemIndex(mockGUI, fncDir, mockRnd):
itemIndex.addItemHeading(nHandle, "T000001", "H1", "Novel")
itemIndex.addItemHeading(sHandle, "T000001", "H3", "Scene One")
# Check Item and Heading Direct Access
# ====================================
# Check repr strings
assert repr(itemIndex[nHandle]) == f"<IndexItem handle='{nHandle}'>"
assert repr(itemIndex[nHandle]["T000001"]) == "<IndexHeading key='T000001'>"
# Check content of a single item
assert "T000001" in itemIndex[nHandle]
assert itemIndex[cHandle].allTags() == ["One"]
# Check the content of a single heading
assert itemIndex[cHandle]["T000001"].key == "T000001"
assert itemIndex[cHandle]["T000001"].level == "H2"
assert itemIndex[cHandle]["T000001"].title == "Chapter One"
assert itemIndex[cHandle]["T000001"].tag == "One"
assert itemIndex[cHandle]["T000001"].charCount == 60
assert itemIndex[cHandle]["T000001"].wordCount == 10
assert itemIndex[cHandle]["T000001"].paraCount == 2
assert itemIndex[cHandle]["T000001"].synopsis == "In the beginning ..."
assert "Jane" in itemIndex[cHandle]["T000001"].references
assert "John" in itemIndex[cHandle]["T000001"].references
# Check heading level setter
itemIndex[cHandle]["T000001"].setLevel("H3") # Change it
assert itemIndex[cHandle]["T000001"].level == "H3"
itemIndex[cHandle]["T000001"].setLevel("H2") # Set it back
assert itemIndex[cHandle]["T000001"].level == "H2"
itemIndex[cHandle]["T000001"].setLevel("H5") # Invalid level
assert itemIndex[cHandle]["T000001"].level == "H2"
# Data Extraction
# ===============
@@ -908,6 +1081,82 @@ def testCoreIndex_ItemIndex(mockGUI, fncDir, mockRnd):
del itemIndex[uHandle]
assert uHandle not in itemIndex
# Unpack Error Handling
# =====================
# Pack/unpack should restore state
content = itemIndex.packData()
itemIndex.clear()
itemIndex.unpackData(content)
assert itemIndex.packData() == content
itemIndex.clear()
# Data must be dictionary
with pytest.raises(ValueError):
itemIndex.unpackData("stuff")
# Keys must be valid handles
with pytest.raises(ValueError):
itemIndex.unpackData({"stuff": "more stuff"})
# Unknown keys should be skipped
itemIndex.unpackData({"0000000000000": {}})
assert itemIndex._items == {}
# Known keys can be added, even witout data
itemIndex.unpackData({nHandle: {}})
assert nHandle in itemIndex
# Title tags must be valid
with pytest.raises(ValueError):
itemIndex.unpackData({cHandle: {"headings": {"TTTTTTT": {}}}})
# Reference without a heading should be rejected
itemIndex.unpackData({
cHandle: {
"headings": {"T000001": {}},
"references": {"T000001": {}, "T000002": {}},
}
})
assert "T000001" in itemIndex[cHandle]
assert "T000002" not in itemIndex[cHandle]
itemIndex.clear()
# Tag keys must be strings
with pytest.raises(ValueError):
itemIndex.unpackData({
cHandle: {
"headings": {"T000001": {}},
"references": {"T000001": {1234: "@pov"}},
}
})
# Type must be strings
with pytest.raises(ValueError):
itemIndex.unpackData({
cHandle: {
"headings": {"T000001": {}},
"references": {"T000001": {"John": []}},
}
})
# Types must be valid
with pytest.raises(ValueError):
itemIndex.unpackData({
cHandle: {
"headings": {"T000001": {}},
"references": {"T000001": {"John": "@pov,@char,@stuff"}},
}
})
# This should pass
itemIndex.unpackData({
cHandle: {
"headings": {"T000001": {}},
"references": {"T000001": {"John": "@pov,@char"}},
}
})
# END Test testCoreIndex_ItemIndex