Add test coverage

This commit is contained in:
Veronica Berglyd Olsen
2025-02-22 15:07:15 +01:00
parent 1fa7e08b17
commit 3a2440fb92
4 changed files with 243 additions and 184 deletions
+18 -18
View File
@@ -291,20 +291,20 @@ class IndexHeading:
self._comments["summary"] = str(text) self._comments["summary"] = str(text)
return return
def setTag(self, tagKey: str) -> None: def setTag(self, tag: str) -> None:
"""Set the tag for references, and make sure it is a string.""" """Set the tag for references, and make sure it is a string."""
self._tag = str(tagKey).lower() self._tag = str(tag).lower()
return return
def addReference(self, tagKey: str, refType: str) -> None: def addReference(self, tag: str, keyword: str) -> None:
"""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 refType in nwKeyWords.VALID_KEYS: if keyword in nwKeyWords.VALID_KEYS:
tagKey = tagKey.lower() tag = tag.lower()
if tagKey not in self._refs: if tag not in self._refs:
self._refs[tagKey] = set() self._refs[tag] = set()
self._refs[tagKey].add(refType) self._refs[tag].add(keyword)
return return
## ##
@@ -337,18 +337,18 @@ class IndexHeading:
self.setLine(entry.get("line", 0)) self.setLine(entry.get("line", 0))
self.setCounts(entry.get("counts", [0, 0, 0])) self.setCounts(entry.get("counts", [0, 0, 0]))
elif key == "refs": elif key == "refs":
for key, types in entry.items(): for tag, value in entry.items():
if not isinstance(key, str): if not isinstance(tag, str):
raise ValueError("itemIndex reference key must be a string") raise ValueError("Heading reference key must be a string")
if not isinstance(types, str): if not isinstance(value, str):
raise ValueError("itemIndex reference type must be a string") raise ValueError("Heading reference value must be a string")
for refType in types.split(","): for keyword in value.split(","):
if refType in nwKeyWords.VALID_KEYS: if keyword in nwKeyWords.VALID_KEYS:
self.addReference(key, refType) self.addReference(tag, keyword)
else: else:
raise ValueError("The itemIndex contains an invalid reference type") raise ValueError("Heading reference contains an invalid keyword")
elif key == "summary" or key.startswith("story"): elif key == "summary" or key.startswith("story"):
self._comments[str(key)] = str(entry) self._comments[str(key)] = str(entry)
else: else:
raise KeyError("Unknown key in itemIndex") raise KeyError("Unknown key in heading entry")
return return
@@ -6,107 +6,103 @@
}, },
"novelWriter.itemIndex": { "novelWriter.itemIndex": {
"7a992350f3eb6": { "7a992350f3eb6": {
"headings": { "T0001": {
"T0001": {"level": "H1", "title": "Lorem Ipsum", "line": 1, "tag": "", "cCount": 259, "wCount": 44, "pCount": 5, "synopsis": ""} "meta": {"level": "H1", "title": "Lorem Ipsum", "line": 1, "tag": "", "counts": [259, 44, 5]}
} }
}, },
"8c58a65414c23": { "8c58a65414c23": {
"headings": { "T0000": {
"T0000": {"level": "H0", "title": "", "line": 0, "tag": "", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""} "meta": {"level": "H0", "title": "", "line": 0, "tag": "", "counts": [1058, 176, 2]}
} }
}, },
"88d59a277361b": { "88d59a277361b": {
"headings": { "T0001": {
"T0001": {"level": "H2", "title": "Prologue", "line": 1, "tag": "", "cCount": 605, "wCount": 94, "pCount": 2, "synopsis": "Explanation from the lipsum.com website."} "meta": {"level": "H2", "title": "Prologue", "line": 1, "tag": "", "counts": [605, 94, 2]},
"summary": "Explanation from the lipsum.com website."
}, },
"notes": { "document": {
"footnotes": ["f9kgf"] "footnotes": ["f9kgf"]
} }
}, },
"db7e733775d4d": { "db7e733775d4d": {
"headings": { "T0001": {
"T0001": {"level": "H1", "title": "Act One", "line": 1, "tag": "", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""} "meta": {"level": "H1", "title": "Act One", "line": 1, "tag": "", "counts": [35, 6, 1]}
} }
}, },
"fb609cd8319dc": { "fb609cd8319dc": {
"headings": { "T0001": {
"T0001": {"level": "H2", "title": "Chapter One", "line": 1, "tag": "", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."} "meta": {"level": "H2", "title": "Chapter One", "line": 1, "tag": "", "counts": [419, 67, 1]},
}, "refs": {"bod": "@pov", "main": "@plot", "europe": "@location"},
"references": { "summary": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."
"T0001": {"bod": "@pov", "main": "@plot", "europe": "@location"}
} }
}, },
"88243afbe5ed8": { "88243afbe5ed8": {
"headings": { "T0001": {
"T0001": {"level": "H3", "title": "Scene One", "line": 1, "tag": "", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."}, "meta": {"level": "H3", "title": "Scene One", "line": 1, "tag": "", "counts": [1197, 174, 2]},
"T0002": {"level": "H4", "title": "Scene One, Section Two", "line": 13, "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""} "refs": {"bod": "@pov", "main": "@plot", "europe": "@location"},
"summary": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."
}, },
"references": { "T0002": {
"T0001": {"bod": "@pov", "main": "@plot", "europe": "@location"} "meta": {"level": "H4", "title": "Scene One, Section Two", "line": 13, "tag": "", "counts": [1561, 230, 2]}
} }
}, },
"f96ec11c6a3da": { "f96ec11c6a3da": {
"headings": { "T0001": {
"T0001": {"level": "H3", "title": "Scene Two", "line": 1, "tag": "", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."}, "meta": {"level": "H3", "title": "Scene Two", "line": 1, "tag": "", "counts": [2034, 299, 3]},
"T0002": {"level": "H4", "title": "Scene Two, Section Two", "line": 15, "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""} "refs": {"bod": "@pov", "main": "@plot", "europe": "@location"},
"summary": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."
}, },
"references": { "T0002": {
"T0001": {"bod": "@pov", "main": "@plot", "europe": "@location"} "meta": {"level": "H4", "title": "Scene Two, Section Two", "line": 15, "tag": "", "counts": [2009, 301, 3]}
} }
}, },
"846352075de7d": { "846352075de7d": {
"headings": { "T0001": {
"T0001": {"level": "H2", "title": "Why do we use it?", "line": 1, "tag": "", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""} "meta": {"level": "H2", "title": "Why do we use it?", "line": 1, "tag": "", "counts": [631, 109, 3]}
} }
}, },
"441420a886d82": { "441420a886d82": {
"headings": { "T0001": {
"T0001": {"level": "H2", "title": "Chapter Two", "line": 1, "tag": "", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."} "meta": {"level": "H2", "title": "Chapter Two", "line": 1, "tag": "", "counts": [477, 70, 1]},
}, "refs": {"bod": "@pov", "main": "@plot", "europe": "@location"},
"references": { "summary": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."
"T0001": {"bod": "@pov", "main": "@plot", "europe": "@location"}
} }
}, },
"eb103bc70c90c": { "eb103bc70c90c": {
"headings": { "T0001": {
"T0001": {"level": "H3", "title": "Scene Three", "line": 1, "tag": "", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."} "meta": {"level": "H3", "title": "Scene Three", "line": 1, "tag": "", "counts": [3006, 439, 4]},
}, "refs": {"bod": "@pov", "main": "@plot", "europe": "@location"},
"references": { "summary": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."
"T0001": {"bod": "@pov", "main": "@plot", "europe": "@location"}
} }
}, },
"f8c0562e50f1b": { "f8c0562e50f1b": {
"headings": { "T0001": {
"T0001": {"level": "H3", "title": "Scene Four", "line": 1, "tag": "", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."} "meta": {"level": "H3", "title": "Scene Four", "line": 1, "tag": "", "counts": [3839, 563, 6]},
}, "refs": {"bod": "@pov", "main": "@plot", "europe": "@location"},
"references": { "summary": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."
"T0001": {"bod": "@pov", "main": "@plot", "europe": "@location"}
} }
}, },
"47666c91c7ccf": { "47666c91c7ccf": {
"headings": { "T0001": {
"T0001": {"level": "H3", "title": "Scene Five", "line": 1, "tag": "", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."} "meta": {"level": "H3", "title": "Scene Five", "line": 1, "tag": "", "counts": [3644, 543, 5]},
}, "refs": {"bod": "@pov", "main": "@plot", "europe": "@location"},
"references": { "summary": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."
"T0001": {"bod": "@pov", "main": "@plot", "europe": "@location"}
} }
}, },
"4c4f28287af27": { "4c4f28287af27": {
"headings": { "T0001": {
"T0001": {"level": "H1", "title": "Nobody Owens", "line": 1, "tag": "bod", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""} "meta": {"level": "H1", "title": "Nobody Owens", "line": 1, "tag": "bod", "counts": [1864, 284, 3]},
}, "refs": {"main": "@plot"}
"references": {
"T0001": {"main": "@plot"}
} }
}, },
"2426c6f0ca922": { "2426c6f0ca922": {
"headings": { "T0001": {
"T0001": {"level": "H1", "title": "Main Plot", "line": 1, "tag": "main", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""} "meta": {"level": "H1", "title": "Main Plot", "line": 1, "tag": "main", "counts": [1369, 195, 2]}
} }
}, },
"04468803b92e1": { "04468803b92e1": {
"headings": { "T0001": {
"T0001": {"level": "H1", "title": "Ancient Europe", "line": 1, "tag": "europe", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""} "meta": {"level": "H1", "title": "Ancient Europe", "line": 1, "tag": "europe", "counts": [1770, 259, 3]}
} }
} }
} }
+2 -111
View File
@@ -1198,13 +1198,13 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
idxData = itemIndex.packData() idxData = itemIndex.packData()
assert idxData[cHandle]["T0001"]["meta"] == { assert idxData[cHandle]["T0001"]["meta"] == {
"level": "H2", "line": 1, "title": "Chapter One", "tag": "one", "level": "H2", "line": 1, "title": "Chapter One", "tag": "one", "counts": (60, 10, 2),
"counts": (60, 10, 2), "summary": "In the beginning ...",
} }
assert "@pov" in idxData[cHandle]["T0001"]["refs"]["jane"] assert "@pov" in idxData[cHandle]["T0001"]["refs"]["jane"]
assert "@focus" in idxData[cHandle]["T0001"]["refs"]["jane"] assert "@focus" in idxData[cHandle]["T0001"]["refs"]["jane"]
assert "@char" in idxData[cHandle]["T0001"]["refs"]["jane"] assert "@char" in idxData[cHandle]["T0001"]["refs"]["jane"]
assert "@char" in idxData[cHandle]["T0001"]["refs"]["john"] assert "@char" in idxData[cHandle]["T0001"]["refs"]["john"]
assert idxData[cHandle]["T0001"]["summary"] == "In the beginning ..."
# Add the other two files # Add the other two files
itemIndex.add(nHandle, project.tree[nHandle]) # type: ignore itemIndex.add(nHandle, project.tree[nHandle]) # type: ignore
@@ -1310,112 +1310,3 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
# Delete new item # Delete new item
del itemIndex[uHandle] # type: ignore del itemIndex[uHandle] # type: ignore
assert uHandle not in itemIndex 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) as exc:
itemIndex.unpackData("stuff") # type: ignore
assert str(exc.value) == "itemIndex is not a dict"
# Keys must be valid handles
with pytest.raises(ValueError) as exc:
itemIndex.unpackData({"stuff": "more stuff"})
assert str(exc.value) == "itemIndex keys must be handles"
# Unknown keys should be skipped
itemIndex.unpackData({C.hInvalid: {}})
assert itemIndex._items == {}
# Known keys can be added, even without data
itemIndex.unpackData({nHandle: {}})
assert nHandle in itemIndex
assert itemIndex[nHandle].handle == nHandle # type: ignore
# Title tags must be valid
with pytest.raises(ValueError) as exc:
itemIndex.unpackData({cHandle: {"headings": {"TTTTTTT": {}}}})
assert str(exc.value) == "The itemIndex contains an invalid title key"
# Reference without a heading should be rejected
itemIndex.unpackData({
cHandle: {
"headings": {"T0001": {}},
"references": {"T0001": {}, "T0002": {}},
}
})
assert "T0001" in itemIndex[cHandle] # type: ignore
assert "T0002" not in itemIndex[cHandle] # type: ignore
itemIndex.clear()
# Tag keys must be strings
with pytest.raises(ValueError) as exc:
itemIndex.unpackData({
cHandle: {
"headings": {"T0001": {}},
"references": {"T0001": {1234: "@pov"}},
"notes": {"footnotes": [], "comments": []},
}
})
assert str(exc.value) == "itemIndex reference key must be a string"
# Type must be strings
with pytest.raises(ValueError) as exc:
itemIndex.unpackData({
cHandle: {
"headings": {"T0001": {}},
"references": {"T0001": {"John": []}},
"notes": {"footnotes": [], "comments": []},
}
})
assert str(exc.value) == "itemIndex reference type must be a string"
# Types must be valid
with pytest.raises(ValueError) as exc:
itemIndex.unpackData({
cHandle: {
"headings": {"T0001": {}},
"references": {"T0001": {"John": "@pov,@char,@stuff"}},
"notes": {"footnotes": [], "comments": []},
}
})
assert str(exc.value) == "The itemIndex contains an invalid reference type"
# Note type must be valid
with pytest.raises(ValueError) as exc:
itemIndex.unpackData({
cHandle: {
"headings": {"T0001": {}},
"references": {"T0001": {"John": "@pov,@char"}},
"notes": {"stuff": [], "comments": []},
}
})
assert str(exc.value) == "The notes style is invalid"
# Note keys must be all strings
with pytest.raises(ValueError) as exc:
itemIndex.unpackData({
cHandle: {
"headings": {"T0001": {}},
"references": {"T0001": {"John": "@pov,@char"}},
"notes": {"footnotes": ["fkey", 1], "comments": []},
}
})
assert str(exc.value) == "The notes keys must be a list of strings"
# This should pass
itemIndex.unpackData({
cHandle: {
"headings": {"T0001": {}},
"references": {"T0001": {"John": "@pov,@char"}},
"notes": {"footnotes": ["fkey"], "comments": ["ckey"]},
}
})
+172
View File
@@ -0,0 +1,172 @@
"""
novelWriter NWIndex Class Tester
==================================
This file is a part of novelWriter
Copyright (C) 2020 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 novelwriter.core.indexdata import IndexHeading
@pytest.mark.core
def testCoreIndexData_IndexHeading():
"""Test the IndexHeading class."""
# Defaults
head = IndexHeading("T0001")
assert repr(head) == "<IndexHeading key='T0001'>"
assert str(head) == "<IndexHeading key='T0001'>"
assert head.key == "T0001"
assert head.line == 0
assert head.level == "H0"
assert head.title == ""
assert head.charCount == 0
assert head.wordCount == 0
assert head.paraCount == 0
assert head.synopsis == ""
assert head.tag == ""
assert head.references == {}
# Set Level
head.setLevel("Stuff") # Invalid
assert head.level == "H0"
head.setLevel("H1") # Valid
assert head.level == "H1"
# Set Line
head.setLine(-1) # Invalid
assert head.line == 0
head.setLine(42) # Valid
assert head.line == 42
# Set Counts
head.setCounts([1, 2]) # Invalid, must be three values
assert head.charCount == 0
assert head.wordCount == 0
assert head.paraCount == 0
head.setCounts([42, 4, 2]) # Valid
assert head.charCount == 42
assert head.wordCount == 4
assert head.paraCount == 2
# Set Summary
head.setSynopsis("In the beginning ...")
assert head.synopsis == "In the beginning ..."
# Set Tag
head.setTag("Stuff")
assert head.tag == "stuff" # Case insensitive
# Add References
head.addReference("Stuff", "@stuff") # Invalid type
assert head.references == {}
head.addReference("Stuff", "@object") # Valid type
assert head.references == {"stuff": {"@object"}}
# Pack Data
assert head.packData() == {
"meta": {"level": "H1", "title": "", "line": 42, "tag": "stuff", "counts": (42, 4, 2)},
"refs": {"stuff": "@object"},
"summary": "In the beginning ...",
}
# Unpack KeyError
with pytest.raises(KeyError, match="Unknown key in heading entry"):
head.unpackData({"stuff": "more stuff"})
# Unpack Comments
head.unpackData({"summary": "How it started ..."})
assert head.synopsis == "How it started ..."
@pytest.mark.core
def testCoreIndexData_IndexHeadingUnpackMeta():
"""Test IndexHeading class meta unpacking."""
# Valid
data = {"meta": {
"level": "H1", "title": "So it Begins", "line": 1, "tag": "begins", "counts": [95, 18, 1]
}}
head = IndexHeading("T0001")
head.unpackData(data)
assert head.level == "H1"
assert head.title == "So it Begins"
assert head.line == 1
assert head.tag == "begins"
assert head.charCount == 95
assert head.wordCount == 18
assert head.paraCount == 1
# Invalid
data = {"meta": {
"level": "H9", "title": None, "line": None, "tag": None, "counts": [42]
}}
head = IndexHeading("T0001")
head.unpackData(data)
assert head.level == "H0"
assert head.title == "None"
assert head.line == 0
assert head.tag == "None"
assert head.charCount == 0
assert head.wordCount == 0
assert head.paraCount == 0
# Empty
data = {"meta": {}}
head = IndexHeading("T0001")
head.unpackData(data)
assert head.level == "H0"
assert head.title == ""
assert head.line == 0
assert head.tag == ""
assert head.charCount == 0
assert head.wordCount == 0
assert head.paraCount == 0
@pytest.mark.core
def testCoreIndexData_IndexHeadingUnpackRefs():
"""Test IndexHeading class refs unpacking."""
# Valid
data = {"refs": {
"jane": "@char,@pov", "john": "@char", "earth": "@location", "space": "@mention,@location"
}}
head = IndexHeading("T0001")
head.unpackData(data)
assert head.references["jane"] == {"@char", "@pov"}
assert head.references["john"] == {"@char"}
assert head.references["earth"] == {"@location"}
assert head.references["space"] == {"@location", "@mention"}
# Invalid key
data = {"refs": {0: "@char,@pov"}}
head = IndexHeading("T0001")
with pytest.raises(ValueError, match="Heading reference key must be a string"):
head.unpackData(data)
# Invalid value
data = {"refs": {"jane": None}}
head = IndexHeading("T0001")
with pytest.raises(ValueError, match="Heading reference value must be a string"):
head.unpackData(data)
# Invalid keyword
data = {"refs": {"jane": "@char,@pov,@stuff"}}
head = IndexHeading("T0001")
with pytest.raises(ValueError, match="Heading reference contains an invalid keyword"):
head.unpackData(data)