Add test coverage of the project tree consistency function
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user