Make some fixes to the index class, and improve test coverage

This commit is contained in:
Veronica Berglyd Olsen
2022-05-30 00:28:43 +02:00
parent 865acfd7e9
commit eed59a96be
7 changed files with 320 additions and 92 deletions
+37 -16
View File
@@ -7,6 +7,7 @@ File History:
Created: 2019-04-22 [0.0.1] countWords Created: 2019-04-22 [0.0.1] countWords
Created: 2019-05-27 [0.1.4] NWIndex Created: 2019-05-27 [0.1.4] NWIndex
Created: 2022-05-28 [1.7rc1] IndexItem, IndexHeading Created: 2022-05-28 [1.7rc1] IndexItem, IndexHeading
Created: 2022-05-29 [1.7rc1] TagsIndex, ItemIndex
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen Copyright 20182022, Veronica Berglyd Olsen
@@ -43,10 +44,24 @@ logger = logging.getLogger(__name__)
H_VALID = ("H0", "H1", "H2", "H3", "H4") H_VALID = ("H0", "H1", "H2", "H3", "H4")
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
H_NONE = "T000000" TT_NONE = "T000000"
class NWIndex: class NWIndex:
"""This class holds the entire index for a given project. The index
contains the data that isn't stored in the project items themselves.
The content of the index is updated every time a file item is saved.
The primary index data is contained in the ItemIndex class, which
contains an IndexItem representing each NWItem. Each IndexItem holds
an IndexHeading object for each heading of the item's text.
A reverse index of all tags is contained in the TagsIndex class.
This is duplicate information used for quicker lookups from the tags
and back to items where they are defined.
The index data is cached in a JSON file between writing sessions.
"""
def __init__(self, theProject): def __init__(self, theProject):
@@ -104,10 +119,10 @@ class NWIndex:
moved from the archive or trash folders back into the active moved from the archive or trash folders back into the active
project. project.
""" """
logger.debug("Re-indexing item '%s'", tHandle)
if not self.theProject.tree.checkType(tHandle, nwItemType.FILE): if not self.theProject.tree.checkType(tHandle, nwItemType.FILE):
return False return False
logger.debug("Re-indexing item '%s'", tHandle)
theDoc = NWDoc(self.theProject, tHandle) theDoc = NWDoc(self.theProject, tHandle)
theText = theDoc.readDocument() theText = theDoc.readDocument()
self.scanText(tHandle, theText if theText is not None else "") self.scanText(tHandle, theText if theText is not None else "")
@@ -140,6 +155,7 @@ class NWIndex:
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE) indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
tStart = time() tStart = time()
self._indexBroken = False
if os.path.isfile(indexFile): if os.path.isfile(indexFile):
logger.debug("Loading index file") logger.debug("Loading index file")
try: try:
@@ -222,8 +238,9 @@ class NWIndex:
logger.info("Not indexing non-file item '%s'", tHandle) logger.info("Not indexing non-file item '%s'", tHandle)
return False return False
# Delete the old entry and create a new # Delete tags and create new item entry
self.deleteHandle(tHandle) for tTag in self._itemIndex.allItemTags(tHandle):
del self._tagsIndex[tTag]
self._itemIndex.add(tHandle, theItem) self._itemIndex.add(tHandle, theItem)
# Run word counter for the whole text # Run word counter for the whole text
@@ -644,7 +661,7 @@ class TagsIndex:
""" """
if tagKey in self._tags: if tagKey in self._tags:
return self._tags.get(tagKey).get("heading") return self._tags.get(tagKey).get("heading")
return H_NONE return TT_NONE
def tagClass(self, tagKey): def tagClass(self, tagKey):
"""Get the class of a given tag. """Get the class of a given tag.
@@ -782,11 +799,11 @@ class ItemIndex:
continue continue
if rootHandle is None: if rootHandle is None:
for sTitle, hItem in self._items[tHandle].items(): for sTitle in self._items[tHandle].headings():
yield tHandle, sTitle, hItem yield tHandle, sTitle, self._items[tHandle][sTitle]
elif tItem.rootHandle == rootHandle: elif tItem.itemRoot == rootHandle:
for sTitle, hItem in self._items[tHandle].items(): for sTitle in self._items[tHandle].headings():
yield tHandle, sTitle, hItem yield tHandle, sTitle, self._items[tHandle][sTitle]
else: else:
continue continue
@@ -876,7 +893,7 @@ class IndexItem:
self._index = 0 self._index = 0
# Add a placeholder heading # Add a placeholder heading
self._headings[H_NONE] = IndexHeading(H_NONE) self._headings[TT_NONE] = IndexHeading(TT_NONE)
return return
@@ -910,8 +927,8 @@ class IndexItem:
"""Add a heading to the item. Also remove the placeholder entry """Add a heading to the item. Also remove the placeholder entry
if it exists. if it exists.
""" """
if H_NONE in self._headings: if TT_NONE in self._headings:
self._headings.pop(H_NONE) self._headings.pop(TT_NONE)
self._headings[tHeading.key] = tHeading self._headings[tHeading.key] = tHeading
return return
@@ -957,6 +974,9 @@ class IndexItem:
def items(self): def items(self):
return self._headings.items() return self._headings.items()
def headings(self):
return sorted(self._headings.keys())
def allTags(self): def allTags(self):
"""Return a list of all tags in the current item. """Return a list of all tags in the current item.
""" """
@@ -1102,9 +1122,10 @@ class IndexHeading:
"""Add a record of a reference tag, and what keyword types it is """Add a record of a reference tag, and what keyword types it is
associated with. associated with.
""" """
if tagKey not in self._refs: if refType in nwKeyWords.VALID_KEYS:
self._refs[tagKey] = set() if tagKey not in self._refs:
self._refs[tagKey].add(refType) self._refs[tagKey] = set()
self._refs[tagKey].add(refType)
return return
## ##
+6 -15
View File
@@ -865,22 +865,13 @@ class GuiMain(QMainWindow):
self.theProject.index.clearIndex() self.theProject.index.clearIndex()
for tItem in self.theProject.tree: for tItem in self.theProject.tree:
if tItem is None: # pragma: no cover
continue # This is a bug trap
if tItem is not None: logger.verbose("Indexing '%s'", tItem.itemName)
self.setStatus(self.tr("Indexing: '{0}'").format(tItem.itemName)) if self.theProject.index.reIndexHandle(tItem.itemHandle):
else: # Update Word Counts
self.setStatus(self.tr("Indexing: '{0}'").format(self.tr("Unknown item"))) self.treeView.propagateCount(tItem.itemHandle, tItem.wordCount, countChildren=True)
if tItem is not None and tItem.itemType == nwItemType.FILE:
logger.verbose("Scanning '%s'", tItem.itemName)
self.theProject.index.reIndexHandle(tItem.itemHandle)
# Get Word Counts
cC, wC, pC = self.theProject.index.getCounts(tItem.itemHandle)
tItem.setCharCount(cC)
tItem.setWordCount(wC)
tItem.setParaCount(pC)
self.treeView.propagateCount(tItem.itemHandle, wC, countChildren=True)
self.treeView.setTreeItemValues(tItem.itemHandle) self.treeView.setTreeItemValues(tItem.itemHandle)
tEnd = time() tEnd = time()
+1
View File
@@ -4,5 +4,6 @@
# Mars # Mars
@tag: Mars @tag: Mars
@location: Space
Its red. Dusty and red. Its red. Dusty and red.
+1 -1
View File
@@ -1,5 +1,5 @@
%%~name: Chapter Two %%~name: Chapter Two
%%~path: e7ded148d6e4a/88706ddc78b1b %%~path: 7031beac91f75/88706ddc78b1b
%%~kind: NOVEL/DOCUMENT %%~kind: NOVEL/DOCUMENT
## Where has John Gone? ## Where has John Gone?
+1
View File
@@ -4,5 +4,6 @@
# Earth # Earth
@tag: Earth @tag: Earth
@location: Space
Third planet from the sun, fairly dense, and with lots of people on it. Third planet from the sun, fairly dense, and with lots of people on it.
+1 -1
View File
@@ -1,6 +1,6 @@
%%~name: Delete Me! %%~name: Delete Me!
%%~path: 98acd8c76c93a/b8136a5a774a0 %%~path: 98acd8c76c93a/b8136a5a774a0
%%~kind: NOVEL/DOCUMENT %%~kind: TRASH/DOCUMENT
### Delete Me! ### Delete Me!
This scene is trash. This scene is trash.
+273 -59
View File
@@ -19,14 +19,14 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import pytest
import os import os
import json import json
import pytest
from shutil import copyfile from shutil import copyfile
from mock import causeException from mock import causeException
from tools import cmpFiles from tools import buildTestProject, cmpFiles, writeFile
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.core.index import NWIndex, countWords from novelwriter.core.index import NWIndex, countWords
@@ -87,36 +87,58 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir):
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(json, "load", causeException) mp.setattr(json, "load", causeException)
assert theIndex.loadIndex() is False assert theIndex.loadIndex() is False
assert theIndex.indexBroken is True
# Make the load pass # Make the load pass
assert theIndex.loadIndex() is True assert theIndex.loadIndex() is True
assert theIndex.indexBroken is False
assert str(theIndex._tagsIndex.packData()) == tagIndex assert str(theIndex._tagsIndex.packData()) == tagIndex
assert str(theIndex._itemIndex.packData()) == itemsIndex assert str(theIndex._itemIndex.packData()) == itemsIndex
# Break the index and check that we notice # Check File
# assert theIndex.indexBroken is False copyfile(projFile, testFile)
# theIndex._tagIndex["Bod"].append("Stuff") assert cmpFiles(testFile, compFile)
# theIndex._checkIndex()
# assert theIndex.indexBroken is True # Write an emtpy index file and load it
writeFile(projFile, "{}")
assert theIndex.loadIndex() is False
assert theIndex.indexBroken is True
# Write an index file that passes loading, but is still empty
writeFile(projFile, '{"tagsIndex": {}, "itemIndex": {}}')
assert theIndex.loadIndex() is True
assert theIndex.indexBroken is False
# Check that the index is re-populated
assert "04468803b92e1" in theIndex._itemIndex
assert "2426c6f0ca922" in theIndex._itemIndex
assert "441420a886d82" in theIndex._itemIndex
assert "47666c91c7ccf" in theIndex._itemIndex
assert "4c4f28287af27" in theIndex._itemIndex
assert "846352075de7d" in theIndex._itemIndex
assert "88243afbe5ed8" in theIndex._itemIndex
assert "88d59a277361b" in theIndex._itemIndex
assert "8c58a65414c23" in theIndex._itemIndex
assert "db7e733775d4d" in theIndex._itemIndex
assert "eb103bc70c90c" in theIndex._itemIndex
assert "f8c0562e50f1b" in theIndex._itemIndex
assert "f96ec11c6a3da" in theIndex._itemIndex
assert "fb609cd8319dc" in theIndex._itemIndex
assert "7a992350f3eb6" in theIndex._itemIndex
# Finalise # Finalise
assert theProject.closeProject() is True assert theProject.closeProject() is True
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile)
# END Test testCoreIndex_LoadSave # END Test testCoreIndex_LoadSave
@pytest.mark.core @pytest.mark.core
def testCoreIndex_ScanThis(nwMinimal, mockGUI): def testCoreIndex_ScanThis(mockGUI):
"""Test the tag scanner function scanThis. """Test the tag scanner function scanThis.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
assert theProject.openProject(nwMinimal) is True theIndex = theProject.index
theIndex = NWIndex(theProject)
isValid, theBits, thePos = theIndex.scanThis("tag: this, and this") isValid, theBits, thePos = theIndex.scanThis("tag: this, and this")
assert isValid is False assert isValid is False
@@ -161,15 +183,15 @@ def testCoreIndex_ScanThis(nwMinimal, mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreIndex_CheckThese(nwMinimal, mockGUI): def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd):
"""Test the tag checker function checkThese. """Test the tag checker function checkThese.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
assert theProject.openProject(nwMinimal) is True buildTestProject(theProject, fncDir)
theIndex = theProject.index
theIndex = NWIndex(theProject) nHandle = theProject.newFile("Hello", "0000000000010")
nHandle = theProject.newFile("Hello", "a508bb932959c") cHandle = theProject.newFile("Jane", "0000000000012")
cHandle = theProject.newFile("Jane", "afb3043c7b2b3")
nItem = theProject.tree[nHandle] nItem = theProject.tree[nHandle]
cItem = theProject.tree[cHandle] cItem = theProject.tree[cHandle]
@@ -239,17 +261,16 @@ def testCoreIndex_CheckThese(nwMinimal, mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreIndex_ScanText(nwMinimal, mockGUI): def testCoreIndex_ScanText(mockGUI, fncDir, mockRnd):
"""Check the index text scanner. """Check the index text scanner.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
assert theProject.openProject(nwMinimal) is True buildTestProject(theProject, fncDir)
theIndex = theProject.index
theIndex = NWIndex(theProject)
# Some items for fail to scan tests # Some items for fail to scan tests
dHandle = theProject.newFolder("Folder", "a508bb932959c") dHandle = theProject.newFolder("Folder", "0000000000010")
xHandle = theProject.newFile("No Layout", "a508bb932959c") xHandle = theProject.newFile("No Layout", "0000000000010")
xItem = theProject.tree[xHandle] xItem = theProject.tree[xHandle]
xItem.setLayout(nwItemLayout.NO_LAYOUT) xItem.setLayout(nwItemLayout.NO_LAYOUT)
@@ -279,11 +300,11 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI):
assert theIndex.scanText(xHandle, "Hello World!") is False assert theIndex.scanText(xHandle, "Hello World!") is False
# Make some usable items # Make some usable items
tHandle = theProject.newFile("Title", "a508bb932959c") tHandle = theProject.newFile("Title", "0000000000010")
pHandle = theProject.newFile("Page", "a508bb932959c") pHandle = theProject.newFile("Page", "0000000000010")
nHandle = theProject.newFile("Hello", "a508bb932959c") nHandle = theProject.newFile("Hello", "0000000000010")
cHandle = theProject.newFile("Jane", "afb3043c7b2b3") cHandle = theProject.newFile("Jane", "0000000000012")
sHandle = theProject.newFile("Scene", "a508bb932959c") sHandle = theProject.newFile("Scene", "0000000000010")
# Text Indexing # Text Indexing
# ============= # =============
@@ -449,18 +470,27 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreIndex_ExtractData(nwMinimal, mockGUI): def testCoreIndex_ExtractData(mockGUI, fncDir, mockRnd):
"""Check the index data extraction functions. """Check the index data extraction functions.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
assert theProject.openProject(nwMinimal) is True buildTestProject(theProject, fncDir)
theIndex = NWIndex(theProject) theIndex = theProject.index
nHandle = theProject.newFile("Hello", "a508bb932959c") theIndex.reIndexHandle("0000000000010")
cHandle = theProject.newFile("Jane", "afb3043c7b2b3") theIndex.reIndexHandle("0000000000011")
theIndex.reIndexHandle("0000000000012")
theIndex.reIndexHandle("0000000000013")
theIndex.reIndexHandle("0000000000014")
theIndex.reIndexHandle("0000000000015")
theIndex.reIndexHandle("0000000000016")
theIndex.reIndexHandle("0000000000017")
nHandle = theProject.newFile("Hello", "0000000000010")
cHandle = theProject.newFile("Jane", "0000000000012")
assert theIndex.getNovelData("", "") is None assert theIndex.getNovelData("", "") is None
assert theIndex.getNovelData("a508bb932959c", "") is None assert theIndex.getNovelData("0000000000010", "") is None
assert theIndex.scanText(cHandle, ( assert theIndex.scanText(cHandle, (
"# Jane Smith\n" "# Jane Smith\n"
@@ -480,7 +510,12 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
for aKey, _, _, _ in theIndex.novelStructure(): for aKey, _, _, _ in theIndex.novelStructure():
theKeys.append(aKey) theKeys.append(aKey)
assert theKeys == ["%s:T000001" % nHandle] assert theKeys == [
"0000000000014:T000001",
"0000000000016:T000001",
"0000000000017:T000001",
"%s:T000001" % nHandle,
]
# Check that excluded files can be skipped # Check that excluded files can be skipped
theProject.tree[nHandle].setExported(False) theProject.tree[nHandle].setExported(False)
@@ -489,19 +524,22 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
for aKey, _, _, _ in theIndex.novelStructure(skipExcl=False): for aKey, _, _, _ in theIndex.novelStructure(skipExcl=False):
theKeys.append(aKey) theKeys.append(aKey)
assert theKeys == ["%s:T000001" % nHandle] assert theKeys == [
"0000000000014:T000001",
"0000000000016:T000001",
"0000000000017:T000001",
"%s:T000001" % nHandle,
]
theKeys = [] theKeys = []
for aKey, _, _, _ in theIndex.novelStructure(skipExcl=True): for aKey, _, _, _ in theIndex.novelStructure(skipExcl=True):
theKeys.append(aKey) theKeys.append(aKey)
assert theKeys == [] assert theKeys == [
"0000000000014:T000001",
theKeys = [] "0000000000016:T000001",
for aKey, _, _, _ in theIndex.novelStructure(): "0000000000017:T000001",
theKeys.append(aKey) ]
assert theKeys == []
# The novel file should have the correct counts # The novel file should have the correct counts
cC, wC, pC = theIndex.getCounts(nHandle) cC, wC, pC = theIndex.getCounts(nHandle)
@@ -528,6 +566,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
# None handle should return an empty dict # None handle should return an empty dict
assert theIndex.getBackReferenceList(None) == {} assert theIndex.getBackReferenceList(None) == {}
# The Title Page file should have no references as it has no tag
assert theIndex.getBackReferenceList("0000000000014") == {}
# The character file should have a record of the reference from the novel file # The character file should have a record of the reference from the novel file
theRefs = theIndex.getBackReferenceList(cHandle) theRefs = theIndex.getBackReferenceList(cHandle)
assert theRefs == {nHandle: "T000001"} assert theRefs == {nHandle: "T000001"}
@@ -542,6 +583,10 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
# ========= # =========
# For whole text and sections # For whole text and sections
# Invalid handle or title should return 0s
assert theIndex.getCounts("stuff") == (0, 0, 0)
assert theIndex.getCounts(nHandle, "stuff") == (0, 0, 0)
# Get section counts for a novel file # Get section counts for a novel file
assert theIndex.scanText(nHandle, ( assert theIndex.scanText(nHandle, (
"# Hello World!\n" "# Hello World!\n"
@@ -611,9 +656,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
# Novel Stats # Novel Stats
# =========== # ===========
hHandle = theProject.newFile("Chapter", "a508bb932959c") hHandle = theProject.newFile("Chapter", "0000000000010")
sHandle = theProject.newFile("Scene One", "a508bb932959c") sHandle = theProject.newFile("Scene One", "0000000000010")
tHandle = theProject.newFile("Scene Two", "a508bb932959c") tHandle = theProject.newFile("Scene Two", "0000000000010")
theProject.tree[hHandle].itemLayout == nwItemLayout.DOCUMENT theProject.tree[hHandle].itemLayout == nwItemLayout.DOCUMENT
theProject.tree[sHandle].itemLayout == nwItemLayout.DOCUMENT theProject.tree[sHandle].itemLayout == nwItemLayout.DOCUMENT
@@ -624,6 +669,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
assert theIndex.scanText(tHandle, "### Scene Two\n\n") assert theIndex.scanText(tHandle, "### Scene Two\n\n")
assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [ assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [
("0000000000014", "T000001"),
("0000000000016", "T000001"),
("0000000000017", "T000001"),
(nHandle, "T000001"), (nHandle, "T000001"),
(nHandle, "T000011"), (nHandle, "T000011"),
(hHandle, "T000001"), (hHandle, "T000001"),
@@ -632,6 +680,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
] ]
assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=True)] == [ assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=True)] == [
("0000000000014", "T000001"),
("0000000000016", "T000001"),
("0000000000017", "T000001"),
(hHandle, "T000001"), (hHandle, "T000001"),
(sHandle, "T000001"), (sHandle, "T000001"),
(tHandle, "T000001"), (tHandle, "T000001"),
@@ -640,6 +691,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
# Add a fake handle to the tree and check that it's ignored # Add a fake handle to the tree and check that it's ignored
theProject.tree._treeOrder.append("0000000000000") theProject.tree._treeOrder.append("0000000000000")
assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [ assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [
("0000000000014", "T000001"),
("0000000000016", "T000001"),
("0000000000017", "T000001"),
(nHandle, "T000001"), (nHandle, "T000001"),
(nHandle, "T000011"), (nHandle, "T000011"),
(hHandle, "T000001"), (hHandle, "T000001"),
@@ -649,25 +703,33 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
theProject.tree._treeOrder.remove("0000000000000") theProject.tree._treeOrder.remove("0000000000000")
# Extract stats # Extract stats
assert theIndex.getNovelWordCount(False) == 34 assert theIndex.getNovelWordCount(skipExcl=False) == 43
assert theIndex.getNovelWordCount(True) == 6 assert theIndex.getNovelWordCount(skipExcl=True) == 15
assert theIndex.getNovelTitleCounts(False) == [0, 2, 1, 2, 0] assert theIndex.getNovelTitleCounts(skipExcl=False) == [0, 3, 2, 3, 0]
assert theIndex.getNovelTitleCounts(True) == [0, 0, 1, 2, 0] assert theIndex.getNovelTitleCounts(skipExcl=True) == [0, 1, 2, 3, 0]
# Table of Contents # Table of Contents
assert theIndex.getTableOfContents(0, True) == [] assert theIndex.getTableOfContents(0, skipExcl=True) == []
assert theIndex.getTableOfContents(1, True) == [] assert theIndex.getTableOfContents(1, skipExcl=True) == [
assert theIndex.getTableOfContents(2, True) == [ ("0000000000014:T000001", 1, "New Novel", 15),
]
assert theIndex.getTableOfContents(2, skipExcl=True) == [
("0000000000014:T000001", 1, "New Novel", 5),
("0000000000016:T000001", 2, "New Chapter", 4),
("%s:T000001" % hHandle, 2, "Chapter One", 6), ("%s:T000001" % hHandle, 2, "Chapter One", 6),
] ]
assert theIndex.getTableOfContents(3, True) == [ assert theIndex.getTableOfContents(3, skipExcl=True) == [
("0000000000014:T000001", 1, "New Novel", 5),
("0000000000016:T000001", 2, "New Chapter", 2),
("0000000000017:T000001", 3, "New Scene", 2),
("%s:T000001" % hHandle, 2, "Chapter One", 2), ("%s:T000001" % hHandle, 2, "Chapter One", 2),
("%s:T000001" % sHandle, 3, "Scene One", 2), ("%s:T000001" % sHandle, 3, "Scene One", 2),
("%s:T000001" % tHandle, 3, "Scene Two", 2), ("%s:T000001" % tHandle, 3, "Scene Two", 2),
] ]
assert theIndex.getTableOfContents(0, False) == [] assert theIndex.getTableOfContents(0, skipExcl=False) == []
assert theIndex.getTableOfContents(1, False) == [ assert theIndex.getTableOfContents(1, skipExcl=False) == [
("0000000000014:T000001", 1, "New Novel", 9),
("%s:T000001" % nHandle, 1, "Hello World!", 12), ("%s:T000001" % nHandle, 1, "Hello World!", 12),
("%s:T000011" % nHandle, 1, "Hello World!", 22), ("%s:T000011" % nHandle, 1, "Hello World!", 22),
] ]
@@ -682,7 +744,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
("%s:T000001" % nHandle, 12), ("%s:T000011" % nHandle, 16) ("%s:T000001" % nHandle, 12), ("%s:T000011" % nHandle, 16)
] ]
assert theProject.closeProject() assert theIndex.saveIndex() is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
# Header Record # Header Record
bHandle = "0000000000000" bHandle = "0000000000000"
@@ -697,6 +761,156 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
# END Test testCoreIndex_ExtractData # END Test testCoreIndex_ExtractData
@pytest.mark.core
def testCoreIndex_ItemIndex(mockGUI, fncDir, mockRnd):
"""Check the ItemIndex class.
"""
theProject = NWProject(mockGUI)
buildTestProject(theProject, fncDir)
nHandle = "0000000000014"
cHandle = "0000000000016"
sHandle = "0000000000017"
assert theProject.index.saveIndex() is True
itemIndex = theProject.index._itemIndex
# The index should be empty
assert nHandle not in itemIndex
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
# Add the novel chapter file
itemIndex.add(cHandle, theProject.tree[cHandle])
assert cHandle in itemIndex
assert itemIndex[cHandle].item == theProject.tree[cHandle]
assert itemIndex.mainItemHeader(cHandle) == "H0"
assert itemIndex.allItemTags(cHandle) == []
assert list(itemIndex.iterItemHeaders(cHandle))[0][0] == "T000000"
# Add a heading to the item, which should replace the T000000 heading
itemIndex.addItemHeading(cHandle, "T000001", "H2", "Chapter One")
assert itemIndex.mainItemHeader(cHandle) == "H2"
assert list(itemIndex.iterItemHeaders(cHandle))[0][0] == "T000001"
# 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.addHeadingReferences(cHandle, "T000001", ["Jane"], "@pov")
itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane"], "@focus")
itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane", "John"], "@char")
idxData = itemIndex.packData()
assert idxData[cHandle]["level"] == "H2"
assert idxData[cHandle]["headings"]["T000001"] == {
"level": "H2", "title": "Chapter One", "tag": "One",
"cCount": 60, "wCount": 10, "pCount": 2, "synopsis": "In the beginning ...",
}
assert "@pov" in idxData[cHandle]["references"]["T000001"]["Jane"]
assert "@focus" in idxData[cHandle]["references"]["T000001"]["Jane"]
assert "@char" in idxData[cHandle]["references"]["T000001"]["Jane"]
assert "@char" in idxData[cHandle]["references"]["T000001"]["John"]
# Add the other two files
itemIndex.add(nHandle, theProject.tree[nHandle])
itemIndex.add(sHandle, theProject.tree[sHandle])
itemIndex.addItemHeading(nHandle, "T000001", "H1", "Novel")
itemIndex.addItemHeading(sHandle, "T000001", "H3", "Scene One")
# Data Extraction
# ===============
# Get headers
allHeads = list(itemIndex.iterAllHeaders())
assert allHeads[0][0] == cHandle
assert allHeads[1][0] == nHandle
assert allHeads[2][0] == sHandle
assert allHeads[0][1] == "T000001"
assert allHeads[1][1] == "T000001"
assert allHeads[2][1] == "T000001"
# Ask for stuff that doesn't exist
assert itemIndex.mainItemHeader("blablabla") == "H0"
assert itemIndex.allItemTags("blablabla") == []
# Novel Structure
# ===============
# Add a second novel
mHandle = theProject.newRoot(nwItemClass.NOVEL)
uHandle = theProject.newFile("Title Page", mHandle)
itemIndex.add(uHandle, theProject.tree[uHandle])
itemIndex.addItemHeading(uHandle, "T000001", "H1", "Novel 2")
assert uHandle in itemIndex
# Structure of all novels
nStruct = list(itemIndex.iterNovelStructure())
assert len(nStruct) == 4
assert nStruct[0][0] == nHandle
assert nStruct[1][0] == cHandle
assert nStruct[2][0] == sHandle
assert nStruct[3][0] == uHandle
# Novel structure with root handle set
nStruct = list(itemIndex.iterNovelStructure(rootHandle="0000000000010"))
assert len(nStruct) == 3
assert nStruct[0][0] == nHandle
assert nStruct[1][0] == cHandle
assert nStruct[2][0] == sHandle
nStruct = list(itemIndex.iterNovelStructure(rootHandle=mHandle))
assert len(nStruct) == 1
assert nStruct[0][0] == uHandle
# Inject garbage into tree
theProject.tree._treeOrder.append("stuff")
nStruct = list(itemIndex.iterNovelStructure())
assert len(nStruct) == 4
assert nStruct[0][0] == nHandle
assert nStruct[1][0] == cHandle
assert nStruct[2][0] == sHandle
assert nStruct[3][0] == uHandle
# Skip excluded
theProject.tree[sHandle].setExported(False)
nStruct = list(itemIndex.iterNovelStructure(skipExcl=True))
assert len(nStruct) == 3
assert nStruct[0][0] == nHandle
assert nStruct[1][0] == cHandle
assert nStruct[2][0] == uHandle
# Delete new item
del itemIndex[uHandle]
assert uHandle not in itemIndex
# END Test testCoreIndex_ItemIndex
@pytest.mark.core @pytest.mark.core
def testCoreIndex_CountWords(): def testCoreIndex_CountWords():
"""Test the word counter and the exclusion filers. """Test the word counter and the exclusion filers.