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 random
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Iterator, overload
|
from typing import TYPE_CHECKING, Iterator, Literal, overload
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
||||||
@@ -94,19 +94,21 @@ class NWTree:
|
|||||||
return self._treeOrder.copy()
|
return self._treeOrder.copy()
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def create(self, label: str, parent: None, itemType: nwItemType,
|
def create(self, label: str, parent: None, itemType: Literal[nwItemType.ROOT],
|
||||||
itemClass: nwItemClass = nwItemClass.NO_CLASS) -> str:
|
itemClass: nwItemClass) -> str: # pragma: no cover
|
||||||
...
|
pass
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def create(self, label: str, parent: str | None, itemType: nwItemType,
|
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):
|
def create(self, label, parent, itemType, itemClass=nwItemClass.NO_CLASS):
|
||||||
"""Create a new item in the project tree, and return its handle.
|
"""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:
|
if parent is None or parent in self._treeOrder:
|
||||||
tHandle = self._makeHandle()
|
tHandle = self._makeHandle()
|
||||||
newItem = NWItem(self._project, tHandle)
|
newItem = NWItem(self._project, tHandle)
|
||||||
@@ -238,6 +240,7 @@ class NWTree:
|
|||||||
newItem.setClass(oClass)
|
newItem.setClass(oClass)
|
||||||
newItem.setLayout(oLayout)
|
newItem.setLayout(oLayout)
|
||||||
if self.append(newItem):
|
if self.append(newItem):
|
||||||
|
self.updateItemData(cHandle)
|
||||||
recovered += 1
|
recovered += 1
|
||||||
|
|
||||||
return orphans, recovered
|
return orphans, recovered
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ from shutil import copyfile
|
|||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
from mocked import causeOSError
|
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 import CONFIG
|
||||||
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
|
from novelwriter.enum import nwItemClass
|
||||||
from novelwriter.constants import nwFiles
|
from novelwriter.constants import nwFiles
|
||||||
from novelwriter.core.tree import NWTree
|
from novelwriter.core.tree import NWTree
|
||||||
from novelwriter.core.index import NWIndex
|
from novelwriter.core.index import NWIndex
|
||||||
@@ -573,90 +573,6 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
|
|||||||
# END Test testCoreProject_Methods
|
# 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
|
@pytest.mark.core
|
||||||
def testCoreProject_Backup(monkeypatch, mockGUI, fncPath, tstPaths):
|
def testCoreProject_Backup(monkeypatch, mockGUI, fncPath, tstPaths):
|
||||||
"""Test the automated backup feature of the project class. The test
|
"""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.state == XMLReadState.PARSED_OK
|
||||||
assert xmlReader.xmlRoot == "novelWriterXML"
|
assert xmlReader.xmlRoot == "novelWriterXML"
|
||||||
assert xmlReader.xmlVersion == 0x0105
|
assert xmlReader.xmlVersion == 0x0105
|
||||||
|
assert xmlReader.xmlRevision == 1
|
||||||
assert xmlReader.appVersion == "2.0-rc1"
|
assert xmlReader.appVersion == "2.0-rc1"
|
||||||
assert xmlReader.hexVersion == 0x020000c1
|
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
|
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/>.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tools import C, buildTestProject
|
||||||
from mocked import causeOSError
|
from mocked import causeOSError
|
||||||
|
|
||||||
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
|
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
|
||||||
@@ -212,6 +214,14 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
|||||||
assert theTree.append(itemU) is False
|
assert theTree.append(itemU) is False
|
||||||
assert len(theTree) == len(mockItems) + 1
|
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
|
# Duplicate Items
|
||||||
# ===============
|
# ===============
|
||||||
|
|
||||||
@@ -224,7 +234,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
|||||||
assert len(theTree) == len(mockItems) + 2
|
assert len(theTree) == len(mockItems) + 2
|
||||||
|
|
||||||
dHandle = itemV.itemHandle
|
dHandle = itemV.itemHandle
|
||||||
assert dHandle == "0000000000001"
|
assert dHandle == "0000000000002"
|
||||||
|
|
||||||
# Delete Items
|
# Delete Items
|
||||||
# ============
|
# ============
|
||||||
@@ -285,6 +295,73 @@ def testCoreTree_PackUnpack(mockGUI, mockItems):
|
|||||||
# END Test testCoreTree_PackUnpack
|
# 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
|
@pytest.mark.core
|
||||||
def testCoreTree_Methods(mockGUI, mockItems):
|
def testCoreTree_Methods(mockGUI, mockItems):
|
||||||
"""Test various class methods."""
|
"""Test various class methods."""
|
||||||
|
|||||||
Reference in New Issue
Block a user