Refactor Storage class (#1635)

This commit is contained in:
Veronica Berglyd Olsen
2023-12-01 18:22:26 +01:00
committed by GitHub
10 changed files with 714 additions and 585 deletions
+1 -1
View File
@@ -340,7 +340,7 @@ class ProjectBuilder:
return False return False
project = NWProject() project = NWProject()
if not project.storage.openProjectInPlace(projPath, newProject=True): if not project.storage.createNewProject(projPath):
return False return False
lblNewProject = self.tr("New Project") lblNewProject = self.tr("New Project")
+37 -36
View File
@@ -26,6 +26,7 @@ from __future__ import annotations
import json import json
import logging import logging
from enum import Enum
from time import time from time import time
from typing import TYPE_CHECKING, Iterator from typing import TYPE_CHECKING, Iterator
from pathlib import Path from pathlib import Path
@@ -40,7 +41,7 @@ from novelwriter.constants import trConst, nwLabels
from novelwriter.core.tree import NWTree from novelwriter.core.tree import NWTree
from novelwriter.core.index import NWIndex from novelwriter.core.index import NWIndex
from novelwriter.core.options import OptionState from novelwriter.core.options import OptionState
from novelwriter.core.storage import NWStorage from novelwriter.core.storage import NWStorage, NWStorageOpen
from novelwriter.core.sessions import NWSessionLog from novelwriter.core.sessions import NWSessionLog
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
from novelwriter.core.projectdata import NWProjectData from novelwriter.core.projectdata import NWProjectData
@@ -55,6 +56,16 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class NWProjectState(Enum):
UNKNOWN = 0
LOCKED = 1
RECOVERY = 2
READY = 3
# END Enum NWProjectState
class NWProject: class NWProject:
def __init__(self) -> None: def __init__(self) -> None:
@@ -69,9 +80,9 @@ class NWProject:
# Project Status # Project Status
self._langData = {} # Localisation data self._langData = {} # Localisation data
self._lockedBy = None # Data on which computer has the project open
self._changed = False # The project has unsaved changes self._changed = False # The project has unsaved changes
self._valid = False # The project was successfully loaded self._valid = False # The project was successfully loaded
self._state = NWProjectState.UNKNOWN
# Internal Mapping # Internal Mapping
self.tr = partial(QCoreApplication.translate, "NWProject") self.tr = partial(QCoreApplication.translate, "NWProject")
@@ -125,12 +136,15 @@ class NWProject:
"""Return True if a project is loaded.""" """Return True if a project is loaded."""
return self._valid return self._valid
@property
def state(self) -> NWProjectState:
"""Return the current project state."""
return self._state
@property @property
def lockStatus(self) -> list | None: def lockStatus(self) -> list | None:
"""Return the project lock information.""" """Return the project lock information."""
if isinstance(self._lockedBy, list) and len(self._lockedBy) == 4: return self._storage.lockStatus
return self._lockedBy
return None
@property @property
def currentEditTime(self) -> int: def currentEditTime(self) -> int:
@@ -219,29 +233,21 @@ class NWProject:
build the tree of project items. build the tree of project items.
""" """
logger.info("Opening project: %s", projPath) logger.info("Opening project: %s", projPath)
if not self._storage.openProjectInPlace(projPath):
SHARED.error(self.tr("Could not open project with path: {0}").format(projPath)) status = self._storage.initProjectStorage(projPath, clearLock)
if status != NWStorageOpen.READY:
if status == NWStorageOpen.UNKOWN:
SHARED.error(self.tr("Not a known project file format."))
elif status == NWStorageOpen.NOT_FOUND:
SHARED.error(self.tr("Project file not found."))
elif status == NWStorageOpen.LOCKED:
self._state = NWProjectState.LOCKED
elif status == NWStorageOpen.FAILED:
SHARED.error(self.tr("Failed to open project."), exc=self._storage.exc)
return False return False
# Project Lock # Read Project XML
# ============ # ================
if clearLock:
self._storage.clearLockFile()
lockStatus = self._storage.readLockFile()
if len(lockStatus) > 0:
if lockStatus[0] == "ERROR":
logger.warning("Failed to check lock file")
else:
logger.error("Project is locked, so not opening")
self._lockedBy = lockStatus
return False
else:
logger.debug("Project is not locked")
# Open The Project XML File
# =========================
xmlReader = self._storage.getXmlReader() xmlReader = self._storage.getXmlReader()
if not isinstance(xmlReader, ProjectXMLReader): if not isinstance(xmlReader, ProjectXMLReader):
@@ -250,9 +256,7 @@ class NWProject:
self._data = NWProjectData(self) self._data = NWProjectData(self)
projContent = [] projContent = []
xmlParsed = xmlReader.read(self._data, projContent) xmlParsed = xmlReader.read(self._data, projContent)
appVersion = xmlReader.appVersion or self.tr("Unknown") appVersion = xmlReader.appVersion or self.tr("Unknown")
if not xmlParsed: if not xmlParsed:
if xmlReader.state == XMLReadState.NOT_NWX_FILE: if xmlReader.state == XMLReadState.NOT_NWX_FILE:
SHARED.error(self.tr( SHARED.error(self.tr(
@@ -302,8 +306,7 @@ class NWProject:
self._loadProjectLocalisation() self._loadProjectLocalisation()
# Update recent projects # Update recent projects
storePath = self._storage.storagePath if storePath := self._storage.storagePath:
if storePath:
CONFIG.recentProjects.update( CONFIG.recentProjects.update(
storePath, self._data.name, sum(self._data.initCounts), time() storePath, self._data.name, sum(self._data.initCounts), time()
) )
@@ -323,9 +326,9 @@ class NWProject:
self.updateWordCounts() self.updateWordCounts()
self._session.startSession() self._session.startSession()
self._storage.writeLockFile()
self.setProjectChanged(False) self.setProjectChanged(False)
self._valid = True self._valid = True
self._state = NWProjectState.READY
SHARED.newStatusMessage(self.tr("Opened Project: {0}").format(self._data.name)) SHARED.newStatusMessage(self.tr("Opened Project: {0}").format(self._data.name))
@@ -370,13 +373,11 @@ class NWProject:
self._storage.runPostSaveTasks(autoSave=autoSave) self._storage.runPostSaveTasks(autoSave=autoSave)
# Update recent projects # Update recent projects
storePath = self._storage.storagePath if storagePath := self._storage.storagePath:
if storePath:
CONFIG.recentProjects.update( CONFIG.recentProjects.update(
storePath, self._data.name, sum(self._data.currCounts), saveTime storagePath, self._data.name, sum(self._data.currCounts), saveTime
) )
self._storage.writeLockFile()
SHARED.newStatusMessage(self.tr("Saved Project: {0}").format(self._data.name)) SHARED.newStatusMessage(self.tr("Saved Project: {0}").format(self._data.name))
self.setProjectChanged(False) self.setProjectChanged(False)
@@ -420,8 +421,8 @@ class NWProject:
timeStamp = formatTimeStamp(time(), fileSafe=True) timeStamp = formatTimeStamp(time(), fileSafe=True)
archName = baseDir / f"{cleanName} {timeStamp}.zip" archName = baseDir / f"{cleanName} {timeStamp}.zip"
if self._storage.zipIt(archName, compression=2): if self._storage.zipIt(archName, compression=2):
size = formatInt(getFileSize(archName))
if doNotify: if doNotify:
size = formatInt(getFileSize(archName))
SHARED.info( SHARED.info(
self.tr("Created a backup of your project of size {0}B.").format(size), self.tr("Created a backup of your project of size {0}B.").format(size),
info=self.tr("Path: {0}").format(str(backupPath)) info=self.tr("Path: {0}").format(str(backupPath))
+5 -7
View File
@@ -38,7 +38,6 @@ from novelwriter.common import (
checkBool, checkInt, checkString, checkStringNone, formatTimeStamp, checkBool, checkInt, checkString, checkStringNone, formatTimeStamp,
hexToInt, simplified, xmlIndent, yesNo hexToInt, simplified, xmlIndent, yesNo
) )
from novelwriter.constants import nwFiles
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from novelwriter.core.status import NWStatus from novelwriter.core.status import NWStatus
@@ -558,9 +557,8 @@ class ProjectXMLWriter:
xName.text = item["name"] xName.text = item["name"]
# Write the XML tree to file # Write the XML tree to file
saveFile = self._path / nwFiles.PROJ_FILE tempFile = self._path.with_suffix(".tmp")
tempFile = saveFile.with_suffix(".tmp") backFile = self._path.with_suffix(".bak")
backFile = saveFile.with_suffix(".bak")
try: try:
xml = ET.ElementTree(xRoot) xml = ET.ElementTree(xRoot)
xmlIndent(xml) xmlIndent(xml)
@@ -572,9 +570,9 @@ class ProjectXMLWriter:
# If we're here, the file was successfully saved, # If we're here, the file was successfully saved,
# so let's sort out the temps and backups # so let's sort out the temps and backups
try: try:
if saveFile.exists(): if self._path.exists():
saveFile.replace(backFile) self._path.replace(backFile)
tempFile.replace(saveFile) tempFile.replace(self._path)
except Exception as exc: except Exception as exc:
self._error = exc self._error = exc
return False return False
+166 -137
View File
@@ -26,6 +26,7 @@ from __future__ import annotations
import json import json
import logging import logging
from enum import Enum
from time import time from time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
@@ -45,6 +46,17 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class NWStorageOpen(Enum):
UNKOWN = 0
NOT_FOUND = 1
LOCKED = 2
FAILED = 3
READY = 4
# END Enum NWStorageOpen
class NWStorage: class NWStorage:
"""Core: Project Storage Class """Core: Project Storage Class
@@ -60,7 +72,10 @@ class NWStorage:
self._storagePath = None self._storagePath = None
self._runtimePath = None self._runtimePath = None
self._lockFilePath = None self._lockFilePath = None
self._lockedBy = None
self._openMode = self.MODE_INACTIVE self._openMode = self.MODE_INACTIVE
self._ready = False
self._exception = None
return return
def clear(self) -> None: def clear(self) -> None:
@@ -69,6 +84,7 @@ class NWStorage:
self._runtimePath = None self._runtimePath = None
self._lockFilePath = None self._lockFilePath = None
self._openMode = self.MODE_INACTIVE self._openMode = self.MODE_INACTIVE
self._ready = False
return return
## ##
@@ -77,12 +93,12 @@ class NWStorage:
@property @property
def storagePath(self) -> Path | None: def storagePath(self) -> Path | None:
"""Get the path where the project is saved.""" """Return the path where the project is stored."""
return self._storagePath return self._storagePath
@property @property
def runtimePath(self) -> Path | None: def runtimePath(self) -> Path | None:
"""Get the path where the project is saved at runtime.""" """Return the path where the project is stored at runtime."""
return self._runtimePath return self._runtimePath
@property @property
@@ -97,26 +113,31 @@ class NWStorage:
logger.error("Content path cannot be resolved") logger.error("Content path cannot be resolved")
return None return None
@property
def lockStatus(self) -> list | None:
"""Return the project lock information."""
if isinstance(self._lockedBy, list) and len(self._lockedBy) == 4:
return self._lockedBy
return None
@property
def exc(self) -> Exception | None:
"""Return the latest exception of the storage instance."""
return self._exception
## ##
# Core Methods # Core Methods
## ##
def isOpen(self) -> bool: def isOpen(self) -> bool:
"""Check if the storage location is open.""" """Check if the storage location is open."""
return self._runtimePath is not None return self._ready and self._runtimePath is not None
def openProjectInPlace(self, path: str | Path, newProject: bool = False) -> bool: def createNewProject(self, path: str | Path) -> bool:
"""Open a novelWriter project in-place. That is, it is opened """Create a new project at the given location."""
directly from a project folder.
"""
inPath = Path(path).resolve() inPath = Path(path).resolve()
if inPath.is_file(): if inPath.is_dir() and len(list(inPath.iterdir())) > 0:
# The path should not point to an existing file, logger.error("Folder is not empty: %s", inPath)
# but it can point to a folder containing files
inPath = inPath.parent
if not (inPath.is_dir() or newProject):
# If the project is not new, the folder must already exist.
return False return False
self._storagePath = inPath self._storagePath = inPath
@@ -124,17 +145,91 @@ class NWStorage:
self._lockFilePath = inPath / nwFiles.PROJ_LOCK self._lockFilePath = inPath / nwFiles.PROJ_LOCK
self._openMode = self.MODE_INPLACE self._openMode = self.MODE_INPLACE
if not self._prepareStorage(checkLegacy=True, newProject=newProject): basePath = self._runtimePath
metaPath = basePath / "meta"
contPath = basePath / "content"
try:
basePath.mkdir(exist_ok=True)
metaPath.mkdir(exist_ok=True)
contPath.mkdir(exist_ok=True)
except Exception as exc:
self._exception = exc
logger.error("Failed to create project folders", exc_info=exc)
self.clear() self.clear()
return False return False
self._ready = True
return True return True
def openProjectArchive(self, path: str | Path) -> bool: # pragma: no cover def initProjectStorage(self, path: str | Path, clearLock: bool = False) -> NWStorageOpen:
"""Open the project from a single file. """Initialise a novelWriter project location."""
Placeholder for later implementation. See #977. inPath = Path(path).resolve()
"""
return False # Initialise Storage Instance
# ===========================
# Check what we're opening. Only two options are allowed:
# 1. A folder with an nwProject.nwx file in it (not home)
# 2. A full path to an nwProject.nwx file
if inPath.is_dir() and inPath != Path.home().resolve():
nwxFile = inPath / nwFiles.PROJ_FILE
elif inPath.is_file() and inPath.name == nwFiles.PROJ_FILE:
nwxFile = inPath
else:
logger.error("Not a novelWriter project")
return NWStorageOpen.UNKOWN
if not nwxFile.exists():
# The .nwx file must exist to continue
logger.error("Not found: %s", nwxFile)
return NWStorageOpen.NOT_FOUND
nwxPath = nwxFile.parent
self._storagePath = nwxPath
self._runtimePath = nwxPath
self._lockFilePath = nwxPath / nwFiles.PROJ_LOCK
self._openMode = self.MODE_INPLACE
# Check Project Lock
# ==================
if clearLock:
self._clearLockFile()
self._readLockFile()
if self._lockedBy:
logger.error("Project is locked, so not opening")
return NWStorageOpen.LOCKED
else:
logger.debug("Project is not locked")
# Prepare Folder
# ==============
basePath = self._runtimePath
metaPath = basePath / "meta"
contPath = basePath / "content"
try:
metaPath.mkdir(exist_ok=True)
contPath.mkdir(exist_ok=True)
except Exception as exc:
logger.error("Failed to create project folders", exc_info=exc)
self.clear()
return NWStorageOpen.FAILED
# Check for legacy data folders
legacy = _LegacyStorage(self._project)
legacy.deprecatedFiles(basePath)
for child in basePath.iterdir():
if child.is_dir() and child.name.startswith("data_"):
legacy.legacyDataFolder(basePath, child)
self._writeLockFile()
self._ready = True
return NWStorageOpen.READY
def runPostSaveTasks(self, autoSave: bool = False) -> bool: # pragma: no cover def runPostSaveTasks(self, autoSave: bool = False) -> bool: # pragma: no cover
"""Run tasks after the project has been saved. """Run tasks after the project has been saved.
@@ -147,7 +242,7 @@ class NWStorage:
def closeSession(self) -> None: def closeSession(self) -> None:
"""Run tasks related to closing the session.""" """Run tasks related to closing the session."""
self.clearLockFile() self._clearLockFile()
self.clear() self.clear()
return return
@@ -157,26 +252,25 @@ class NWStorage:
def getXmlReader(self) -> ProjectXMLReader | None: def getXmlReader(self) -> ProjectXMLReader | None:
"""Return a properly configured ProjectXMLReader instance.""" """Return a properly configured ProjectXMLReader instance."""
if isinstance(self._runtimePath, Path): if isinstance(self._runtimePath, Path) and self._ready:
projFile = self._runtimePath / nwFiles.PROJ_FILE return ProjectXMLReader(self._runtimePath / nwFiles.PROJ_FILE)
return ProjectXMLReader(projFile)
return None return None
def getXmlWriter(self) -> ProjectXMLWriter | None: def getXmlWriter(self) -> ProjectXMLWriter | None:
"""Return a properly configured ProjectXMLWriter instance.""" """Return a properly configured ProjectXMLWriter instance."""
if isinstance(self._runtimePath, Path): if isinstance(self._runtimePath, Path) and self._ready:
return ProjectXMLWriter(self._runtimePath) return ProjectXMLWriter(self._runtimePath / nwFiles.PROJ_FILE)
return None return None
def getDocument(self, tHandle: str | None) -> NWDocument: def getDocument(self, tHandle: str | None) -> NWDocument:
"""Return a document wrapper object.""" """Return a document wrapper object."""
if isinstance(self._runtimePath, Path): if isinstance(self._runtimePath, Path) and self._ready:
return NWDocument(self._project, tHandle) return NWDocument(self._project, tHandle)
return NWDocument(self._project, None) return NWDocument(self._project, None)
def getMetaFile(self, fileName: str) -> Path | None: def getMetaFile(self, fileName: str) -> Path | None:
"""Return the path to a file in the project meta folder.""" """Return the path to a file in the project meta folder."""
if isinstance(self._runtimePath, Path): if isinstance(self._runtimePath, Path) and self._ready:
return self._runtimePath / "meta" / fileName return self._runtimePath / "meta" / fileName
return None return None
@@ -190,59 +284,6 @@ class NWStorage:
if item.suffix == ".nwd" and isHandle(item.stem) if item.suffix == ".nwd" and isHandle(item.stem)
] if contentPath else [] ] if contentPath else []
def readLockFile(self) -> list[str]:
"""Read the project lock file."""
if self._lockFilePath is None:
return ["ERROR"]
if not self._lockFilePath.exists():
return []
try:
lines = self._lockFilePath.read_text(encoding="utf-8").strip().split(";")
except Exception:
logger.error("Failed to read project lockfile")
logException()
return ["ERROR"]
if len(lines) != 4:
return ["ERROR"]
return lines
def writeLockFile(self) -> bool:
"""Write the project lock file."""
if self._lockFilePath is None:
return False
data = [
CONFIG.hostName, CONFIG.osType,
CONFIG.kernelVer, str(int(time()))
]
try:
self._lockFilePath.write_text(";".join(data), encoding="utf-8")
except Exception:
logger.error("Failed to write project lockfile")
logException()
return False
return True
def clearLockFile(self) -> bool:
"""Remove the lock file, if it exists."""
if self._lockFilePath is None:
return False
if self._lockFilePath.exists():
try:
self._lockFilePath.unlink()
except Exception:
logger.error("Failed to remove project lockfile")
logException()
return False
return True
def zipIt(self, target: str | Path, compression: int | None = None) -> bool: def zipIt(self, target: str | Path, compression: int | None = None) -> bool:
"""Zip the content of the project at its runtime location into a """Zip the content of the project at its runtime location into a
zip file. This process will only grab files that are supposed to zip file. This process will only grab files that are supposed to
@@ -279,7 +320,7 @@ class NWStorage:
zipObj.write(srcPath, zipPath) zipObj.write(srcPath, zipPath)
logger.debug("Added: %s", zipPath) logger.debug("Added: %s", zipPath)
except Exception: except Exception:
logger.error("Failed to create acrhive") logger.error("Failed to create archive")
logException() logException()
return False return False
@@ -289,59 +330,48 @@ class NWStorage:
# Internal Functions # Internal Functions
## ##
def _prepareStorage(self, checkLegacy: bool = True, newProject: bool = False) -> bool: def _readLockFile(self) -> None:
"""Prepare the storage area for the project.""" """Read the project lock file."""
path = self._runtimePath self._lockedBy = None
if not isinstance(path, Path): path = self._lockFilePath
logger.error("No path set") if isinstance(path, Path) and path.exists():
self.clear() try:
self._lockedBy = path.read_text(encoding="utf-8").strip().split(";")
except Exception:
logger.error("Failed to read project lockfile")
logException()
self._lockedBy = ["ERROR", "ERROR", "ERROR", "ERROR"]
return
return
def _writeLockFile(self) -> bool:
"""Write the project lock file."""
if self._lockFilePath is None:
return False return False
if path == Path.home().absolute():
logger.error("Cannot use the user's home path as the root of a project")
self.clear()
return False
if newProject:
# If it's a new project, we check that there is no existing
# project in the selected path.
if path.exists() and len(list(path.iterdir())) > 0:
logger.error("The new project folder is not empty")
self.clear()
return False
# The folder is not required to exist, as it could be a new
# project, so we make sure it does. Then we add subfolders.
try: try:
path.mkdir(exist_ok=True) self._lockFilePath.write_text(
(path / "content").mkdir(exist_ok=True) f"{CONFIG.hostName};{CONFIG.osType};{CONFIG.kernelVer};{int(time())}",
(path / "meta").mkdir(exist_ok=True) encoding="utf-8"
except Exception as exc: )
logger.error("Failed to create required project folders", exc_info=exc) except Exception:
self.clear() logger.error("Failed to write project lockfile")
logException()
return False return False
if not checkLegacy:
# The legacy content check is only needed for project folder
# storage, so if it is not expected to be that, there's no
# need for the remaining checks.
return True
legacy = _LegacyStorage(self._project)
# Check for legacy data folders
for child in path.iterdir():
if child.is_dir() and child.name.startswith("data_"):
legacy.legacyDataFolder(path, child)
# Check for no longer used files, and delete them
legacy.deprecatedFiles(path)
return True return True
## def _clearLockFile(self) -> bool:
# Legacy Project Data Handlers """Remove the lock file, if it exists."""
## if self._lockFilePath is None:
return False
if self._lockFilePath.exists():
try:
self._lockFilePath.unlink()
except Exception:
logger.error("Failed to remove project lockfile")
logException()
return False
self._lockedBy = None
return True
# END Class NWStorage # END Class NWStorage
@@ -350,7 +380,7 @@ class _LegacyStorage:
"""Core: Legacy Storage Converter Utils """Core: Legacy Storage Converter Utils
A class with various functions to convert old file formats and A class with various functions to convert old file formats and
file/folder layout to the current project format. file/folder layouts to the current project format.
""" """
def __init__(self, project: NWProject) -> None: def __init__(self, project: NWProject) -> None:
@@ -359,7 +389,8 @@ class _LegacyStorage:
def legacyDataFolder(self, path: Path, child: Path) -> None: def legacyDataFolder(self, path: Path, child: Path) -> None:
"""Handle the content of a legacy data folder from a version 1.0 """Handle the content of a legacy data folder from a version 1.0
project. project. This format had 16 data folders where there now is only
one content folder.
""" """
logger.info("Processing legacy data folder: %s", path) logger.info("Processing legacy data folder: %s", path)
@@ -411,6 +442,7 @@ class _LegacyStorage:
path / "meta" / nwFiles.OPTS_FILE path / "meta" / nwFiles.OPTS_FILE
) )
# Delete removed files
remove = [ remove = [
path / "meta" / "tagsIndex.json", # Renamed in 2.1 Beta 1 path / "meta" / "tagsIndex.json", # Renamed in 2.1 Beta 1
path / "meta" / "mainOptions.json", # Replaced in 0.5 path / "meta" / "mainOptions.json", # Replaced in 0.5
@@ -457,7 +489,6 @@ class _LegacyStorage:
# Save dictionary and clean up old file # Save dictionary and clean up old file
userDict.save() userDict.save()
assert wordJson.exists()
wordList.unlink() wordList.unlink()
except Exception: except Exception:
@@ -467,9 +498,7 @@ class _LegacyStorage:
return return
def _convertOldLogFile(self, sessLog: Path, sessJson: Path) -> None: def _convertOldLogFile(self, sessLog: Path, sessJson: Path) -> None:
"""Convert the old text log file format to the new JSON Lines """Convert the old text log file format to JSON Lines."""
format.
"""
if sessJson.exists() or not sessLog.exists(): if sessJson.exists() or not sessLog.exists():
# If the new file already exists, we won't overwrite it # If the new file already exists, we won't overwrite it
return return
@@ -508,7 +537,7 @@ class _LegacyStorage:
return return
def _convertOldOptionsFile(self, optsOld: Path, optsNew: Path) -> None: def _convertOldOptionsFile(self, optsOld: Path, optsNew: Path) -> None:
"""Convert the old options state file format to the format.""" """Convert the old options state file format to new format."""
if optsNew.exists() or not optsOld.exists(): if optsNew.exists() or not optsOld.exists():
# If the new file already exists, we won't overwrite it # If the new file already exists, we won't overwrite it
return return
+1 -2
View File
@@ -462,8 +462,7 @@ class GuiMain(QMainWindow):
if projFile is None: if projFile is None:
return False return False
# Make sure any open project is cleared out first before we load # Make sure any open project is cleared out first
# another one
if not self.closeProject(): if not self.closeProject():
return False return False
+44 -43
View File
@@ -34,8 +34,8 @@ from novelwriter.gui.noveltree import NovelTreeColumn
@pytest.mark.core @pytest.mark.core
def testCoreOptions_LoadSave(monkeypatch, mockGUI, fncPath): def testCoreOptions_LoadSave(monkeypatch, mockGUI, fncPath):
"""Test loading and saving from the OptionState class.""" """Test loading and saving from the OptionState class."""
theProject = NWProject() project = NWProject()
theOpts = OptionState(theProject) options = OptionState(project)
metaDir = fncPath / "meta" metaDir = fncPath / "meta"
metaDir.mkdir() metaDir.mkdir()
@@ -58,25 +58,26 @@ def testCoreOptions_LoadSave(monkeypatch, mockGUI, fncPath):
}), encoding="utf-8") }), encoding="utf-8")
# Load and save with no path set # Load and save with no path set
theProject.storage._runtimePath = None project.storage._runtimePath = None
assert theOpts.loadSettings() is False assert options.loadSettings() is False
assert theOpts.saveSettings() is False assert options.saveSettings() is False
# Set path # Set path
theProject.storage._runtimePath = fncPath project.storage._runtimePath = fncPath
assert theProject.storage.getMetaFile(nwFiles.OPTS_FILE) == optFile project.storage._ready = True
assert project.storage.getMetaFile(nwFiles.OPTS_FILE) == optFile
# Cause open() to fail # Cause open() to fail
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError) mp.setattr("builtins.open", causeOSError)
assert theOpts.loadSettings() is False assert options.loadSettings() is False
assert theOpts.saveSettings() is False assert options.saveSettings() is False
# Load proper # Load proper
assert theOpts.loadSettings() assert options.loadSettings()
# Check that unwanted items have been removed # Check that unwanted items have been removed
assert theOpts._state == { assert options._state == {
"GuiProjectSettings": { "GuiProjectSettings": {
"winWidth": 570, "winWidth": 570,
"winHeight": 375, "winHeight": 375,
@@ -87,11 +88,11 @@ def testCoreOptions_LoadSave(monkeypatch, mockGUI, fncPath):
} }
# Save proper # Save proper
assert theOpts.saveSettings() assert options.saveSettings()
# Load again to check we get the values back # Load again to check we get the values back
assert theOpts.loadSettings() assert options.loadSettings()
assert theOpts._state == { assert options._state == {
"GuiProjectSettings": { "GuiProjectSettings": {
"winWidth": 570, "winWidth": 570,
"winHeight": 375, "winHeight": 375,
@@ -107,48 +108,48 @@ def testCoreOptions_LoadSave(monkeypatch, mockGUI, fncPath):
@pytest.mark.core @pytest.mark.core
def testCoreOptions_SetGet(mockGUI): def testCoreOptions_SetGet(mockGUI):
"""Test setting and getting values from the OptionState class.""" """Test setting and getting values from the OptionState class."""
theProject = NWProject() project = NWProject()
theOpts = OptionState(theProject) options = OptionState(project)
nwColHidden = NovelTreeColumn.HIDDEN nwColHidden = NovelTreeColumn.HIDDEN
# Set invalid values # Set invalid values
assert theOpts.setValue("MockGroup", "mockItem", None) is False assert options.setValue("MockGroup", "mockItem", None) is False
assert theOpts.setValue("GuiProjectSettings", "mockItem", None) is False assert options.setValue("GuiProjectSettings", "mockItem", None) is False
# Set valid value # Set valid value
assert theOpts.setValue("GuiProjectSettings", "winWidth", 100) is True assert options.setValue("GuiProjectSettings", "winWidth", 100) is True
# Set some values of different types # Set some values of different types
assert theOpts.setValue("GuiProjectDetails", "winWidth", 100) is True assert options.setValue("GuiProjectDetails", "winWidth", 100) is True
assert theOpts.setValue("GuiProjectDetails", "winHeight", 12.34) is True assert options.setValue("GuiProjectDetails", "winHeight", 12.34) is True
assert theOpts.setValue("GuiProjectDetails", "clearDouble", True) is True assert options.setValue("GuiProjectDetails", "clearDouble", True) is True
assert theOpts.setValue("GuiNovelView", "lastCol", nwColHidden) is True assert options.setValue("GuiNovelView", "lastCol", nwColHidden) is True
# Generic get, doesn't check type # Generic get, doesn't check type
assert theOpts.getValue("GuiProjectDetails", "winWidth", None) == 100 assert options.getValue("GuiProjectDetails", "winWidth", None) == 100
assert theOpts.getValue("GuiProjectDetails", "winHeight", None) == 12.34 assert options.getValue("GuiProjectDetails", "winHeight", None) == 12.34
assert theOpts.getValue("GuiProjectDetails", "clearDouble", None) is True assert options.getValue("GuiProjectDetails", "clearDouble", None) is True
assert theOpts.getValue("GuiProjectDetails", "mockItem", None) is None assert options.getValue("GuiProjectDetails", "mockItem", None) is None
# Get type-specific # Get type-specific
assert theOpts.getString("GuiProjectDetails", "winWidth", None) is None assert options.getString("GuiProjectDetails", "winWidth", None) is None # type: ignore
assert theOpts.getString("GuiProjectDetails", "mockItem", None) is None assert options.getString("GuiProjectDetails", "mockItem", None) is None # type: ignore
assert theOpts.getInt("GuiProjectDetails", "winWidth", None) == 100 assert options.getInt("GuiProjectDetails", "winWidth", None) == 100 # type: ignore
assert theOpts.getInt("GuiProjectDetails", "textFont", None) is None assert options.getInt("GuiProjectDetails", "textFont", None) is None # type: ignore
assert theOpts.getInt("GuiProjectDetails", "mockItem", None) is None assert options.getInt("GuiProjectDetails", "mockItem", None) is None # type: ignore
assert theOpts.getFloat("GuiProjectDetails", "winWidth", None) == 100.0 assert options.getFloat("GuiProjectDetails", "winWidth", None) == 100.0 # type: ignore
assert theOpts.getFloat("GuiProjectDetails", "mockItem", None) is None assert options.getFloat("GuiProjectDetails", "mockItem", None) is None # type: ignore
assert theOpts.getBool("GuiProjectDetails", "clearDouble", None) is True assert options.getBool("GuiProjectDetails", "clearDouble", None) is True # type: ignore
assert theOpts.getBool("GuiProjectDetails", "mockItem", None) is None assert options.getBool("GuiProjectDetails", "mockItem", None) is None # type: ignore
assert theOpts.getEnum("GuiNovelView", "lastCol", NovelTreeColumn, nwColHidden) == nwColHidden assert options.getEnum("GuiNovelView", "lastCol", NovelTreeColumn, nwColHidden) == nwColHidden
# Get from non-existent groups # Get from non-existent groups
assert theOpts.getValue("SomeGroup", "mockItem", None) is None assert options.getValue("SomeGroup", "mockItem", None) is None
assert theOpts.getString("SomeGroup", "mockItem", None) is None assert options.getString("SomeGroup", "mockItem", None) is None # type: ignore
assert theOpts.getInt("SomeGroup", "mockItem", None) is None assert options.getInt("SomeGroup", "mockItem", None) is None # type: ignore
assert theOpts.getFloat("SomeGroup", "mockItem", None) is None assert options.getFloat("SomeGroup", "mockItem", None) is None # type: ignore
assert theOpts.getBool("SomeGroup", "mockItem", None) is None assert options.getBool("SomeGroup", "mockItem", None) is None # type: ignore
assert theOpts.getEnum("SomeGroup", "mockItem", NovelTreeColumn, None) is None assert options.getEnum("SomeGroup", "mockItem", NovelTreeColumn, None) is None # type: ignore
# END Test testCoreOptions_SetGet # END Test testCoreOptions_SetGet
+258 -245
View File
@@ -33,9 +33,10 @@ from PyQt5.QtWidgets import QMessageBox
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwItemClass from novelwriter.enum import nwItemClass
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.item import NWItem
from novelwriter.core.tree import NWTree from novelwriter.core.tree import NWTree
from novelwriter.core.index import NWIndex from novelwriter.core.index import NWIndex
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject, NWProjectState
from novelwriter.core.options import OptionState from novelwriter.core.options import OptionState
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
@@ -47,45 +48,45 @@ def testCoreProject_NewRoot(fncPath, tstPaths, mockGUI, mockRnd):
testFile = tstPaths.outDir / "coreProject_NewRoot_nwProject.nwx" testFile = tstPaths.outDir / "coreProject_NewRoot_nwProject.nwx"
compFile = tstPaths.refDir / "coreProject_NewRoot_nwProject.nwx" compFile = tstPaths.refDir / "coreProject_NewRoot_nwProject.nwx"
theProject = NWProject() project = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(project, fncPath)
assert theProject.newRoot(nwItemClass.NOVEL) == "0000000000010" assert project.newRoot(nwItemClass.NOVEL) == "0000000000010"
assert theProject.newRoot(nwItemClass.PLOT) == "0000000000011" assert project.newRoot(nwItemClass.PLOT) == "0000000000011"
assert theProject.newRoot(nwItemClass.CHARACTER) == "0000000000012" assert project.newRoot(nwItemClass.CHARACTER) == "0000000000012"
assert theProject.newRoot(nwItemClass.WORLD) == "0000000000013" assert project.newRoot(nwItemClass.WORLD) == "0000000000013"
assert theProject.newRoot(nwItemClass.TIMELINE) == "0000000000014" assert project.newRoot(nwItemClass.TIMELINE) == "0000000000014"
assert theProject.newRoot(nwItemClass.OBJECT) == "0000000000015" assert project.newRoot(nwItemClass.OBJECT) == "0000000000015"
assert theProject.newRoot(nwItemClass.CUSTOM) == "0000000000016" assert project.newRoot(nwItemClass.CUSTOM) == "0000000000016"
assert theProject.newRoot(nwItemClass.CUSTOM) == "0000000000017" assert project.newRoot(nwItemClass.CUSTOM) == "0000000000017"
assert theProject.projChanged is True assert project.projChanged is True
assert theProject.saveProject() is True assert project.saveProject() is True
theProject.closeProject() project.closeProject()
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
assert theProject.projChanged is False assert project.projChanged is False
# Delete the new items # Delete the new items
assert theProject.removeItem("0000000000010") is True assert project.removeItem("0000000000010") is True
assert theProject.removeItem("0000000000011") is True assert project.removeItem("0000000000011") is True
assert theProject.removeItem("0000000000012") is True assert project.removeItem("0000000000012") is True
assert theProject.removeItem("0000000000013") is True assert project.removeItem("0000000000013") is True
assert theProject.removeItem("0000000000014") is True assert project.removeItem("0000000000014") is True
assert theProject.removeItem("0000000000015") is True assert project.removeItem("0000000000015") is True
assert theProject.removeItem("0000000000016") is True assert project.removeItem("0000000000016") is True
assert theProject.removeItem("0000000000017") is True assert project.removeItem("0000000000017") is True
assert "0000000000010" not in theProject.tree assert "0000000000010" not in project.tree
assert "0000000000011" not in theProject.tree assert "0000000000011" not in project.tree
assert "0000000000012" not in theProject.tree assert "0000000000012" not in project.tree
assert "0000000000013" not in theProject.tree assert "0000000000013" not in project.tree
assert "0000000000014" not in theProject.tree assert "0000000000014" not in project.tree
assert "0000000000015" not in theProject.tree assert "0000000000015" not in project.tree
assert "0000000000016" not in theProject.tree assert "0000000000016" not in project.tree
assert "0000000000017" not in theProject.tree assert "0000000000017" not in project.tree
# END Test testCoreProject_NewRoot # END Test testCoreProject_NewRoot
@@ -97,68 +98,68 @@ def testCoreProject_NewFileFolder(monkeypatch, fncPath, tstPaths, mockGUI, mockR
testFile = tstPaths.outDir / "coreProject_NewFileFolder_nwProject.nwx" testFile = tstPaths.outDir / "coreProject_NewFileFolder_nwProject.nwx"
compFile = tstPaths.refDir / "coreProject_NewFileFolder_nwProject.nwx" compFile = tstPaths.refDir / "coreProject_NewFileFolder_nwProject.nwx"
theProject = NWProject() project = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(project, fncPath)
# Invalid call # Invalid call
assert theProject.newFolder("New Folder", "1234567890abc") is None assert project.newFolder("New Folder", "1234567890abc") is None
assert theProject.newFile("New File", "1234567890abc") is None assert project.newFile("New File", "1234567890abc") is None
# Add files properly # Add files properly
assert theProject.newFolder("Stuff", C.hNovelRoot) == "0000000000010" assert project.newFolder("Stuff", C.hNovelRoot) == "0000000000010"
assert theProject.newFile("Hello", "0000000000010") == "0000000000011" assert project.newFile("Hello", "0000000000010") == "0000000000011"
assert theProject.newFile("Jane", C.hCharRoot) == "0000000000012" assert project.newFile("Jane", C.hCharRoot) == "0000000000012"
assert "0000000000010" in theProject.tree assert "0000000000010" in project.tree
assert "0000000000011" in theProject.tree assert "0000000000011" in project.tree
assert "0000000000012" in theProject.tree assert "0000000000012" in project.tree
# Write to file, failed # Write to file, failed
assert theProject.writeNewFile("blabla", 1, True) is False # Not a handle assert project.writeNewFile("blabla", 1, True) is False # Not a handle
assert theProject.writeNewFile("0000000000010", 1, True) is False # Not a file assert project.writeNewFile("0000000000010", 1, True) is False # Not a file
assert theProject.writeNewFile(C.hTitlePage, 1, True) is False # Already has content assert project.writeNewFile(C.hTitlePage, 1, True) is False # Already has content
# Write to file, success # Write to file, success
assert theProject.writeNewFile("0000000000011", 2, True) is True assert project.writeNewFile("0000000000011", 2, True) is True
assert theProject.storage.getDocument("0000000000011").readDocument() == "## Hello\n\n" assert project.storage.getDocument("0000000000011").readDocument() == "## Hello\n\n"
# Write to file with additional text, success # Write to file with additional text, success
assert theProject.writeNewFile("0000000000012", 1, False, "Hi Jane\n\n") is True assert project.writeNewFile("0000000000012", 1, False, "Hi Jane\n\n") is True
assert theProject.storage.getDocument("0000000000012").readDocument() == ( assert project.storage.getDocument("0000000000012").readDocument() == (
"# Jane\n\nHi Jane\n\n" "# Jane\n\nHi Jane\n\n"
) )
# Save, close and check # Save, close and check
assert theProject.projChanged is True assert project.projChanged is True
assert theProject.saveProject() is True assert project.saveProject() is True
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
assert theProject.projChanged is False assert project.projChanged is False
# Delete new file, but block access # Delete new file, but block access
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("pathlib.Path.unlink", causeOSError) mp.setattr("pathlib.Path.unlink", causeOSError)
assert theProject.removeItem("0000000000011") is False assert project.removeItem("0000000000011") is False
assert "0000000000011" in theProject.tree assert "0000000000011" in project.tree
# Delete new files and folders # Delete new files and folders
assert (fncPath / "content" / "0000000000012.nwd").exists() assert (fncPath / "content" / "0000000000012.nwd").exists()
assert (fncPath / "content" / "0000000000011.nwd").exists() assert (fncPath / "content" / "0000000000011.nwd").exists()
assert theProject.removeItem("0000000000012") is True assert project.removeItem("0000000000012") is True
assert theProject.removeItem("0000000000011") is True assert project.removeItem("0000000000011") is True
assert theProject.removeItem("0000000000010") is True assert project.removeItem("0000000000010") is True
assert not (fncPath / "content" / "0000000000012.nwd").exists() assert not (fncPath / "content" / "0000000000012.nwd").exists()
assert not (fncPath / "content" / "0000000000011.nwd").exists() assert not (fncPath / "content" / "0000000000011.nwd").exists()
assert "0000000000010" not in theProject.tree assert "0000000000010" not in project.tree
assert "0000000000011" not in theProject.tree assert "0000000000011" not in project.tree
assert "0000000000012" not in theProject.tree assert "0000000000012" not in project.tree
theProject.closeProject() project.closeProject()
# END Test testCoreProject_NewFileFolder # END Test testCoreProject_NewFileFolder
@@ -166,93 +167,103 @@ def testCoreProject_NewFileFolder(monkeypatch, fncPath, tstPaths, mockGUI, mockR
@pytest.mark.core @pytest.mark.core
def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd): def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
"""Test opening a project.""" """Test opening a project."""
theProject = NWProject() project = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) # buildTestProject(project, fncPath)
# Initialising the storage class fails # Unknown project file
with monkeypatch.context() as mp: fooBar = fncPath / "foobar.txt"
mp.setattr("novelwriter.core.storage.NWStorage.openProjectInPlace", lambda *a, **k: False) fooBar.touch()
assert theProject.openProject(fncPath) is False caplog.clear()
assert project.openProject(fooBar) is False
assert "Not a known project file format." in caplog.text
# Fail on lock file # No project file
theProject.storage._lockFilePath = fncPath / nwFiles.PROJ_LOCK caplog.clear()
assert theProject.storage.writeLockFile() is True assert project.openProject(fncPath) is False
assert theProject.openProject(fncPath) is False assert "Project file not found." in caplog.text
assert isinstance(theProject.lockStatus, list) fooBar.unlink()
# Fail to read lockfile (which still opens the project) # Fail to open project location
with monkeypatch.context() as mp: projFile = fncPath / nwFiles.PROJ_FILE
mp.setattr("novelwriter.core.storage.NWStorage.readLockFile", lambda *a: ["ERROR"]) metaFile = fncPath / "meta"
caplog.clear() projFile.touch()
assert theProject.openProject(fncPath) is True metaFile.touch()
assert "Failed to check lock file" in caplog.text caplog.clear()
theProject.closeProject() assert project.openProject(fncPath) is False
assert "Failed to open project." in caplog.text
metaFile.unlink()
projFile.unlink()
# Force open with lockfile # Create test project
theProject.storage._lockFilePath = fncPath / nwFiles.PROJ_LOCK buildTestProject(project, fncPath)
assert theProject.storage.writeLockFile() is True
assert theProject.openProject(fncPath, clearLock=True) is True # Open successfully
theProject.closeProject() assert project.openProject(fncPath) is True
assert theProject.lockStatus is None
# Open again should fail on lock file
assert project.openProject(fncPath) is False
assert project.state == NWProjectState.LOCKED
# Force re-open
assert project.openProject(fncPath, clearLock=True) is True
assert project.state == NWProjectState.READY
# Fail getting xml reader # Fail getting xml reader
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.storage.NWStorage.getXmlReader", lambda *a: None) mp.setattr("novelwriter.core.storage.NWStorage.getXmlReader", lambda *a: None)
assert theProject.openProject(fncPath) is False assert project.openProject(fncPath, clearLock=True) is False
# Not a novelwriter XML file # Not a novelwriter XML file
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "read", lambda *a: False) mp.setattr(ProjectXMLReader, "read", lambda *a: False)
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.NOT_NWX_FILE)) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.NOT_NWX_FILE))
assert theProject.openProject(fncPath) is False assert project.openProject(fncPath, clearLock=True) is False
assert "Project file does not appear" in SHARED.lastAlert assert "Project file does not appear" in SHARED.lastAlert
# Unknown project file version # Unknown project file version
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "read", lambda *a: False) mp.setattr(ProjectXMLReader, "read", lambda *a: False)
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.UNKNOWN_VERSION)) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.UNKNOWN_VERSION))
assert theProject.openProject(fncPath) is False assert project.openProject(fncPath, clearLock=True) is False
assert "Unknown or unsupported novelWriter project file" in SHARED.lastAlert assert "Unknown or unsupported novelWriter project file" in SHARED.lastAlert
# Other parse error # Other parse error
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "read", lambda *a: False) mp.setattr(ProjectXMLReader, "read", lambda *a: False)
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.CANNOT_PARSE)) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.CANNOT_PARSE))
assert theProject.openProject(fncPath) is False assert project.openProject(fncPath, clearLock=True) is False
assert "Failed to parse project xml" in SHARED.lastAlert assert "Failed to parse project xml" in SHARED.lastAlert
# Won't convert legacy file # Won't convert legacy file
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY)) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY))
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert theProject.openProject(fncPath) is False assert project.openProject(fncPath, clearLock=True) is False
assert "The file format of your project is about to be" in SHARED.lastAlert assert "The file format of your project is about to be" in SHARED.lastAlert
# Won't open project from newer version # Won't open project from newer version
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999)) mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999))
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert theProject.openProject(fncPath) is False assert project.openProject(fncPath, clearLock=True) is False
assert "This project was saved by a newer version" in SHARED.lastAlert assert "This project was saved by a newer version" in SHARED.lastAlert
# Fail checking items should still pass # Fail checking items should still pass
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.tree.NWTree.updateItemData", lambda *a: False) mp.setattr("novelwriter.core.tree.NWTree.updateItemData", lambda *a: False)
assert theProject.openProject(fncPath) is True assert project.openProject(fncPath, clearLock=True) is True
theProject.closeProject()
# Trigger an index rebuild # Trigger an index rebuild
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY)) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY))
mp.setattr("novelwriter.core.index.NWIndex.loadIndex", lambda *a: True) mp.setattr("novelwriter.core.index.NWIndex.loadIndex", lambda *a: True)
theProject.index._indexBroken = True project.index._indexBroken = True
assert theProject.openProject(fncPath) is True assert project.openProject(fncPath, clearLock=True) is True
assert "The file format of your project is about to be" in SHARED.lastAlert assert "The file format of your project is about to be" in SHARED.lastAlert
assert theProject.index._indexBroken is False assert project.index._indexBroken is False
theProject.closeProject() project.closeProject()
# END Test testCoreProject_Open # END Test testCoreProject_Open
@@ -260,28 +271,28 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
@pytest.mark.core @pytest.mark.core
def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncPath): def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncPath):
"""Test saving a project.""" """Test saving a project."""
theProject = NWProject() project = NWProject()
# Nothing to save # Nothing to save
assert theProject.saveProject() is False assert project.saveProject() is False
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(project, fncPath)
# Fail getting xml writer # Fail getting xml writer
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.storage.NWStorage.getXmlWriter", lambda *a: None) mp.setattr("novelwriter.core.storage.NWStorage.getXmlWriter", lambda *a: None)
assert theProject.saveProject() is False assert project.saveProject() is False
# Fail writing # Fail writing
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLWriter, "write", lambda *a: False) mp.setattr(ProjectXMLWriter, "write", lambda *a: False)
assert theProject.saveProject() is False assert project.saveProject() is False
# Save with and without autosave # Save with and without autosave
assert theProject.saveProject(autoSave=False) is True assert project.saveProject(autoSave=False) is True
assert theProject.saveProject(autoSave=True) is True assert project.saveProject(autoSave=True) is True
theProject.closeProject() project.closeProject()
# END Test testCoreProject_Save # END Test testCoreProject_Save
@@ -289,13 +300,13 @@ def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncPath):
@pytest.mark.core @pytest.mark.core
def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd): def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd):
"""Test helper functions for the project folder.""" """Test helper functions for the project folder."""
theProject = NWProject() project = NWProject()
buildTestProject(theProject, fncPath) buildTestProject(project, fncPath)
# Storage Objects # Storage Objects
assert isinstance(theProject.index, NWIndex) assert isinstance(project.index, NWIndex)
assert isinstance(theProject.tree, NWTree) assert isinstance(project.tree, NWTree)
assert isinstance(theProject.options, OptionState) assert isinstance(project.options, OptionState)
# Move Novel ROOT to after its files # Move Novel ROOT to after its files
oldOrder = [ oldOrder = [
@@ -318,20 +329,22 @@ def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd):
C.hCharRoot, C.hCharRoot,
C.hWorldRoot, C.hWorldRoot,
] ]
assert theProject.tree.handles() == oldOrder assert project.tree.handles() == oldOrder
theProject.setTreeOrder(newOrder) project.setTreeOrder(newOrder)
assert theProject.tree.handles() == newOrder assert project.tree.handles() == newOrder
# Add a non-existing item # Add a non-existing item
theProject.tree._order.append(C.hInvalid) project.tree._order.append(C.hInvalid)
# Add an item with a non-existent parent # Add an item with a non-existent parent
nHandle = theProject.newFile("Test File", C.hChapterDir) nHandle = project.newFile("Test File", C.hChapterDir)
theProject.tree[nHandle].setParent("cba9876543210") nItem = project.tree[nHandle]
assert theProject.tree[nHandle].itemParent == "cba9876543210" assert isinstance(nItem, NWItem)
nItem.setParent("cba9876543210")
assert nItem.itemParent == "cba9876543210"
retOrder = [] retOrder = []
for tItem in theProject.iterProjectItems(): for tItem in project.iterProjectItems():
retOrder.append(tItem.itemHandle) retOrder.append(tItem.itemHandle)
assert retOrder == [ assert retOrder == [
@@ -345,7 +358,7 @@ def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd):
C.hChapterDoc, C.hChapterDoc,
C.hSceneDoc, C.hSceneDoc,
] ]
assert theProject.tree[nHandle].itemParent is None assert nItem.itemParent is None
# END Test testCoreProject_AccessItems # END Test testCoreProject_AccessItems
@@ -353,9 +366,9 @@ def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd):
@pytest.mark.core @pytest.mark.core
def testCoreProject_StatusImport(mockGUI, fncPath, mockRnd): def testCoreProject_StatusImport(mockGUI, fncPath, mockRnd):
"""Test the status and importance flag handling.""" """Test the status and importance flag handling."""
theProject = NWProject() project = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(project, fncPath)
statusKeys = [C.sNew, C.sNote, C.sDraft, C.sFinished] statusKeys = [C.sNew, C.sNote, C.sDraft, C.sFinished]
importKeys = [C.iNew, C.iMinor, C.iMajor, C.iMain] importKeys = [C.iNew, C.iMinor, C.iMajor, C.iMain]
@@ -363,15 +376,15 @@ def testCoreProject_StatusImport(mockGUI, fncPath, mockRnd):
# Change Status # Change Status
# ============= # =============
theProject.tree[C.hNovelRoot].setStatus(statusKeys[3]) project.tree[C.hNovelRoot].setStatus(statusKeys[3]) # type: ignore
theProject.tree[C.hPlotRoot].setStatus(statusKeys[2]) project.tree[C.hPlotRoot].setStatus(statusKeys[2]) # type: ignore
theProject.tree[C.hCharRoot].setStatus(statusKeys[1]) project.tree[C.hCharRoot].setStatus(statusKeys[1]) # type: ignore
theProject.tree[C.hWorldRoot].setStatus(statusKeys[3]) project.tree[C.hWorldRoot].setStatus(statusKeys[3]) # type: ignore
assert theProject.tree[C.hNovelRoot].itemStatus == statusKeys[3] assert project.tree[C.hNovelRoot].itemStatus == statusKeys[3] # type: ignore
assert theProject.tree[C.hPlotRoot].itemStatus == statusKeys[2] assert project.tree[C.hPlotRoot].itemStatus == statusKeys[2] # type: ignore
assert theProject.tree[C.hCharRoot].itemStatus == statusKeys[1] assert project.tree[C.hCharRoot].itemStatus == statusKeys[1] # type: ignore
assert theProject.tree[C.hWorldRoot].itemStatus == statusKeys[3] assert project.tree[C.hWorldRoot].itemStatus == statusKeys[3] # type: ignore
newList = [ newList = [
{"key": statusKeys[0], "name": "New", "cols": (1, 1, 1)}, {"key": statusKeys[0], "name": "New", "cols": (1, 1, 1)},
@@ -380,36 +393,36 @@ def testCoreProject_StatusImport(mockGUI, fncPath, mockRnd):
{"key": statusKeys[3], "name": "Edited", "cols": (4, 4, 4)}, # Renamed {"key": statusKeys[3], "name": "Edited", "cols": (4, 4, 4)}, # Renamed
{"key": None, "name": "Finished", "cols": (5, 5, 5)}, # New, reused name {"key": None, "name": "Finished", "cols": (5, 5, 5)}, # New, reused name
] ]
assert theProject.setStatusColours(None, None) is False assert project.setStatusColours(None, None) is False # type: ignore
assert theProject.setStatusColours([], []) is False assert project.setStatusColours([], []) is False
assert theProject.setStatusColours(newList, []) is True assert project.setStatusColours(newList, []) is True
assert theProject.data.itemStatus.name(statusKeys[0]) == "New" assert project.data.itemStatus.name(statusKeys[0]) == "New"
assert theProject.data.itemStatus.name(statusKeys[1]) == "Draft" assert project.data.itemStatus.name(statusKeys[1]) == "Draft"
assert theProject.data.itemStatus.name(statusKeys[2]) == "Note" assert project.data.itemStatus.name(statusKeys[2]) == "Note"
assert theProject.data.itemStatus.name(statusKeys[3]) == "Edited" assert project.data.itemStatus.name(statusKeys[3]) == "Edited"
assert theProject.data.itemStatus.cols(statusKeys[0]) == (1, 1, 1) assert project.data.itemStatus.cols(statusKeys[0]) == (1, 1, 1)
assert theProject.data.itemStatus.cols(statusKeys[1]) == (2, 2, 2) assert project.data.itemStatus.cols(statusKeys[1]) == (2, 2, 2)
assert theProject.data.itemStatus.cols(statusKeys[2]) == (3, 3, 3) assert project.data.itemStatus.cols(statusKeys[2]) == (3, 3, 3)
assert theProject.data.itemStatus.cols(statusKeys[3]) == (4, 4, 4) assert project.data.itemStatus.cols(statusKeys[3]) == (4, 4, 4)
# Check the new entry # Check the new entry
lastKey = theProject.data.itemStatus.check("s000010") lastKey = project.data.itemStatus.check("s000010")
assert lastKey == "s000010" assert lastKey == "s000010"
assert theProject.data.itemStatus.name(lastKey) == "Finished" assert project.data.itemStatus.name(lastKey) == "Finished"
assert theProject.data.itemStatus.cols(lastKey) == (5, 5, 5) assert project.data.itemStatus.cols(lastKey) == (5, 5, 5)
# Delete last entry # Delete last entry
assert theProject.setStatusColours([], [lastKey]) is True assert project.setStatusColours([], [lastKey]) is True
assert theProject.data.itemStatus.name(lastKey) == "New" assert project.data.itemStatus.name(lastKey) == "New"
# Change Importance # Change Importance
# ================= # =================
fHandle = theProject.newFile("Jane Doe", C.hCharRoot) fHandle = project.newFile("Jane Doe", C.hCharRoot)
theProject.tree[fHandle].setImport(importKeys[3]) project.tree[fHandle].setImport(importKeys[3]) # type: ignore
assert theProject.tree[fHandle].itemImport == importKeys[3] assert project.tree[fHandle].itemImport == importKeys[3] # type: ignore
newList = [ newList = [
{"key": importKeys[0], "name": "New", "cols": (1, 1, 1)}, {"key": importKeys[0], "name": "New", "cols": (1, 1, 1)},
{"key": importKeys[1], "name": "Minor", "cols": (2, 2, 2)}, {"key": importKeys[1], "name": "Minor", "cols": (2, 2, 2)},
@@ -417,44 +430,44 @@ def testCoreProject_StatusImport(mockGUI, fncPath, mockRnd):
{"key": importKeys[3], "name": "Min", "cols": (4, 4, 4)}, {"key": importKeys[3], "name": "Min", "cols": (4, 4, 4)},
{"key": None, "name": "Max", "cols": (5, 5, 5)}, {"key": None, "name": "Max", "cols": (5, 5, 5)},
] ]
assert theProject.setImportColours(None, None) is False assert project.setImportColours(None, None) is False # type: ignore
assert theProject.setImportColours([], []) is False assert project.setImportColours([], []) is False
assert theProject.setImportColours(newList, []) is True assert project.setImportColours(newList, []) is True
assert theProject.data.itemImport.name(importKeys[0]) == "New" assert project.data.itemImport.name(importKeys[0]) == "New"
assert theProject.data.itemImport.name(importKeys[1]) == "Minor" assert project.data.itemImport.name(importKeys[1]) == "Minor"
assert theProject.data.itemImport.name(importKeys[2]) == "Major" assert project.data.itemImport.name(importKeys[2]) == "Major"
assert theProject.data.itemImport.name(importKeys[3]) == "Min" assert project.data.itemImport.name(importKeys[3]) == "Min"
assert theProject.data.itemImport.cols(importKeys[0]) == (1, 1, 1) assert project.data.itemImport.cols(importKeys[0]) == (1, 1, 1)
assert theProject.data.itemImport.cols(importKeys[1]) == (2, 2, 2) assert project.data.itemImport.cols(importKeys[1]) == (2, 2, 2)
assert theProject.data.itemImport.cols(importKeys[2]) == (3, 3, 3) assert project.data.itemImport.cols(importKeys[2]) == (3, 3, 3)
assert theProject.data.itemImport.cols(importKeys[3]) == (4, 4, 4) assert project.data.itemImport.cols(importKeys[3]) == (4, 4, 4)
# Check the new entry # Check the new entry
lastKey = theProject.data.itemImport.check("i000012") lastKey = project.data.itemImport.check("i000012")
assert lastKey == "i000012" assert lastKey == "i000012"
assert theProject.data.itemImport.name(lastKey) == "Max" assert project.data.itemImport.name(lastKey) == "Max"
assert theProject.data.itemImport.cols(lastKey) == (5, 5, 5) assert project.data.itemImport.cols(lastKey) == (5, 5, 5)
# Delete last entry # Delete last entry
assert theProject.setImportColours([], [lastKey]) is True assert project.setImportColours([], [lastKey]) is True
assert theProject.data.itemImport.name(lastKey) == "New" assert project.data.itemImport.name(lastKey) == "New"
# Delete Status/Import # Delete Status/Import
# ==================== # ====================
theProject.data.itemStatus.resetCounts() project.data.itemStatus.resetCounts()
for key in list(theProject.data.itemStatus.keys()): for key in list(project.data.itemStatus.keys()):
assert theProject.data.itemStatus.remove(key) is True assert project.data.itemStatus.remove(key) is True
theProject.data.itemImport.resetCounts() project.data.itemImport.resetCounts()
for key in list(theProject.data.itemImport.keys()): for key in list(project.data.itemImport.keys()):
assert theProject.data.itemImport.remove(key) is True assert project.data.itemImport.remove(key) is True
assert len(theProject.data.itemStatus) == 0 assert len(project.data.itemStatus) == 0
assert len(theProject.data.itemImport) == 0 assert len(project.data.itemImport) == 0
assert theProject.saveProject() is True assert project.saveProject() is True
theProject.closeProject() project.closeProject()
# END Test testCoreProject_StatusImport # END Test testCoreProject_StatusImport
@@ -462,96 +475,96 @@ def testCoreProject_StatusImport(mockGUI, fncPath, mockRnd):
@pytest.mark.core @pytest.mark.core
def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd): def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
"""Test other project class methods and functions.""" """Test other project class methods and functions."""
theProject = NWProject() project = NWProject()
buildTestProject(theProject, fncPath) buildTestProject(project, fncPath)
# Project Name # Project Name
theProject.data.setName(" A Name ") project.data.setName(" A Name ")
assert theProject.data.name == "A Name" assert project.data.name == "A Name"
# Project Title # Project Title
theProject.data.setTitle(" A Title ") project.data.setTitle(" A Title ")
assert theProject.data.title == "A Title" assert project.data.title == "A Title"
# Project Author # Project Author
theProject.data.setAuthor(" Jane\tDoe ") project.data.setAuthor(" Jane\tDoe ")
assert theProject.data.author == "Jane Doe" assert project.data.author == "Jane Doe"
# Edit Time # Edit Time
theProject.data.setEditTime(1234) project.data.setEditTime(1234)
theProject._session._start = 1600000000 project._session._start = 1600000000
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.project.time", lambda: 1600005600) mp.setattr("novelwriter.core.project.time", lambda: 1600005600)
assert theProject.currentEditTime == 6834 assert project.currentEditTime == 6834
# Trash folder # Trash folder
# Should create on first call, and just returned on later calls # Should create on first call, and just returned on later calls
hTrash = "0000000000010" hTrash = "0000000000010"
assert theProject.tree[hTrash] is None assert project.tree[hTrash] is None
assert theProject.trashFolder() == hTrash assert project.trashFolder() == hTrash
assert theProject.trashFolder() == hTrash assert project.trashFolder() == hTrash
# Spell check # Spell check
theProject.setProjectChanged(False) project.setProjectChanged(False)
theProject.data.setSpellCheck(True) project.data.setSpellCheck(True)
theProject.data.setSpellCheck(False) project.data.setSpellCheck(False)
assert theProject.projChanged is True assert project.projChanged is True
assert theProject.projOpened > 0 assert project.projOpened > 0
# Spell language # Spell language
theProject.setProjectChanged(False) project.setProjectChanged(False)
assert theProject.data.spellLang is None assert project.data.spellLang is None
theProject.data.setSpellLang(None) project.data.setSpellLang(None)
assert theProject.data.spellLang is None assert project.data.spellLang is None
theProject.data.setSpellLang("None") # Should be interpreted as None project.data.setSpellLang("None") # Should be interpreted as None
assert theProject.data.spellLang is None assert project.data.spellLang is None
theProject.data.setSpellLang("en_GB") project.data.setSpellLang("en_GB")
assert theProject.data.spellLang == "en_GB" assert project.data.spellLang == "en_GB"
assert theProject.projChanged is True assert project.projChanged is True
# Project Language # Project Language
theProject.setProjectChanged(False) project.setProjectChanged(False)
theProject.data.setLanguage("en") project.data.setLanguage("en")
theProject.setProjectLang(None) project.setProjectLang(None)
assert theProject.data.language is None assert project.data.language is None
theProject.setProjectLang("en_GB") project.setProjectLang("en_GB")
assert theProject.data.language == "en_GB" assert project.data.language == "en_GB"
# Language Lookup # Language Lookup
assert theProject.localLookup(1) == "One" assert project.localLookup(1) == "One"
assert theProject.localLookup(10) == "Ten" assert project.localLookup(10) == "Ten"
# Set invalid language # Set invalid language
theProject.data.setLanguage("foo") project.data.setLanguage("foo")
theProject._loadProjectLocalisation() project._loadProjectLocalisation()
assert theProject.localLookup(1) == "One" assert project.localLookup(1) == "One"
assert theProject.localLookup(10) == "Ten" assert project.localLookup(10) == "Ten"
# Block reading language data # Block reading language data
theProject.data.setLanguage("en") project.data.setLanguage("en")
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError) mp.setattr("builtins.open", causeOSError)
theProject._loadProjectLocalisation() project._loadProjectLocalisation()
assert theProject.localLookup(1) == "One" assert project.localLookup(1) == "One"
assert theProject.localLookup(10) == "Ten" assert project.localLookup(10) == "Ten"
# Last edited # Last edited
theProject.setProjectChanged(False) project.setProjectChanged(False)
theProject._data.setLastHandle("0123456789abc", "editor") project._data.setLastHandle("0123456789abc", "editor")
assert theProject._data.getLastHandle("editor") == "0123456789abc" assert project._data.getLastHandle("editor") == "0123456789abc"
assert theProject.projChanged assert project.projChanged
# Last viewed # Last viewed
theProject.setProjectChanged(False) project.setProjectChanged(False)
theProject._data.setLastHandle("0123456789abc", "viewer") project._data.setLastHandle("0123456789abc", "viewer")
assert theProject._data.getLastHandle("viewer") == "0123456789abc" assert project._data.getLastHandle("viewer") == "0123456789abc"
assert theProject.projChanged assert project.projChanged
# Autoreplace # Auto Replace
theProject.setProjectChanged(False) project.setProjectChanged(False)
theProject.data.setAutoReplace({"A": "B", "C": "D"}) project.data.setAutoReplace({"A": "B", "C": "D"})
assert theProject.data.autoReplace == {"A": "B", "C": "D"} assert project.data.autoReplace == {"A": "B", "C": "D"}
assert theProject.projChanged assert project.projChanged
# Change project tree order # Change project tree order
oldOrder = [ oldOrder = [
@@ -564,11 +577,11 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
"0000000000008", "0000000000009", "000000000000a", "0000000000008", "0000000000009", "000000000000a",
"000000000000e", "000000000000f", "000000000000e", "000000000000f",
] ]
assert theProject.tree.handles() == oldOrder assert project.tree.handles() == oldOrder
theProject.setTreeOrder(newOrder) project.setTreeOrder(newOrder)
assert theProject.tree.handles() == newOrder assert project.tree.handles() == newOrder
theProject.setTreeOrder(oldOrder) project.setTreeOrder(oldOrder)
assert theProject.tree.handles() == oldOrder assert project.tree.handles() == oldOrder
# END Test testCoreProject_Methods # END Test testCoreProject_Methods
@@ -580,38 +593,38 @@ def testCoreProject_Backup(monkeypatch, mockGUI, fncPath, tstPaths):
backup file and checks that the project XML file is identical to backup file and checks that the project XML file is identical to
the original file. the original file.
""" """
theProject = NWProject() project = NWProject()
# No Project # No Project
assert theProject.backupProject(doNotify=False) is False assert project.backupProject(doNotify=False) is False
buildTestProject(theProject, fncPath) buildTestProject(project, fncPath)
# Invalid Settings # Invalid Settings
# ================ # ================
# Missing project name # Missing project name
CONFIG._backupPath = tstPaths.tmpDir CONFIG._backupPath = tstPaths.tmpDir
theProject.data.setName("") project.data.setName("")
assert theProject.backupProject(doNotify=False) is False assert project.backupProject(doNotify=False) is False
# Valid Settings # Valid Settings
# ============== # ==============
CONFIG._backupPath = tstPaths.tmpDir CONFIG._backupPath = tstPaths.tmpDir
theProject.data.setName("Test Minimal") project.data.setName("Test Minimal")
# Can't make folder # Can't make folder
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("pathlib.Path.mkdir", causeOSError) mp.setattr("pathlib.Path.mkdir", causeOSError)
assert theProject.backupProject(doNotify=False) is False assert project.backupProject(doNotify=False) is False
# Can't write archive # Can't write archive
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("zipfile.ZipFile.write", causeOSError) mp.setattr("zipfile.ZipFile.write", causeOSError)
assert theProject.backupProject(doNotify=False) is False assert project.backupProject(doNotify=False) is False
# Test correct settings # Test correct settings
assert theProject.backupProject(doNotify=True) is True assert project.backupProject(doNotify=True) is True
theFiles = sorted((tstPaths.tmpDir / "Test Minimal").iterdir()) theFiles = sorted((tstPaths.tmpDir / "Test Minimal").iterdir())
assert len(theFiles) in (1, 2) # Sometimes 2 due to clock tick assert len(theFiles) in (1, 2) # Sometimes 2 due to clock tick
+20 -19
View File
@@ -25,6 +25,7 @@ import pytest
from shutil import copyfile from shutil import copyfile
from datetime import datetime from datetime import datetime
from novelwriter.constants import nwFiles
from tools import cmpFiles, writeFile from tools import cmpFiles, writeFile
from mocked import causeOSError from mocked import causeOSError
@@ -62,7 +63,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
xmlReader = ProjectXMLReader(xmlFile) xmlReader = ProjectXMLReader(xmlFile)
assert xmlReader.state == XMLReadState.NO_ACTION assert xmlReader.state == XMLReadState.NO_ACTION
data = NWProjectData(MockProject()) data = NWProjectData(MockProject()) # type: ignore
content = [] content = []
# With no valid files, the read should fail # With no valid files, the read should fail
@@ -132,7 +133,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
assert xmlReader.state == XMLReadState.WAS_LEGACY assert xmlReader.state == XMLReadState.WAS_LEGACY
# Reset data objects # Reset data objects
data = NWProjectData(MockProject()) data = NWProjectData(MockProject()) # type: ignore
content = [] content = []
# Parse a valid, complete file # Parse a valid, complete file
@@ -215,13 +216,13 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
mockProject = MockProject() mockProject = MockProject()
mockProject.__setattr__("data", data) mockProject.__setattr__("data", data)
for entry in content: for entry in content:
item = NWItem(mockProject, "0000000000000") item = NWItem(mockProject, "0000000000000") # type: ignore
item.unpack(entry) item.unpack(entry)
packedContent.append(item.pack()) packedContent.append(item.pack())
# Save the project again, which should produce an identical project xml # Save the project again, which should produce an identical project xml
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
xmlWriter = ProjectXMLWriter(fncPath) xmlWriter = ProjectXMLWriter(fncPath / nwFiles.PROJ_FILE)
# Fail saving # Fail saving
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
@@ -254,7 +255,7 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockRnd):
xmlReader = ProjectXMLReader(xmlFile) xmlReader = ProjectXMLReader(xmlFile)
assert xmlReader.state == XMLReadState.NO_ACTION assert xmlReader.state == XMLReadState.NO_ACTION
data = NWProjectData(MockProject()) data = NWProjectData(MockProject()) # type: ignore
content = [] content = []
assert xmlReader.read(data, content) is True assert xmlReader.read(data, content) is True
@@ -335,7 +336,7 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockRnd):
mockProject.__setattr__("data", data) mockProject.__setattr__("data", data)
status = {} status = {}
for entry in content: for entry in content:
item = NWItem(mockProject, "0000000000000") item = NWItem(mockProject, "0000000000000") # type: ignore
item.unpack(entry) item.unpack(entry)
status[item.itemHandle] = item.getImportStatus(incIcon=False)[0] status[item.itemHandle] = item.getImportStatus(incIcon=False)[0]
packedContent.append(item.pack()) packedContent.append(item.pack())
@@ -367,7 +368,7 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockRnd):
# Save the project again, which should produce an identical project xml # Save the project again, which should produce an identical project xml
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
xmlWriter = ProjectXMLWriter(fncPath) xmlWriter = ProjectXMLWriter(fncPath / nwFiles.PROJ_FILE)
data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed") data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
testFile = tstPaths.outDir / "projectXML_ReadLegacy10.nwx" testFile = tstPaths.outDir / "projectXML_ReadLegacy10.nwx"
@@ -389,7 +390,7 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockRnd):
xmlReader = ProjectXMLReader(xmlFile) xmlReader = ProjectXMLReader(xmlFile)
assert xmlReader.state == XMLReadState.NO_ACTION assert xmlReader.state == XMLReadState.NO_ACTION
data = NWProjectData(MockProject()) data = NWProjectData(MockProject()) # type: ignore
content = [] content = []
assert xmlReader.read(data, content) is True assert xmlReader.read(data, content) is True
@@ -470,7 +471,7 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockRnd):
mockProject.__setattr__("data", data) mockProject.__setattr__("data", data)
status = {} status = {}
for entry in content: for entry in content:
item = NWItem(mockProject, "0000000000000") item = NWItem(mockProject, "0000000000000") # type: ignore
item.unpack(entry) item.unpack(entry)
status[item.itemHandle] = item.getImportStatus(incIcon=False)[0] status[item.itemHandle] = item.getImportStatus(incIcon=False)[0]
packedContent.append(item.pack()) packedContent.append(item.pack())
@@ -502,7 +503,7 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockRnd):
# Save the project again, which should produce an identical project xml # Save the project again, which should produce an identical project xml
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
xmlWriter = ProjectXMLWriter(fncPath) xmlWriter = ProjectXMLWriter(fncPath / nwFiles.PROJ_FILE)
data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed") data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
testFile = tstPaths.outDir / "projectXML_ReadLegacy11.nwx" testFile = tstPaths.outDir / "projectXML_ReadLegacy11.nwx"
@@ -524,7 +525,7 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockRnd):
xmlReader = ProjectXMLReader(xmlFile) xmlReader = ProjectXMLReader(xmlFile)
assert xmlReader.state == XMLReadState.NO_ACTION assert xmlReader.state == XMLReadState.NO_ACTION
data = NWProjectData(MockProject()) data = NWProjectData(MockProject()) # type: ignore
content = [] content = []
assert xmlReader.read(data, content) is True assert xmlReader.read(data, content) is True
@@ -605,7 +606,7 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockRnd):
mockProject.__setattr__("data", data) mockProject.__setattr__("data", data)
status = {} status = {}
for entry in content: for entry in content:
item = NWItem(mockProject, "0000000000000") item = NWItem(mockProject, "0000000000000") # type: ignore
item.unpack(entry) item.unpack(entry)
status[item.itemHandle] = item.getImportStatus(incIcon=False)[0] status[item.itemHandle] = item.getImportStatus(incIcon=False)[0]
packedContent.append(item.pack()) packedContent.append(item.pack())
@@ -640,7 +641,7 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockRnd):
# Save the project again, which should produce an identical project xml # Save the project again, which should produce an identical project xml
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
xmlWriter = ProjectXMLWriter(fncPath) xmlWriter = ProjectXMLWriter(fncPath / nwFiles.PROJ_FILE)
data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed") data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
testFile = tstPaths.outDir / "projectXML_ReadLegacy12.nwx" testFile = tstPaths.outDir / "projectXML_ReadLegacy12.nwx"
@@ -662,7 +663,7 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd):
xmlReader = ProjectXMLReader(xmlFile) xmlReader = ProjectXMLReader(xmlFile)
assert xmlReader.state == XMLReadState.NO_ACTION assert xmlReader.state == XMLReadState.NO_ACTION
data = NWProjectData(MockProject()) data = NWProjectData(MockProject()) # type: ignore
content = [] content = []
assert xmlReader.read(data, content) is True assert xmlReader.read(data, content) is True
@@ -743,7 +744,7 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd):
mockProject.__setattr__("data", data) mockProject.__setattr__("data", data)
status = {} status = {}
for entry in content: for entry in content:
item = NWItem(mockProject, "0000000000000") item = NWItem(mockProject, "0000000000000") # type: ignore
item.unpack(entry) item.unpack(entry)
status[item.itemHandle] = item.getImportStatus(incIcon=False)[0] status[item.itemHandle] = item.getImportStatus(incIcon=False)[0]
packedContent.append(item.pack()) packedContent.append(item.pack())
@@ -778,7 +779,7 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd):
# Save the project again, which should produce an identical project xml # Save the project again, which should produce an identical project xml
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
xmlWriter = ProjectXMLWriter(fncPath) xmlWriter = ProjectXMLWriter(fncPath / nwFiles.PROJ_FILE)
data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed") data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
testFile = tstPaths.outDir / "projectXML_ReadLegacy13.nwx" testFile = tstPaths.outDir / "projectXML_ReadLegacy13.nwx"
@@ -800,7 +801,7 @@ def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockRnd):
xmlReader = ProjectXMLReader(xmlFile) xmlReader = ProjectXMLReader(xmlFile)
assert xmlReader.state == XMLReadState.NO_ACTION assert xmlReader.state == XMLReadState.NO_ACTION
data = NWProjectData(MockProject()) data = NWProjectData(MockProject()) # type: ignore
content = [] content = []
assert xmlReader.read(data, content) is True assert xmlReader.read(data, content) is True
@@ -881,7 +882,7 @@ def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockRnd):
mockProject.__setattr__("data", data) mockProject.__setattr__("data", data)
status = {} status = {}
for entry in content: for entry in content:
item = NWItem(mockProject, "0000000000000") item = NWItem(mockProject, "0000000000000") # type: ignore
item.unpack(entry) item.unpack(entry)
status[item.itemHandle] = item.getImportStatus(incIcon=False)[0] status[item.itemHandle] = item.getImportStatus(incIcon=False)[0]
packedContent.append(item.pack()) packedContent.append(item.pack())
@@ -918,7 +919,7 @@ def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockRnd):
# Save the project again, which should produce an identical project xml # Save the project again, which should produce an identical project xml
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
xmlWriter = ProjectXMLWriter(fncPath) xmlWriter = ProjectXMLWriter(fncPath / nwFiles.PROJ_FILE)
data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed") data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
testFile = tstPaths.outDir / "projectXML_ReadLegacy14.nwx" testFile = tstPaths.outDir / "projectXML_ReadLegacy14.nwx"
+181 -94
View File
@@ -26,13 +26,14 @@ import pytest
from pathlib import Path from pathlib import Path
from zipfile import ZipFile from zipfile import ZipFile
from tools import C, buildTestProject, writeFile from tools import C, buildTestProject
from mocked import causeOSError from mocked import causeOSError
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.core.storage import NWStorage, _LegacyStorage from novelwriter.core.storage import NWStorage, NWStorageOpen, _LegacyStorage
from novelwriter.core.document import NWDocument
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
@@ -42,21 +43,19 @@ class MockProject:
@pytest.mark.core @pytest.mark.core
def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd): def testCoreStorage_CreateNewProject(mockGUI, fncPath):
"""Test opening a project in a folder.""" """Test creating a project in a folder."""
theProject = NWProject() project = NWProject()
mockRnd.reset()
buildTestProject(theProject, fncPath)
theProject.closeProject()
# Create instance # Create instance
storage = NWStorage(theProject) storage = NWStorage(project)
# Check defaults # Check defaults
assert storage.storagePath is None assert storage.storagePath is None
assert storage.runtimePath is None assert storage.runtimePath is None
assert storage.contentPath is None assert storage.contentPath is None
assert storage._openMode == NWStorage.MODE_INACTIVE assert storage._openMode == NWStorage.MODE_INACTIVE
assert storage._ready is False
# Check closed project return values # Check closed project return values
assert storage.isOpen() is False assert storage.isOpen() is False
@@ -66,52 +65,137 @@ def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd):
assert storage.getMetaFile("file") is None assert storage.getMetaFile("file") is None
assert storage.scanContent() == [] assert storage.scanContent() == []
# Open project as a new project should fail # Cannot prepare a non-empty folder
assert storage.openProjectInPlace(fncPath, newProject=True) is False (fncPath / "foobar.txt").touch()
assert storage.createNewProject(fncPath) is False
# Opening as a non-new project is fine # Try creating in a non-existent subfolder instead
assert storage.openProjectInPlace(fncPath, newProject=False) is True assert storage.createNewProject(fncPath / "project1") is True
assert (fncPath / "project1").is_dir()
assert (fncPath / "project1" / "meta").is_dir()
assert (fncPath / "project1" / "content").is_dir()
# Opening the project file is also fine # However, the parent folder must exist
assert storage.openProjectInPlace(fncPath / nwFiles.PROJ_FILE, newProject=False) is True assert storage.createNewProject(fncPath / "foobar" / "project1") is False
assert isinstance(storage.exc, FileNotFoundError)
# Opening as a non-new project on a non-existing folder should fail project.closeProject()
assert storage.openProjectInPlace(fncPath / "foobar", newProject=False) is False
# Check settings # END Test testCoreStorage_CreateNewProject
assert storage.storagePath == fncPath
assert storage.runtimePath == fncPath
assert storage.contentPath == fncPath / "content"
assert storage._openMode == NWStorage.MODE_INPLACE
# Open the project itself
theProject.openProject(fncPath)
storage = theProject.storage
# Get XML components @pytest.mark.core
assert isinstance(storage.getXmlReader(), ProjectXMLReader) def testCoreStorage_InitProjectStorage(mockGUI, fncPath, mockRnd):
assert isinstance(storage.getXmlWriter(), ProjectXMLWriter) """Test initialising a project in a folder."""
project = NWProject()
# Get content # Create instance
assert sorted(storage.scanContent()) == [C.hTitlePage, C.hChapterDoc, C.hSceneDoc] storage = NWStorage(project)
# Get document # Check defaults
assert storage.getDocument(C.hSceneDoc).readDocument() == "### New Scene\n\n" assert storage.storagePath is None
assert storage.runtimePath is None
assert storage.contentPath is None
assert storage._openMode == NWStorage.MODE_INACTIVE
assert storage._ready is False
# Get paths # Check closed project return values
assert storage.getMetaFile("stuff") == fncPath / "meta" / "stuff"
# Clean up
theProject.closeProject()
# Check closed project return values (again)
assert storage.isOpen() is False assert storage.isOpen() is False
assert storage.getXmlReader() is None assert storage.getXmlReader() is None
assert storage.getXmlWriter() is None assert storage.getXmlWriter() is None
assert bool(storage.getDocument(C.hSceneDoc)) is False assert bool(storage.getDocument(C.hSceneDoc)) is False
assert storage.getMetaFile("file") is None assert storage.getMetaFile("file") is None
assert storage.scanContent() == []
# END Test testCoreStorage_ProjectInPlace # Create a new project
buildTestProject(project, fncPath)
# Init with the wrong file
assert storage.initProjectStorage(fncPath / "foobar.txt") == NWStorageOpen.UNKOWN
storage._clearLockFile()
storage.clear()
# Init with the user's home dir
assert storage.initProjectStorage(Path.home()) == NWStorageOpen.UNKOWN
storage._clearLockFile()
storage.clear()
# Init with the project folder is OK
assert storage.initProjectStorage(fncPath) == NWStorageOpen.READY
assert storage.runtimePath == fncPath
assert storage.storagePath == fncPath
assert storage.contentPath == fncPath / "content"
assert storage._openMode == NWStorage.MODE_INPLACE
storage._clearLockFile()
storage.clear()
# Init with the project main file is OK
assert storage.initProjectStorage(fncPath / nwFiles.PROJ_FILE) == NWStorageOpen.READY
assert storage.runtimePath == fncPath
assert storage.storagePath == fncPath
assert storage.contentPath == fncPath / "content"
assert storage._openMode == NWStorage.MODE_INPLACE
storage._clearLockFile()
storage.clear()
# Open twice, where second should fail due to lockfile
assert storage.initProjectStorage(fncPath) == NWStorageOpen.READY
assert storage.initProjectStorage(fncPath) == NWStorageOpen.LOCKED
assert isinstance(storage.lockStatus, list)
assert len(storage.lockStatus) == 4
# But open again with clear lock file flag set is OK
assert storage.initProjectStorage(fncPath, clearLock=True) == NWStorageOpen.READY
assert storage.lockStatus is None
# We should now have access to project resources
assert isinstance(storage.getXmlReader(), ProjectXMLReader)
assert isinstance(storage.getXmlWriter(), ProjectXMLWriter)
assert isinstance(storage.getDocument(C.hSceneDoc), NWDocument)
assert repr(storage.getDocument(C.hSceneDoc)) == f"<NWDocument handle={C.hSceneDoc}>"
project.closeProject()
# END Test testCoreStorage_InitProjectStorage
@pytest.mark.core
def testCoreStorage_InitProjectStorage_Invalid(mockGUI, fncPath):
"""Test initialising a project in an invalid folder."""
project = NWProject()
# Create instance
storage = NWStorage(project)
# Check defaults
assert storage.storagePath is None
assert storage.runtimePath is None
assert storage.contentPath is None
assert storage._openMode == NWStorage.MODE_INACTIVE
assert storage._ready is False
# Check closed project return values
assert storage.isOpen() is False
assert storage.getXmlReader() is None
assert storage.getXmlWriter() is None
assert bool(storage.getDocument(C.hSceneDoc)) is False
assert storage.getMetaFile("file") is None
assert storage.scanContent() == []
# Populate folder with invalid files
(fncPath / "meta").touch() # These are now files but should be folders
(fncPath / "content").touch() # These are now files but should be folders
# Try opening the folder, but there is no project file
assert storage.initProjectStorage(fncPath) == NWStorageOpen.NOT_FOUND
# Add the project file, and we should now fail on the folders
(fncPath / nwFiles.PROJ_FILE).touch()
assert storage.initProjectStorage(fncPath) == NWStorageOpen.FAILED
project.closeProject()
# END Test testCoreStorage_InitProjectStorage_Invalid
@pytest.mark.core @pytest.mark.core
@@ -123,54 +207,55 @@ def testCoreStorage_LockFile(monkeypatch, fncPath):
assert storage.isOpen() is False assert storage.isOpen() is False
# Project not open, so cannot read/write lock file # Project not open, so cannot read/write lock file
assert storage.readLockFile() == ["ERROR"] assert storage._readLockFile() is None
assert storage.writeLockFile() is False assert storage._writeLockFile() is False
assert storage.clearLockFile() is False assert storage._clearLockFile() is False
# Set a path to work with # Set a path to work with
lockFilePath = fncPath / nwFiles.PROJ_LOCK lockFilePath = fncPath / nwFiles.PROJ_LOCK
storage._lockFilePath = lockFilePath storage._lockFilePath = lockFilePath
# Path is set, but there is no lockfile # Path is set, but there is no lockfile
assert storage.readLockFile() == [] storage._readLockFile()
assert storage.lockStatus is None
# Write lockfile fails # Write lockfile fails
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("pathlib.Path.write_text", causeOSError) mp.setattr("pathlib.Path.write_text", causeOSError)
assert storage.writeLockFile() is False assert storage._writeLockFile() is False
assert not lockFilePath.exists() assert not lockFilePath.exists()
# Successful write # Successful write
assert storage.writeLockFile() is True assert storage._writeLockFile() is True
assert lockFilePath.exists() assert lockFilePath.exists()
assert lockFilePath.read_text().split(";")[3] == "1000" assert lockFilePath.read_text().split(";")[3] == "1000"
# Read lockfile fails # Read lockfile fails
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("pathlib.Path.read_text", causeOSError) mp.setattr("pathlib.Path.read_text", causeOSError)
assert storage.readLockFile() == ["ERROR"] storage._readLockFile()
assert storage.lockStatus == ["ERROR", "ERROR", "ERROR", "ERROR"]
assert lockFilePath.exists() assert lockFilePath.exists()
# Successful read # Successful read
assert storage.readLockFile() == [ storage._readLockFile()
CONFIG.hostName, assert storage.lockStatus == [
CONFIG.osType, CONFIG.hostName, CONFIG.osType, CONFIG.kernelVer, "1000",
CONFIG.kernelVer,
"1000",
] ]
# Write an invalid lockfile # Write an invalid lockfile
writeFile(lockFilePath, "a;b;c") lockFilePath.write_text("a;b;c")
assert storage.readLockFile() == ["ERROR"] storage._readLockFile()
assert storage.lockStatus is None
# Fail to remove lockfile # Fail to remove lockfile
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("pathlib.Path.unlink", causeOSError) mp.setattr("pathlib.Path.unlink", causeOSError)
assert storage.clearLockFile() is False assert storage._clearLockFile() is False
assert lockFilePath.exists() assert lockFilePath.exists()
# Successful remove # Successful remove
assert storage.clearLockFile() is True assert storage._clearLockFile() is True
assert not lockFilePath.exists() assert not lockFilePath.exists()
# END Test testCoreStorage_LockFile # END Test testCoreStorage_LockFile
@@ -212,46 +297,46 @@ def testCoreStorage_ZipIt(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd):
# END Test testCoreStorage_ZipIt # END Test testCoreStorage_ZipIt
@pytest.mark.core # @pytest.mark.core
def testCoreStorage_PrepareStorage(monkeypatch, fncPath): # def testCoreStorage_PrepareStorage(monkeypatch, fncPath):
"""Test the project path preparation functions.""" # """Test the project path preparation functions."""
storage = NWStorage(MockProject()) # type: ignore # storage = NWStorage(MockProject()) # type: ignore
assert storage.isOpen() is False # assert storage.isOpen() is False
# No path set # # No path set
assert storage._prepareStorage() is False # assert storage._prepareStorage() is False
# Set path to home # # Set path to home
storage._runtimePath = fncPath # storage._runtimePath = fncPath
with monkeypatch.context() as mp: # with monkeypatch.context() as mp:
mp.setattr("pathlib.Path.home", lambda: fncPath) # mp.setattr("pathlib.Path.home", lambda: fncPath)
assert storage._prepareStorage() is False # assert storage._prepareStorage() is False
# Fail on mkdir # # Fail on mkdir
storage._runtimePath = fncPath # storage._runtimePath = fncPath
with monkeypatch.context() as mp: # with monkeypatch.context() as mp:
mp.setattr("pathlib.Path.mkdir", causeOSError) # mp.setattr("pathlib.Path.mkdir", causeOSError)
assert storage._prepareStorage() is False # assert storage._prepareStorage() is False
# Set up the folder # # Set up the folder
storage._runtimePath = fncPath # storage._runtimePath = fncPath
assert storage._prepareStorage(checkLegacy=False) is True # assert storage._prepareStorage(checkLegacy=False) is True
assert (fncPath / "content").exists() # assert (fncPath / "content").exists()
assert (fncPath / "meta").exists() # assert (fncPath / "meta").exists()
assert not (fncPath / "cache").exists() # Removed in 2.1b1 # assert not (fncPath / "cache").exists() # Removed in 2.1b1
# Add a legacy folder # # Add a legacy folder
storage._runtimePath = fncPath # storage._runtimePath = fncPath
dataDir = fncPath / "data_0" # dataDir = fncPath / "data_0"
dataDir.mkdir() # dataDir.mkdir()
assert storage._prepareStorage(checkLegacy=True) is True # assert storage._prepareStorage(checkLegacy=True) is True
assert not dataDir.exists() # assert not dataDir.exists()
# We cannot add a new project here # # We cannot add a new project here
storage._runtimePath = fncPath # storage._runtimePath = fncPath
assert storage._prepareStorage(checkLegacy=False, newProject=True) is False # assert storage._prepareStorage(checkLegacy=False, newProject=True) is False
# END Test testCoreStorage_PrepareStorage # # END Test testCoreStorage_PrepareStorage
@pytest.mark.core @pytest.mark.core
@@ -261,7 +346,8 @@ def testCoreStorage_LegacyDataFolder(monkeypatch, fncPath):
storage = NWStorage(project) # type: ignore storage = NWStorage(project) # type: ignore
assert storage.isOpen() is False assert storage.isOpen() is False
storage._runtimePath = fncPath storage._runtimePath = fncPath
assert storage._prepareStorage() is True (fncPath / nwFiles.PROJ_FILE).touch()
storage.initProjectStorage(fncPath)
legacy = _LegacyStorage(project) # type: ignore legacy = _LegacyStorage(project) # type: ignore
data = [] data = []
@@ -290,8 +376,8 @@ def testCoreStorage_LegacyDataFolder(monkeypatch, fncPath):
legacy.legacyDataFolder(fncPath, data[i]) legacy.legacyDataFolder(fncPath, data[i])
# Files form 0 to 8 should now be in content # Files form 0 to 8 should now be in content
for c in "012345678": for i in range(9):
assert (fncPath / "content" / f"{c}00000000000{c}.nwd").exists() assert (fncPath / "content" / f"{i}00000000000{i}.nwd").exists()
# Folders 0 to 6 should be deleted # Folders 0 to 6 should be deleted
for i in range(7): for i in range(7):
@@ -314,7 +400,7 @@ def testCoreStorage_LegacyDataFolder(monkeypatch, fncPath):
assert not (fncPath / "content" / "9000000000009.nwd").exists() assert not (fncPath / "content" / "9000000000009.nwd").exists()
# Run the remaining through the prepare storage call # Run the remaining through the prepare storage call
assert storage._prepareStorage(checkLegacy=True) is True assert storage.initProjectStorage(fncPath, clearLock=True) == NWStorageOpen.READY
for c in "0123456789abcdef": for c in "0123456789abcdef":
assert (fncPath / "content" / f"{c}00000000000{c}.nwd").exists() assert (fncPath / "content" / f"{c}00000000000{c}.nwd").exists()
@@ -328,7 +414,8 @@ def testCoreStorage_DeprecatedFiles(monkeypatch, fncPath):
storage = NWStorage(project) # type: ignore storage = NWStorage(project) # type: ignore
assert storage.isOpen() is False assert storage.isOpen() is False
storage._runtimePath = fncPath storage._runtimePath = fncPath
assert storage._prepareStorage() is True (fncPath / nwFiles.PROJ_FILE).touch()
storage.initProjectStorage(fncPath)
legacy = _LegacyStorage(project) # type: ignore legacy = _LegacyStorage(project) # type: ignore
# Files/Folders to be Deleted or Renamed # Files/Folders to be Deleted or Renamed
+1 -1
View File
@@ -171,7 +171,7 @@ def buildTestProject(obj, projPath):
nwGUI = obj nwGUI = obj
project = SHARED.project project = SHARED.project
project.storage.openProjectInPlace(projPath) project.storage.createNewProject(projPath)
project.setDefaultStatusImport() project.setDefaultStatusImport()
project.data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed") project.data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")