Refactor Storage class (#1635)
This commit is contained in:
@@ -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")
|
||||
|
||||
+37
-36
@@ -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,21 @@ 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.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 +256,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(
|
||||
@@ -302,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()
|
||||
)
|
||||
@@ -323,9 +326,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 +373,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 +421,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))
|
||||
|
||||
@@ -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
|
||||
|
||||
+166
-137
@@ -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,17 @@ if TYPE_CHECKING: # pragma: no cover
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWStorageOpen(Enum):
|
||||
|
||||
UNKOWN = 0
|
||||
NOT_FOUND = 1
|
||||
LOCKED = 2
|
||||
FAILED = 3
|
||||
READY = 4
|
||||
|
||||
# END Enum NWStorageOpen
|
||||
|
||||
|
||||
class NWStorage:
|
||||
"""Core: Project Storage Class
|
||||
|
||||
@@ -60,7 +72,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 +84,7 @@ class NWStorage:
|
||||
self._runtimePath = None
|
||||
self._lockFilePath = None
|
||||
self._openMode = self.MODE_INACTIVE
|
||||
self._ready = False
|
||||
return
|
||||
|
||||
##
|
||||
@@ -77,12 +93,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,26 +113,31 @@ 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
|
||||
##
|
||||
|
||||
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 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 +145,91 @@ 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:
|
||||
self._exception = 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 a novelWriter project location."""
|
||||
inPath = Path(path).resolve()
|
||||
|
||||
# Initialise Storage Instance
|
||||
# ===========================
|
||||
|
||||
# Check what we're opening. Only two options are allowed:
|
||||
# 1. A folder with an nwProject.nwx file in it (not home)
|
||||
# 2. A full path to an nwProject.nwx file
|
||||
if inPath.is_dir() and inPath != Path.home().resolve():
|
||||
nwxFile = inPath / nwFiles.PROJ_FILE
|
||||
elif inPath.is_file() and inPath.name == nwFiles.PROJ_FILE:
|
||||
nwxFile = inPath
|
||||
else:
|
||||
logger.error("Not a novelWriter project")
|
||||
return NWStorageOpen.UNKOWN
|
||||
|
||||
if not nwxFile.exists():
|
||||
# The .nwx file must exist to continue
|
||||
logger.error("Not found: %s", nwxFile)
|
||||
return NWStorageOpen.NOT_FOUND
|
||||
|
||||
nwxPath = nwxFile.parent
|
||||
|
||||
self._storagePath = nwxPath
|
||||
self._runtimePath = nwxPath
|
||||
self._lockFilePath = nwxPath / nwFiles.PROJ_LOCK
|
||||
self._openMode = self.MODE_INPLACE
|
||||
|
||||
# Check Project Lock
|
||||
# ==================
|
||||
|
||||
if clearLock:
|
||||
self._clearLockFile()
|
||||
|
||||
self._readLockFile()
|
||||
if self._lockedBy:
|
||||
logger.error("Project is locked, so not opening")
|
||||
return NWStorageOpen.LOCKED
|
||||
else:
|
||||
logger.debug("Project is not locked")
|
||||
|
||||
# Prepare Folder
|
||||
# ==============
|
||||
|
||||
basePath = self._runtimePath
|
||||
metaPath = basePath / "meta"
|
||||
contPath = basePath / "content"
|
||||
try:
|
||||
metaPath.mkdir(exist_ok=True)
|
||||
contPath.mkdir(exist_ok=True)
|
||||
except Exception as exc:
|
||||
logger.error("Failed to create project folders", exc_info=exc)
|
||||
self.clear()
|
||||
return NWStorageOpen.FAILED
|
||||
|
||||
# Check for legacy data folders
|
||||
legacy = _LegacyStorage(self._project)
|
||||
legacy.deprecatedFiles(basePath)
|
||||
for child in basePath.iterdir():
|
||||
if child.is_dir() and child.name.startswith("data_"):
|
||||
legacy.legacyDataFolder(basePath, child)
|
||||
|
||||
self._writeLockFile()
|
||||
self._ready = True
|
||||
|
||||
return NWStorageOpen.READY
|
||||
|
||||
def runPostSaveTasks(self, autoSave: bool = False) -> bool: # pragma: no cover
|
||||
"""Run tasks after the project has been saved.
|
||||
@@ -147,7 +242,7 @@ class NWStorage:
|
||||
|
||||
def closeSession(self) -> None:
|
||||
"""Run tasks related to closing the session."""
|
||||
self.clearLockFile()
|
||||
self._clearLockFile()
|
||||
self.clear()
|
||||
return
|
||||
|
||||
@@ -157,26 +252,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 +284,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 +320,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 +330,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 +380,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 +389,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 +442,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 +489,6 @@ class _LegacyStorage:
|
||||
|
||||
# Save dictionary and clean up old file
|
||||
userDict.save()
|
||||
assert wordJson.exists()
|
||||
wordList.unlink()
|
||||
|
||||
except Exception:
|
||||
@@ -467,9 +498,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 +537,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,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
|
||||
|
||||
@@ -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,103 @@ 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.openProjectInPlace", lambda *a, **k: False)
|
||||
assert theProject.openProject(fncPath) is False
|
||||
# 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
|
||||
|
||||
# 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)
|
||||
# No project file
|
||||
caplog.clear()
|
||||
assert project.openProject(fncPath) is False
|
||||
assert "Project file not found." in caplog.text
|
||||
fooBar.unlink()
|
||||
|
||||
# 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()
|
||||
# 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()
|
||||
|
||||
# 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
|
||||
# Create test project
|
||||
buildTestProject(project, fncPath)
|
||||
|
||||
# Open successfully
|
||||
assert project.openProject(fncPath) is True
|
||||
|
||||
# Open again should fail on lock file
|
||||
assert project.openProject(fncPath) is False
|
||||
assert project.state == NWProjectState.LOCKED
|
||||
|
||||
# Force re-open
|
||||
assert project.openProject(fncPath, clearLock=True) is True
|
||||
assert project.state == NWProjectState.READY
|
||||
|
||||
# Fail getting xml reader
|
||||
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, 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 theProject.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 theProject.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 theProject.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 theProject.openProject(fncPath) is False
|
||||
assert project.openProject(fncPath, clearLock=True) is False
|
||||
assert "The file format of your project is about to be" in SHARED.lastAlert
|
||||
|
||||
# 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, 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 theProject.openProject(fncPath) is True
|
||||
|
||||
theProject.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)
|
||||
theProject.index._indexBroken = True
|
||||
assert theProject.openProject(fncPath) is True
|
||||
project.index._indexBroken = True
|
||||
assert project.openProject(fncPath, clearLock=True) 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 +271,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 +300,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 +329,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 +358,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 +366,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 +376,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 +393,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 +430,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 +475,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 +577,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 +593,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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.openProjectInPlace(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.openProjectInPlace(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.openProjectInPlace(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.openProjectInPlace(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"<NWDocument handle={C.hSceneDoc}>"
|
||||
|
||||
project.closeProject()
|
||||
|
||||
# END Test testCoreStorage_InitProjectStorage
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreStorage_InitProjectStorage_Invalid(mockGUI, fncPath):
|
||||
"""Test initialising a project in an invalid folder."""
|
||||
project = NWProject()
|
||||
|
||||
# Create instance
|
||||
storage = NWStorage(project)
|
||||
|
||||
# Check defaults
|
||||
assert storage.storagePath is None
|
||||
assert storage.runtimePath is None
|
||||
assert storage.contentPath is None
|
||||
assert storage._openMode == NWStorage.MODE_INACTIVE
|
||||
assert storage._ready is False
|
||||
|
||||
# Check closed project return values
|
||||
assert storage.isOpen() is False
|
||||
assert storage.getXmlReader() is None
|
||||
assert storage.getXmlWriter() is None
|
||||
assert bool(storage.getDocument(C.hSceneDoc)) is False
|
||||
assert storage.getMetaFile("file") is None
|
||||
assert storage.scanContent() == []
|
||||
|
||||
# Populate folder with invalid files
|
||||
(fncPath / "meta").touch() # These are now files but should be folders
|
||||
(fncPath / "content").touch() # These are now files but should be folders
|
||||
|
||||
# Try opening the folder, but there is no project file
|
||||
assert storage.initProjectStorage(fncPath) == NWStorageOpen.NOT_FOUND
|
||||
|
||||
# Add the project file, and we should now fail on the folders
|
||||
(fncPath / nwFiles.PROJ_FILE).touch()
|
||||
assert storage.initProjectStorage(fncPath) == NWStorageOpen.FAILED
|
||||
|
||||
project.closeProject()
|
||||
|
||||
# END Test testCoreStorage_InitProjectStorage_Invalid
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
@@ -123,54 +207,55 @@ 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() is None
|
||||
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() == []
|
||||
storage._readLockFile()
|
||||
assert storage.lockStatus is None
|
||||
|
||||
# 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"]
|
||||
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:
|
||||
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
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user