Add test coverage of the project tree consistency function

This commit is contained in:
Veronica Berglyd Olsen
2023-07-26 18:57:31 +02:00
parent 1752d5c5cb
commit 39ef662807
4 changed files with 91 additions and 94 deletions
+10 -7
View File
@@ -26,7 +26,7 @@ from __future__ import annotations
import random
import logging
from typing import TYPE_CHECKING, Iterator, overload
from typing import TYPE_CHECKING, Iterator, Literal, overload
from pathlib import Path
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
@@ -94,19 +94,21 @@ class NWTree:
return self._treeOrder.copy()
@overload
def create(self, label: str, parent: None, itemType: nwItemType,
itemClass: nwItemClass = nwItemClass.NO_CLASS) -> str:
...
def create(self, label: str, parent: None, itemType: Literal[nwItemType.ROOT],
itemClass: nwItemClass) -> str: # pragma: no cover
pass
@overload
def create(self, label: str, parent: str | None, itemType: nwItemType,
itemClass: nwItemClass = nwItemClass.NO_CLASS) -> str | None:
...
itemClass: nwItemClass = nwItemClass.NO_CLASS) -> str | None: # pragma: no cover
pass
def create(self, label, parent, itemType, itemClass=nwItemClass.NO_CLASS):
"""Create a new item in the project tree, and return its handle.
If the item cannot be added to the project, None is returned.
If the item cannot be added to the project because of an invalid
parent, None is returned. For root elements, this cannot occur.
"""
parent = None if itemType == nwItemType.ROOT else parent
if parent is None or parent in self._treeOrder:
tHandle = self._makeHandle()
newItem = NWItem(self._project, tHandle)
@@ -238,6 +240,7 @@ class NWTree:
newItem.setClass(oClass)
newItem.setLayout(oLayout)
if self.append(newItem):
self.updateItemData(cHandle)
recovered += 1
return orphans, recovered
+2 -86
View File
@@ -25,10 +25,10 @@ from shutil import copyfile
from zipfile import ZipFile
from mocked import causeOSError
from tools import C, cmpFiles, writeFile, buildTestProject, XML_IGNORE
from tools import C, cmpFiles, buildTestProject, XML_IGNORE
from novelwriter import CONFIG
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
from novelwriter.enum import nwItemClass
from novelwriter.constants import nwFiles
from novelwriter.core.tree import NWTree
from novelwriter.core.index import NWIndex
@@ -573,90 +573,6 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
# END Test testCoreProject_Methods
@pytest.mark.core
@pytest.mark.skip
def testCoreProject_OrphanedFiles(mockGUI, prjLipsum):
"""Check that files in the content folder that are not tracked in
the project XML file are handled correctly by the orphaned files
function. It should also restore as much meta data as possible from
the meta line at the top of the document file.
"""
theProject = NWProject(mockGUI)
assert theProject.openProject(prjLipsum) is True
assert theProject.tree["636b6aa9b697b"] is None
# Add a file with non-existent parent
# This file will be removed from the project on open
oHandle = theProject.newFile("Oops", "b3643d0f92e32")
theProject.tree[oHandle].setParent("1234567890abc")
# Save and close
assert theProject.saveProject() is True
theProject.closeProject()
# First Item with Meta Data
orphPath = prjLipsum / "content" / "636b6aa9b697b.nwd"
writeFile(orphPath, (
"%%~name:[Recovered] Mars\n"
"%%~path:5eaea4e8cdee8/636b6aa9b697b\n"
"%%~kind:WORLD/NOTE\n"
"%%~invalid\n"
"\n"
))
# Second Item without Meta Data
orphPath = prjLipsum / "content" / "736b6aa9b697b.nwd"
writeFile(orphPath, "\n")
# Invalid File Name
tstPath = prjLipsum / "content" / "636b6aa9b697b.txt"
writeFile(tstPath, "\n")
# Invalid File Name
tstPath = prjLipsum / "content" / "636b6aa9b697bb.nwd"
writeFile(tstPath, "\n")
# Invalid File Name
tstPath = prjLipsum / "content" / "abcdefghijklm.nwd"
writeFile(tstPath, "\n")
assert theProject.openProject(prjLipsum)
assert theProject.storage.storagePath is not None
assert theProject.storage.runtimePath is not None
assert theProject.tree["636b6aa9b697bb"] is None
assert theProject.tree["abcdefghijklm"] is None
# First Item with Meta Data
oItem = theProject.tree["636b6aa9b697b"]
assert oItem is not None
assert oItem.itemName == "[Recovered] Mars"
assert oItem.itemHandle == "636b6aa9b697b"
assert oItem.itemParent == "60bdf227455cc"
assert oItem.itemClass == nwItemClass.WORLD
assert oItem.itemType == nwItemType.FILE
assert oItem.itemLayout == nwItemLayout.NOTE
# Second Item without Meta Data
oItem = theProject.tree["736b6aa9b697b"]
assert oItem is not None
assert oItem.itemName == "Recovered File 1"
assert oItem.itemHandle == "736b6aa9b697b"
assert oItem.itemParent == "b3643d0f92e32"
assert oItem.itemClass == nwItemClass.NOVEL
assert oItem.itemType == nwItemType.FILE
assert oItem.itemLayout == nwItemLayout.NOTE
assert theProject.saveProject(prjLipsum)
theProject.closeProject()
# Finally, check that the orphaned files function returns
# if no project is open and no path is set
assert not theProject._scanProjectFolder()
# END Test testCoreProject_OrphanedFiles
@pytest.mark.core
def testCoreProject_Backup(monkeypatch, mockGUI, fncPath, tstPaths):
"""Test the automated backup feature of the project class. The test
+1
View File
@@ -140,6 +140,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
assert xmlReader.state == XMLReadState.PARSED_OK
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0105
assert xmlReader.xmlRevision == 1
assert xmlReader.appVersion == "2.0-rc1"
assert xmlReader.hexVersion == 0x020000c1
+78 -1
View File
@@ -18,12 +18,14 @@ 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
import random
from pathlib import Path
from tools import C, buildTestProject
from mocked import causeOSError
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
@@ -212,6 +214,14 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
assert theTree.append(itemU) is False
assert len(theTree) == len(mockItems) + 1
# Create a new root, but with a parent set anyway (the parent should be ignored)
zHandle = theTree.create("Custom", "a000000000001", nwItemType.ROOT, nwItemClass.CUSTOM)
assert isinstance(zHandle, str)
itemZ = theTree[zHandle]
assert isinstance(itemZ, NWItem)
assert itemZ.itemParent is None
del theTree[zHandle]
# Duplicate Items
# ===============
@@ -224,7 +234,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
assert len(theTree) == len(mockItems) + 2
dHandle = itemV.itemHandle
assert dHandle == "0000000000001"
assert dHandle == "0000000000002"
# Delete Items
# ============
@@ -285,6 +295,73 @@ def testCoreTree_PackUnpack(mockGUI, mockItems):
# END Test testCoreTree_PackUnpack
@pytest.mark.core
def testCoreTree_CheckConsistency(caplog: pytest.LogCaptureFixture, mockGUI, fncPath, mockRnd):
"""Check the project consistency."""
theProject = NWProject(mockGUI)
buildTestProject(theProject, fncPath)
# By default, all is well
caplog.clear()
assert theProject.tree.checkConsistency("Recovered") == (0, 0)
assert all(m.endswith("OK") for m in caplog.messages)
# Give the scene file an unknown parent
caplog.clear()
theProject.tree[C.hSceneDoc].setParent(C.hInvalid) # type: ignore
assert theProject.tree.checkConsistency("Recovered") == (1, 1)
assert f"'{C.hSceneDoc}' ... ERROR" in caplog.text
# The scene file should have been added back to its home
itemS = theProject.tree[C.hSceneDoc]
assert isinstance(itemS, NWItem)
assert itemS.itemParent == C.hChapterDir
# Create a new file with no meta data, and let the function handle it as orphaned
xHandle = "0123456789abc"
contentPath = theProject.storage.contentPath
assert isinstance(contentPath, Path)
assert contentPath == fncPath / "content"
(contentPath / f"{xHandle}.nwd").write_text("### Stuff", encoding="utf-8")
assert theProject.tree.checkConsistency("Recovered") == (1, 1)
assert xHandle in theProject.tree
itemX = theProject.tree[xHandle]
assert isinstance(itemX, NWItem)
# It should by default be added as a Novel file
assert itemX.itemParent == C.hNovelRoot
assert itemX.itemRoot == C.hNovelRoot
assert itemX.itemClass == nwItemClass.NOVEL
assert itemX.itemName == "[Recovered] 0123456789abc"
# Set an unknown class in the orphaned item
itemX.setClass(nwItemClass.OBJECT)
itemX.setName("Stuff")
itemX.setParent(C.hInvalid)
theProject.storage.getDocument(xHandle).writeDocument("### Stuff") # This adds meta data
# Remove the item in the project, and re-run the consistency check
del theProject.tree[xHandle]
assert theProject.tree.checkConsistency("Recovered") == (1, 1)
assert xHandle in theProject.tree
itemX = theProject.tree[xHandle]
assert isinstance(itemX, NWItem)
# It should again be added as a Novel file
assert itemX.itemParent == C.hNovelRoot
assert itemX.itemRoot == C.hNovelRoot
assert itemX.itemClass == nwItemClass.NOVEL
assert itemX.itemName == "[Recovered] Stuff"
# If the tree is empty, there is nowhere to add any of the 4 files
theProject.tree.clear()
assert theProject.tree.checkConsistency("Recovered") == (4, 0)
assert len(theProject.tree) == 0
# END Test testCoreTree_CheckConsistency
@pytest.mark.core
def testCoreTree_Methods(mockGUI, mockItems):
"""Test various class methods."""