# -*- coding: utf-8 -*-
"""novelWriter Project Class Tester
"""
import pytest
import os
from shutil import copyfile
from zipfile import ZipFile
from lxml import etree
from tools import cmpFiles, writeFile, readFile
from dummy import causeOSError
from nw.core.project import NWProject
from nw.constants import nwItemClass, nwItemType, nwItemLayout, nwFiles
from nw.common import formatTimeStamp
@pytest.mark.core
def testCoreProject_NewMinimal(fncDir, outDir, refDir, tmpDir, dummyGUI):
"""Create a new project from a project wizard dictionary. With
default setting, creating a Minimal project.
"""
projFile = os.path.join(fncDir, "nwProject.nwx")
testFile = os.path.join(outDir, "coreProject_NewMinimal_nwProject.nwx")
compFile = os.path.join(refDir, "coreProject_NewMinimal_nwProject.nwx")
theProject = NWProject(dummyGUI)
theProject.projTree.setSeed(42)
# Setting no data should fail
assert not theProject.newProject({})
# Try again with a proper path
assert theProject.newProject({"projPath": fncDir})
assert theProject.saveProject()
assert theProject.closeProject()
# Creating the project once more should fail
assert not theProject.newProject({"projPath": fncDir})
# Check the new project
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
# Open again
assert theProject.openProject(projFile)
# Save and close
assert theProject.saveProject()
assert theProject.closeProject()
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
assert not theProject.projChanged
# Open a second time
assert theProject.openProject(projFile)
assert not theProject.openProject(projFile)
assert theProject.openProject(projFile, overrideLock=True)
assert theProject.saveProject()
assert theProject.closeProject()
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
# END Test testCoreProject_NewMinimal
@pytest.mark.core
def testCoreProject_NewCustomA(fncDir, outDir, refDir, dummyGUI):
"""Create a new project from a project wizard dictionary.
Custom type with chapters and scenes.
"""
projFile = os.path.join(fncDir, "nwProject.nwx")
testFile = os.path.join(outDir, "coreProject_NewCustomA_nwProject.nwx")
compFile = os.path.join(refDir, "coreProject_NewCustomA_nwProject.nwx")
projData = {
"projName": "Test Custom",
"projTitle": "Test Novel",
"projAuthors": "Jane Doe\nJohn Doh\n",
"projPath": fncDir,
"popSample": False,
"popMinimal": False,
"popCustom": True,
"addRoots": [
nwItemClass.PLOT,
nwItemClass.CHARACTER,
nwItemClass.WORLD,
nwItemClass.TIMELINE,
nwItemClass.OBJECT,
nwItemClass.ENTITY,
],
"numChapters": 3,
"numScenes": 3,
"chFolders": True,
}
theProject = NWProject(dummyGUI)
theProject.projTree.setSeed(42)
assert theProject.newProject(projData)
assert theProject.saveProject()
assert theProject.closeProject()
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
# END Test testCoreProject_NewCustomA
@pytest.mark.core
def testCoreProject_NewCustomB(fncDir, outDir, refDir, dummyGUI):
"""Create a new project from a project wizard dictionary.
Custom type without chapters, but with scenes.
"""
projFile = os.path.join(fncDir, "nwProject.nwx")
testFile = os.path.join(outDir, "coreProject_NewCustomB_nwProject.nwx")
compFile = os.path.join(refDir, "coreProject_NewCustomB_nwProject.nwx")
projData = {
"projName": "Test Custom",
"projTitle": "Test Novel",
"projAuthors": "Jane Doe\nJohn Doh\n",
"projPath": fncDir,
"popSample": False,
"popMinimal": False,
"popCustom": True,
"addRoots": [
nwItemClass.PLOT,
nwItemClass.CHARACTER,
nwItemClass.WORLD,
nwItemClass.TIMELINE,
nwItemClass.OBJECT,
nwItemClass.ENTITY,
],
"numChapters": 0,
"numScenes": 6,
"chFolders": True,
}
theProject = NWProject(dummyGUI)
theProject.projTree.setSeed(42)
assert theProject.newProject(projData)
assert theProject.saveProject()
assert theProject.closeProject()
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
# END Test testCoreProject_NewCustomB
@pytest.mark.core
def testCoreProject_NewSampleA(fncDir, tmpConf, dummyGUI, tmpDir):
"""Check that we can create a new project can be created from the
provided sample project via a zip file.
"""
projData = {
"projName": "Test Sample",
"projTitle": "Test Novel",
"projAuthors": "Jane Doe\nJohn Doh\n",
"projPath": fncDir,
"popSample": True,
"popMinimal": False,
"popCustom": False,
}
theProject = NWProject(dummyGUI)
theProject.projTree.setSeed(42)
theProject.mainConf = tmpConf
# Sample set, but no path
assert not theProject.newProject({"popSample": True})
# Force the lookup path for assets to our temp folder
srcSample = os.path.abspath(os.path.join(tmpConf.appRoot, "sample"))
dstSample = os.path.join(tmpDir, "sample.zip")
tmpConf.assetPath = tmpDir
# Create and open a defective zip file
with open(dstSample, mode="w+") as outFile:
outFile.write("foo")
assert not theProject.newProject(projData)
os.unlink(dstSample)
# Create a real zip file, and unpack it
with ZipFile(dstSample, "w") as zipObj:
zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx")
for docFile in os.listdir(os.path.join(srcSample, "content")):
srcDoc = os.path.join(srcSample, "content", docFile)
zipObj.write(srcDoc, "content/"+docFile)
assert theProject.newProject(projData)
assert theProject.openProject(fncDir)
assert theProject.projName == "Sample Project"
assert theProject.saveProject()
assert theProject.closeProject()
os.unlink(dstSample)
# END Test testCoreProject_NewSampleA
@pytest.mark.core
def testCoreProject_NewSampleB(monkeypatch, fncDir, tmpConf, dummyGUI, tmpDir):
"""Check that we can create a new project can be created from the
provided sample project folder.
"""
projData = {
"projName": "Test Sample",
"projTitle": "Test Novel",
"projAuthors": "Jane Doe\nJohn Doh\n",
"projPath": fncDir,
"popSample": True,
"popMinimal": False,
"popCustom": False,
}
theProject = NWProject(dummyGUI)
theProject.projTree.setSeed(42)
theProject.mainConf = tmpConf
# Make sure we do not pick up the nw/assets/sample.zip file
tmpConf.assetPath = tmpDir
# Set a fake project file name
monkeypatch.setattr(nwFiles, "PROJ_FILE", "nothing.nwx")
assert not theProject.newProject(projData)
monkeypatch.setattr(nwFiles, "PROJ_FILE", "nwProject.nwx")
assert theProject.newProject(projData)
assert theProject.openProject(fncDir)
assert theProject.projName == "Sample Project"
assert theProject.saveProject()
assert theProject.closeProject()
# Misdirect the appRoot path so neither is possible
tmpConf.appRoot = tmpDir
assert not theProject.newProject(projData)
# END Test testCoreProject_NewSampleB
@pytest.mark.core
def testCoreProject_NewRoot(fncDir, outDir, refDir, dummyGUI):
"""Check that new root folders can be added to the project.
"""
projFile = os.path.join(fncDir, "nwProject.nwx")
testFile = os.path.join(outDir, "coreProject_NewRoot_nwProject.nwx")
compFile = os.path.join(refDir, "coreProject_NewRoot_nwProject.nwx")
theProject = NWProject(dummyGUI)
theProject.projTree.setSeed(42)
assert theProject.newProject({"projPath": fncDir})
assert theProject.setProjectPath(fncDir)
assert theProject.saveProject()
assert theProject.closeProject()
assert theProject.openProject(projFile)
assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), type(None))
assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), type(None))
assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), type(None))
assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), type(None))
assert isinstance(theProject.newRoot("Timeline", nwItemClass.TIMELINE), str)
assert isinstance(theProject.newRoot("Object", nwItemClass.OBJECT), str)
assert isinstance(theProject.newRoot("Custom1", nwItemClass.CUSTOM), str)
assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str)
assert theProject.projChanged
assert theProject.saveProject()
assert theProject.closeProject()
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
assert not theProject.projChanged
# END Test testCoreProject_NewRoot
@pytest.mark.core
def testCoreProject_NewFile(fncDir, outDir, refDir, dummyGUI):
"""Check that new files can be added to the project.
"""
projFile = os.path.join(fncDir, "nwProject.nwx")
testFile = os.path.join(outDir, "coreProject_NewFile_nwProject.nwx")
compFile = os.path.join(refDir, "coreProject_NewFile_nwProject.nwx")
theProject = NWProject(dummyGUI)
theProject.projTree.setSeed(42)
assert theProject.newProject({"projPath": fncDir})
assert theProject.setProjectPath(fncDir)
assert theProject.saveProject()
assert theProject.closeProject()
assert theProject.openProject(projFile)
assert isinstance(theProject.newFile("Hello", nwItemClass.NOVEL, "31489056e0916"), str)
assert isinstance(theProject.newFile("Jane", nwItemClass.CHARACTER, "71ee45a3c0db9"), str)
assert theProject.projChanged
assert theProject.saveProject()
assert theProject.closeProject()
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
assert not theProject.projChanged
# END Test testCoreProject_NewFile
@pytest.mark.core
def testCoreProject_Open(monkeypatch, nwMinimal, dummyGUI):
"""Test opening a project.
"""
theProject = NWProject(dummyGUI)
# Rename the project file to check handling
rName = os.path.join(nwMinimal, nwFiles.PROJ_FILE)
wName = os.path.join(nwMinimal, nwFiles.PROJ_FILE+"_sdfghj")
os.rename(rName, wName)
assert theProject.openProject(nwMinimal) is False
os.rename(wName, rName)
# Fail on folder structure check
monkeypatch.setattr("os.mkdir", causeOSError)
assert theProject.openProject(nwMinimal) is False
monkeypatch.undo()
# Fail on lock file
theProject.setProjectPath(nwMinimal)
assert theProject._writeLockFile()
assert theProject.openProject(nwMinimal) is False
# Fail to read lockfile (which still opens the project)
monkeypatch.setattr("builtins.open", causeOSError)
assert theProject.openProject(nwMinimal) is True
monkeypatch.undo()
assert theProject.closeProject()
# Force open with lockfile
theProject.setProjectPath(nwMinimal)
assert theProject._writeLockFile()
assert theProject.openProject(nwMinimal, overrideLock=True) is True
assert theProject.closeProject()
# Make a junk XML file
oName = os.path.join(nwMinimal, nwFiles.PROJ_FILE[:-3]+"orig")
bName = os.path.join(nwMinimal, nwFiles.PROJ_FILE[:-3]+"bak")
os.rename(rName, oName)
writeFile(rName, "dummy")
assert theProject.openProject(nwMinimal) is False
# Also write a jun XML backup file
writeFile(bName, "dummy")
assert theProject.openProject(nwMinimal) is False
# Wrong root item
writeFile(rName, "\n")
assert theProject.openProject(nwMinimal) is False
# Wrong file version
writeFile(rName, (
"\n"
"\n"
"\n"
))
dummyGUI.askResponse = False
assert theProject.openProject(nwMinimal) is False
dummyGUI.undo()
# Future file version
writeFile(rName, (
"\n"
"\n"
"\n"
))
assert theProject.openProject(nwMinimal) is False
# Larger hex version
writeFile(rName, (
"\n"
"\n"
"\n"
))
dummyGUI.askResponse = False
assert theProject.openProject(nwMinimal) is False
dummyGUI.undo()
# Test skipping XML entries
writeFile(rName, (
"\n"
"\n"
"\n"
"\n"
"\n"
))
assert theProject.openProject(nwMinimal) is True
assert theProject.closeProject()
# Test deprecated XML entries
writeFile(rName, (
"\n"
"\n"
"\n"
"\n"
"B\n"
"\n"
"\n"
"\n"
))
assert theProject.openProject(nwMinimal) is True
assert theProject.autoReplace == {"A": "B"}
assert theProject.closeProject()
# Clean up XML files
os.unlink(rName)
os.unlink(bName)
os.rename(oName, rName)
# Add some legacy stuff that cannot be removed
writeFile(os.path.join(nwMinimal, "junk"), "dummy")
os.mkdir(os.path.join(nwMinimal, "data_0"))
writeFile(os.path.join(nwMinimal, "data_0", "junk"), "dummy")
dummyGUI.clear()
assert theProject.openProject(nwMinimal) is True
assert "data_0" in dummyGUI.lastAlert
assert theProject.closeProject()
# END Test testCoreProject_Open
@pytest.mark.core
def testCoreProject_Save(monkeypatch, nwMinimal, dummyGUI, refDir):
"""Test saving a project.
"""
theProject = NWProject(dummyGUI)
testFile = os.path.join(nwMinimal, "nwProject.nwx")
compFile = os.path.join(refDir, os.path.pardir, "minimal", "nwProject.nwx")
# Nothing to save
assert theProject.saveProject() is False
# Open test project
assert theProject.openProject(nwMinimal)
# Fail on folder structure check
monkeypatch.setattr("os.path.isdir", lambda *args: False)
assert theProject.saveProject() is False
monkeypatch.undo()
# Fail on open file
monkeypatch.setattr("builtins.open", causeOSError)
assert theProject.saveProject() is False
monkeypatch.undo()
# Successful save
saveCount = theProject.saveCount
autoCount = theProject.autoCount
assert theProject.saveProject() is True
assert theProject.saveCount == saveCount + 1
assert theProject.autoCount == autoCount
assert cmpFiles(testFile, compFile, [2, 6, 7, 8, 9])
# Successful autosave
saveCount = theProject.saveCount
autoCount = theProject.autoCount
assert theProject.saveProject(autoSave=True) is True
assert theProject.saveCount == saveCount
assert theProject.autoCount == autoCount + 1
assert cmpFiles(testFile, compFile, [2, 6, 7, 8, 9])
# Close test project
assert theProject.closeProject()
# END Test testCoreProject_Save
@pytest.mark.core
def testCoreProject_LockFile(monkeypatch, fncDir, dummyGUI):
"""Test lock file functions for the project folder.
"""
theProject = NWProject(dummyGUI)
lockFile = os.path.join(fncDir, nwFiles.PROJ_LOCK)
# No project
assert theProject._writeLockFile() is False
assert theProject._readLockFile() == ["ERROR"]
assert theProject._clearLockFile() is False
theProject.projPath = fncDir
theProject.mainConf.hostName = "TestHost"
theProject.mainConf.osType = "TestOS"
theProject.mainConf.kernelVer = "1.0"
# Block open
monkeypatch.setattr("builtins.open", causeOSError)
assert theProject._writeLockFile() is False
monkeypatch.undo()
# Write lock file
monkeypatch.setattr("nw.core.project.time", lambda: 123.4)
assert theProject._writeLockFile() is True
monkeypatch.undo()
assert readFile(lockFile) == "TestHost\nTestOS\n1.0\n123\n"
# Block open
monkeypatch.setattr("builtins.open", causeOSError)
assert theProject._readLockFile() == ["ERROR"]
monkeypatch.undo()
# Read lock file
assert theProject._readLockFile() == ["TestHost", "TestOS", "1.0", "123"]
# Block unlink
monkeypatch.setattr("os.unlink", causeOSError)
assert os.path.isfile(lockFile)
assert theProject._clearLockFile() is False
assert os.path.isfile(lockFile)
monkeypatch.undo()
# Clear file
assert os.path.isfile(lockFile)
assert theProject._clearLockFile() is True
assert not os.path.isfile(lockFile)
# Read again, no file
assert theProject._readLockFile() == []
# Read an invalid lock file
writeFile(lockFile, "A\nB")
assert theProject._readLockFile() == ["ERROR"]
assert theProject._clearLockFile() is True
# END Test testCoreProject_LockFile
@pytest.mark.core
def testCoreProject_Helpers(monkeypatch, fncDir, dummyGUI):
"""Test helper functions for the project folder.
"""
theProject = NWProject(dummyGUI)
# No path
assert theProject.ensureFolderStructure() is False
# Set the correct dir
theProject.projPath = fncDir
# Block user's home folder
monkeypatch.setattr("os.path.expanduser", lambda *args, **kwargs: fncDir)
assert theProject.ensureFolderStructure() is False
monkeypatch.undo()
# Create a file to block meta folder
metaDir = os.path.join(fncDir, "meta")
writeFile(metaDir, "dummy")
assert theProject.ensureFolderStructure() is False
os.unlink(metaDir)
# Create a file to block cache folder
cacheDir = os.path.join(fncDir, "cache")
writeFile(cacheDir, "dummy")
assert theProject.ensureFolderStructure() is False
os.unlink(cacheDir)
# Create a file to block content folder
contentDir = os.path.join(fncDir, "content")
writeFile(contentDir, "dummy")
assert theProject.ensureFolderStructure() is False
os.unlink(contentDir)
# Now, do it right
assert theProject.ensureFolderStructure() is True
assert os.path.isdir(metaDir)
assert os.path.isdir(cacheDir)
assert os.path.isdir(contentDir)
# END Test testCoreProject_Helpers
@pytest.mark.core
def testCoreProject_AccessItems(nwMinimal, dummyGUI):
"""Test helper functions for the project folder.
"""
theProject = NWProject(dummyGUI)
theProject.openProject(nwMinimal)
# Move Novel ROOT to after its files
oldOrder = [
"a508bb932959c", # ROOT: Novel
"a35baf2e93843", # FILE: Title Page
"a6d311a93600a", # FOLDER: New Chapter
"f5ab3e30151e1", # FILE: New Chapter
"8c659a11cd429", # FILE: New Scene
"7695ce551d265", # ROOT: Plot
"afb3043c7b2b3", # ROOT: Characters
"9d5247ab588e0", # ROOT: World
]
newOrder = [
"a35baf2e93843", # FILE: Title Page
"f5ab3e30151e1", # FILE: New Chapter
"8c659a11cd429", # FILE: New Scene
"a6d311a93600a", # FOLDER: New Chapter
"a508bb932959c", # ROOT: Novel
"7695ce551d265", # ROOT: Plot
"afb3043c7b2b3", # ROOT: Characters
"9d5247ab588e0", # ROOT: World
]
assert theProject.projTree.handles() == oldOrder
assert theProject.setTreeOrder(newOrder)
assert theProject.projTree.handles() == newOrder
# Add a non-existing item
theProject.projTree._treeOrder.append("01234567789abc")
# Add an item with a non-existent parent
nHandle = theProject.newFile("Test File", nwItemClass.NOVEL, "a6d311a93600a")
theProject.projTree[nHandle].setParent("cba9876543210")
assert theProject.projTree[nHandle].itemParent == "cba9876543210"
retOrder = []
for tItem in theProject.getProjectItems():
retOrder.append(tItem.itemHandle)
assert retOrder == [
"a508bb932959c", # ROOT: Novel
"7695ce551d265", # ROOT: Plot
"afb3043c7b2b3", # ROOT: Characters
"9d5247ab588e0", # ROOT: World
nHandle, # FILE: Test File
"a35baf2e93843", # FILE: Title Page
"a6d311a93600a", # FOLDER: New Chapter
"f5ab3e30151e1", # FILE: New Chapter
"8c659a11cd429", # FILE: New Scene
]
assert theProject.projTree[nHandle].itemParent is None
# END Test testCoreProject_AccessItems
@pytest.mark.core
def testCoreProject_Methods(monkeypatch, nwMinimal, dummyGUI, tmpDir):
"""Test other project class methods and functions.
"""
theProject = NWProject(dummyGUI)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwMinimal)
assert theProject.projPath == nwMinimal
# Setting project path
assert theProject.setProjectPath(None)
assert theProject.projPath is None
assert theProject.setProjectPath("")
assert theProject.projPath is None
assert theProject.setProjectPath("~")
assert theProject.projPath == os.path.expanduser("~")
# Create a new folder and populate it
projPath = os.path.join(nwMinimal, "dummy1")
assert theProject.setProjectPath(projPath, newProject=True)
# Make os.mkdir fail
monkeypatch.setattr("os.mkdir", causeOSError)
projPath = os.path.join(nwMinimal, "dummy2")
assert not theProject.setProjectPath(projPath, newProject=True)
# Set back
assert theProject.setProjectPath(nwMinimal)
# Project Name
assert theProject.setProjectName(" A Name ")
assert theProject.projName == "A Name"
# Project Title
assert theProject.setBookTitle(" A Title ")
assert theProject.bookTitle == "A Title"
# Project Authors
assert not theProject.setBookAuthors([])
assert theProject.setBookAuthors(" Jane Doe \n John Doh \n ")
assert theProject.bookAuthors == ["Jane Doe", "John Doh"]
# Trash folder
# Should create on first call, and just returned on later calls
assert theProject.projTree["73475cb40a568"] is None
assert theProject.trashFolder() == "73475cb40a568"
assert theProject.trashFolder() == "73475cb40a568"
# Project backup
assert theProject.doBackup is True
assert theProject.setProjBackup(False)
assert theProject.doBackup is False
assert not theProject.setProjBackup(True)
theProject.mainConf.backupPath = tmpDir
assert theProject.setProjBackup(True)
assert theProject.setProjectName("")
assert not theProject.setProjBackup(True)
assert theProject.setProjectName("A Name")
assert theProject.setProjBackup(True)
# Spell check
theProject.projChanged = False
assert theProject.setSpellCheck(True)
assert not theProject.setSpellCheck(False)
assert theProject.projChanged
# Spell language
theProject.projChanged = False
assert theProject.setSpellLang(None)
assert theProject.projLang is None
assert theProject.setSpellLang("None")
assert theProject.projLang is None
assert theProject.setSpellLang("en_GB")
assert theProject.projLang == "en_GB"
assert theProject.projChanged
# Automatic outline update
theProject.projChanged = False
assert theProject.setAutoOutline(True)
assert not theProject.setAutoOutline(False)
assert theProject.projChanged
# Last edited
theProject.projChanged = False
assert theProject.setLastEdited("0123456789abc")
assert theProject.lastEdited == "0123456789abc"
assert theProject.projChanged
# Last viewed
theProject.projChanged = False
assert theProject.setLastViewed("0123456789abc")
assert theProject.lastViewed == "0123456789abc"
assert theProject.projChanged
# Autoreplace
theProject.projChanged = False
assert theProject.setAutoReplace({"A": "B", "C": "D"})
assert theProject.autoReplace == {"A": "B", "C": "D"}
assert theProject.projChanged
# Change project tree order
oldOrder = [
"a508bb932959c", "a35baf2e93843", "a6d311a93600a",
"f5ab3e30151e1", "8c659a11cd429", "7695ce551d265",
"afb3043c7b2b3", "9d5247ab588e0", "73475cb40a568",
]
newOrder = [
"f5ab3e30151e1", "8c659a11cd429", "7695ce551d265",
"a508bb932959c", "a35baf2e93843", "a6d311a93600a",
"afb3043c7b2b3", "9d5247ab588e0",
]
assert theProject.projTree.handles() == oldOrder
assert theProject.setTreeOrder(newOrder)
assert theProject.projTree.handles() == newOrder
assert theProject.setTreeOrder(oldOrder)
assert theProject.projTree.handles() == oldOrder
# Change status
theProject.projTree["a35baf2e93843"].setStatus("Finished")
theProject.projTree["a6d311a93600a"].setStatus("Draft")
theProject.projTree["f5ab3e30151e1"].setStatus("Note")
theProject.projTree["8c659a11cd429"].setStatus("Finished")
newList = [
("New", 1, 1, 1, "New"),
("Draft", 2, 2, 2, "Note"), # These are swapped
("Note", 3, 3, 3, "Draft"), # These are swapped
("Edited", 4, 4, 4, "Finished"), # Renamed
("Finished", 5, 5, 5, None), # New, with reused name
]
assert theProject.setStatusColours(newList)
assert theProject.statusItems._theLabels == [
"New", "Draft", "Note", "Edited", "Finished"
]
assert theProject.statusItems._theColours == [
(1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)
]
assert theProject.projTree["a35baf2e93843"].itemStatus == "Edited" # Renamed
assert theProject.projTree["a6d311a93600a"].itemStatus == "Note" # Swapped
assert theProject.projTree["f5ab3e30151e1"].itemStatus == "Draft" # Swapped
assert theProject.projTree["8c659a11cd429"].itemStatus == "Edited" # Renamed
# Change importance
fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "afb3043c7b2b3")
theProject.projTree[fHandle].setStatus("Main")
newList = [
("New", 1, 1, 1, "New"),
("Minor", 2, 2, 2, "Minor"),
("Major", 3, 3, 3, "Major"),
("Min", 4, 4, 4, "Main"),
("Max", 5, 5, 5, None),
]
assert theProject.setImportColours(newList)
assert theProject.importItems._theLabels == [
"New", "Minor", "Major", "Min", "Max"
]
assert theProject.importItems._theColours == [
(1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)
]
assert theProject.projTree[fHandle].itemStatus == "Min"
# Check status counts
assert theProject.statusItems._theCounts == [0, 0, 0, 0, 0]
assert theProject.importItems._theCounts == [0, 0, 0, 0, 0]
theProject.countStatus()
assert theProject.statusItems._theCounts == [1, 1, 1, 2, 0]
assert theProject.importItems._theCounts == [3, 0, 0, 1, 0]
# Check word counts
theProject.currWCount = 200
theProject.lastWCount = 100
assert theProject.getSessionWordCount() == 100
# Session stats
monkeypatch.setattr("os.path.isdir", lambda *args, **kwargs: False)
assert not theProject._appendSessionStats()
monkeypatch.undo()
# Block open
monkeypatch.setattr("builtins.open", causeOSError)
assert not theProject._appendSessionStats()
monkeypatch.undo()
# Write entry
assert theProject.projMeta == os.path.join(nwMinimal, "meta")
statsFile = os.path.join(theProject.projMeta, nwFiles.SESS_STATS)
theProject.projOpened = 1600002000
theProject.novelWCount = 200
theProject.notesWCount = 100
monkeypatch.setattr("nw.core.project.time", lambda: 1600005600)
assert theProject._appendSessionStats()
monkeypatch.undo()
assert readFile(statsFile) == (
"# Offset 100\n"
"# Start Time End Time Novel Notes\n"
"%s %s 200 100\n"
) % (formatTimeStamp(1600002000), formatTimeStamp(1600005600))
# Pack XML Value
xElem = etree.Element("element")
theProject._packProjectValue(xElem, "A", "B", allowNone=False)
assert etree.tostring(xElem, pretty_print=False, encoding="utf-8") == (
b"B"
)
xElem = etree.Element("element")
theProject._packProjectValue(xElem, "A", "", allowNone=False)
assert etree.tostring(xElem, pretty_print=False, encoding="utf-8") == (
b""
)
# Pack XML Key/Value
xElem = etree.Element("element")
theProject._packProjectKeyValue(xElem, "item", {"A": "B", "C": "D"})
assert etree.tostring(xElem, pretty_print=False, encoding="utf-8") == (
b""
b"- "
b"B"
b"D"
b"
"
b""
)
# END Test testCoreProject_Methods
@pytest.mark.core
def testCoreProject_OrphanedFiles(dummyGUI, nwLipsum):
"""Check that files in the content folder that are not tracked in
the project XML file are handled correctly by the orphaned files
function. It should also restore as much meta data as possible from
the meta line at the top of the document file.
"""
theProject = NWProject(dummyGUI)
assert theProject.openProject(nwLipsum)
assert theProject.projTree["636b6aa9b697b"] is None
assert theProject.closeProject()
# First Item with Meta Data
orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd")
with open(orphPath, mode="w", encoding="utf8") as outFile:
outFile.write("%%~name:Mars\n")
outFile.write("%%~path:5eaea4e8cdee8/636b6aa9b697b\n")
outFile.write("%%~kind:WORLD/NOTE\n")
outFile.write("%%~invalid\n")
outFile.write("\n")
# Second Item without Meta Data
orphPath = os.path.join(nwLipsum, "content", "736b6aa9b697b.nwd")
with open(orphPath, mode="w", encoding="utf8") as outFile:
outFile.write("\n")
# Invalid File Name
dummyPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.txt")
with open(dummyPath, mode="w", encoding="utf8") as outFile:
outFile.write("\n")
# Invalid File Name
dummyPath = os.path.join(nwLipsum, "content", "636b6aa9b697bb.nwd")
with open(dummyPath, mode="w", encoding="utf8") as outFile:
outFile.write("\n")
# Invalid File Name
dummyPath = os.path.join(nwLipsum, "content", "abcdefghijklm.nwd")
with open(dummyPath, mode="w", encoding="utf8") as outFile:
outFile.write("\n")
assert theProject.openProject(nwLipsum)
assert theProject.projPath is not None
assert theProject.projTree["636b6aa9b697bb"] is None
assert theProject.projTree["abcdefghijklm"] is None
# First Item with Meta Data
oItem = theProject.projTree["636b6aa9b697b"]
assert oItem is not None
assert oItem.itemName == "Mars"
assert oItem.itemHandle == "636b6aa9b697b"
assert oItem.itemParent is None
assert oItem.itemClass == nwItemClass.WORLD
assert oItem.itemType == nwItemType.FILE
assert oItem.itemLayout == nwItemLayout.NOTE
# Second Item without Meta Data
oItem = theProject.projTree["736b6aa9b697b"]
assert oItem is not None
assert oItem.itemName == "Orphaned File 1"
assert oItem.itemHandle == "736b6aa9b697b"
assert oItem.itemParent is None
assert oItem.itemClass == nwItemClass.NO_CLASS
assert oItem.itemType == nwItemType.FILE
assert oItem.itemLayout == nwItemLayout.NO_LAYOUT
assert theProject.saveProject(nwLipsum)
assert theProject.closeProject()
# Finally, check that the orphaned files function returns
# if no project is open and no path is set
assert not theProject._scanProjectFolder()
# END Test testCoreProject_OrphanedFiles
@pytest.mark.core
def testCoreProject_OldFormat(dummyGUI, nwOldProj):
"""Test that a project folder structure of version 1.0 can be
converted to the latest folder structure. Version 1.0 split the
documents into 'data_0' ... 'data_f' folders, which are now all
contained in a single 'content' folder.
"""
theProject = NWProject(dummyGUI)
# Create dummy files for known legacy files
deleteFiles = [
os.path.join(nwOldProj, "cache", "nwProject.nwx.0"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.1"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.2"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.3"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.4"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.5"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.6"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.7"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.8"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.9"),
os.path.join(nwOldProj, "meta", "mainOptions.json"),
os.path.join(nwOldProj, "meta", "exportOptions.json"),
os.path.join(nwOldProj, "meta", "outlineOptions.json"),
os.path.join(nwOldProj, "meta", "timelineOptions.json"),
os.path.join(nwOldProj, "meta", "docMergeOptions.json"),
os.path.join(nwOldProj, "meta", "sessionLogOptions.json"),
]
# Add some files that shouldn't be there
deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.nwd"))
deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.txt"))
# Add some folders that shouldn't be there
os.mkdir(os.path.join(nwOldProj, "stuff"))
os.mkdir(os.path.join(nwOldProj, "data_1", "stuff"))
# Create dummy files
os.mkdir(os.path.join(nwOldProj, "cache"))
for aFile in deleteFiles:
writeFile(aFile, "Hi")
for aFile in deleteFiles:
assert os.path.isfile(aFile)
# Open project and check that files that are not supposed to be
# there have been removed
assert theProject.openProject(nwOldProj)
for aFile in deleteFiles:
assert not os.path.isfile(aFile)
assert not os.path.isdir(os.path.join(nwOldProj, "data_1", "stuff"))
assert not os.path.isdir(os.path.join(nwOldProj, "data_1"))
assert not os.path.isdir(os.path.join(nwOldProj, "data_7"))
assert not os.path.isdir(os.path.join(nwOldProj, "data_8"))
assert not os.path.isdir(os.path.join(nwOldProj, "data_9"))
assert not os.path.isdir(os.path.join(nwOldProj, "data_a"))
assert not os.path.isdir(os.path.join(nwOldProj, "data_f"))
# Check stuff that has been moved
assert os.path.isdir(os.path.join(nwOldProj, "junk"))
assert os.path.isdir(os.path.join(nwOldProj, "junk", "stuff"))
assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.txt"))
# Check that files we want to keep are in the right place
assert os.path.isdir(os.path.join(nwOldProj, "cache"))
assert os.path.isdir(os.path.join(nwOldProj, "content"))
assert os.path.isdir(os.path.join(nwOldProj, "meta"))
assert os.path.isfile(os.path.join(nwOldProj, "content", "f528d831f5b24.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "content", "88124a4292d8b.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "content", "91239bf2f8b69.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "content", "19752e7f9d8af.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "content", "a764d5acf5a21.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "content", "9058ae29f0dfd.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "content", "7ff63b8afc4cd.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "meta", "tagsIndex.json"))
assert os.path.isfile(os.path.join(nwOldProj, "meta", "sessionInfo.log"))
# Close the project
theProject.closeProject()
# Check that new files have been created
assert os.path.isfile(os.path.join(nwOldProj, "meta", "guiOptions.json"))
assert os.path.isfile(os.path.join(nwOldProj, "meta", "sessionStats.log"))
assert os.path.isfile(os.path.join(nwOldProj, "ToC.txt"))
# END Test testCoreProject_OldFormat
@pytest.mark.core
def testCoreProject_LegacyData(monkeypatch, dummyGUI, fncDir):
"""Test the functins that handle legacy data folders and structure
with additional tests of failure handling.
"""
theProject = NWProject(dummyGUI)
theProject.setProjectPath(fncDir)
# assert theProject.newProject({"projPath": fncDir})
# assert theProject.saveProject()
# assert theProject.closeProject()
# Check behaviour of deprecated files function on OSError
tstFile = os.path.join(fncDir, "ToC.json")
writeFile(tstFile, "dummy")
assert os.path.isfile(tstFile)
monkeypatch.setattr("os.unlink", causeOSError)
assert not theProject._deprecatedFiles()
monkeypatch.undo()
assert theProject._deprecatedFiles()
assert not os.path.isfile(tstFile)
# Check processing non-folders
tstFile = os.path.join(fncDir, "data_0")
writeFile(tstFile, "dummy")
assert os.path.isfile(tstFile)
errList = []
errList = theProject._legacyDataFolder(tstFile, errList)
assert len(errList) > 0
# Move folder in data folder, shouldn't be there
tstData = os.path.join(fncDir, "data_1")
errItem = os.path.join(fncDir, "data_1", "stuff")
os.mkdir(tstData)
os.mkdir(errItem)
assert os.path.isdir(tstData)
assert os.path.isdir(errItem)
# This causes a failure to create the 'junk' folder
monkeypatch.setattr("os.mkdir", causeOSError)
errList = []
errList = theProject._legacyDataFolder(tstData, errList)
assert len(errList) > 0
monkeypatch.undo()
# This causes a failure to move 'stuff' to 'junk'
monkeypatch.setattr("os.rename", causeOSError)
errList = []
errList = theProject._legacyDataFolder(tstData, errList)
assert len(errList) > 0
monkeypatch.undo()
# This should be successful
errList = []
errList = theProject._legacyDataFolder(tstData, errList)
assert len(errList) == 0
assert os.path.isdir(os.path.join(fncDir, "junk", "stuff"))
# Check renaming/deleting of old document files
tstData = os.path.join(fncDir, "data_2")
tstDoc1m = os.path.join(tstData, "000000000001_main.nwd")
tstDoc1b = os.path.join(tstData, "000000000001_main.bak")
tstDoc2m = os.path.join(tstData, "000000000002_main.nwd")
tstDoc2b = os.path.join(tstData, "000000000002_main.bak")
tstDoc3m = os.path.join(tstData, "tooshort003_main.nwd")
tstDoc3b = os.path.join(tstData, "tooshort003_main.bak")
os.mkdir(tstData)
writeFile(tstDoc1m, "dummy")
writeFile(tstDoc1b, "dummy")
writeFile(tstDoc2m, "dummy")
writeFile(tstDoc2b, "dummy")
writeFile(tstDoc3m, "dummy")
writeFile(tstDoc3b, "dummy")
# Make the above fail
monkeypatch.setattr("os.rename", causeOSError)
monkeypatch.setattr("os.unlink", causeOSError)
errList = []
errList = theProject._legacyDataFolder(tstData, errList)
assert len(errList) > 0
assert os.path.isfile(tstDoc1m)
assert os.path.isfile(tstDoc1b)
assert os.path.isfile(tstDoc2m)
assert os.path.isfile(tstDoc2b)
assert os.path.isfile(tstDoc3m)
assert os.path.isfile(tstDoc3b)
monkeypatch.undo()
# And succeed ...
errList = []
errList = theProject._legacyDataFolder(tstData, errList)
assert len(errList) == 0
assert not os.path.isdir(tstData)
assert os.path.isfile(os.path.join(fncDir, "content", "2000000000001.nwd"))
assert os.path.isfile(os.path.join(fncDir, "content", "2000000000002.nwd"))
assert os.path.isfile(os.path.join(fncDir, "junk", "tooshort003_main.nwd"))
assert os.path.isfile(os.path.join(fncDir, "junk", "tooshort003_main.bak"))
# END Test testCoreProject_LegacyData
@pytest.mark.core
def testCoreProject_Backup(monkeypatch, dummyGUI, nwMinimal, tmpDir):
"""Test the automated backup feature of the project class. The test
creates a backup of the Minimal test project, and then unzips the
backupd file and checks that the project XML file is identical to
the original file.
"""
theProject = NWProject(dummyGUI)
assert theProject.openProject(nwMinimal)
# Test faulty settings
# No project
dummyGUI.hasProject = False
assert not theProject.zipIt(doNotify=False)
dummyGUI.hasProject = True
# Invalid path
theProject.mainConf.backupPath = None
assert not theProject.zipIt(doNotify=False)
# Missing project name
theProject.mainConf.backupPath = tmpDir
theProject.projName = ""
assert not theProject.zipIt(doNotify=False)
# Non-existent folder
theProject.mainConf.backupPath = os.path.join(tmpDir, "nonexistent")
theProject.projName = "Test Minimal"
assert not theProject.zipIt(doNotify=False)
# Same folder as project (causes infinite loop in zipping)
theProject.mainConf.backupPath = nwMinimal
assert not theProject.zipIt(doNotify=False)
# Set a valid folder
theProject.mainConf.backupPath = tmpDir
# Can't make folder
monkeypatch.setattr("os.mkdir", causeOSError)
assert not theProject.zipIt(doNotify=False)
monkeypatch.undo()
# Can't write archive
monkeypatch.setattr("shutil.make_archive", causeOSError)
assert not theProject.zipIt(doNotify=False)
monkeypatch.undo()
# Test correct settings
assert theProject.zipIt(doNotify=True)
theFiles = os.listdir(os.path.join(tmpDir, "Test Minimal"))
assert len(theFiles) == 1
theZip = theFiles[0]
assert theZip[:12] == "Backup from "
assert theZip[-4:] == ".zip"
# Extract the archive
with ZipFile(os.path.join(tmpDir, "Test Minimal", theZip), "r") as inZip:
inZip.extractall(os.path.join(tmpDir, "extract"))
# Check that the main project file was restored
assert cmpFiles(
os.path.join(nwMinimal, "nwProject.nwx"),
os.path.join(tmpDir, "extract", "nwProject.nwx")
)
# END Test testCoreProject_Backup