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