# -*- 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_1_nwProject.nwx") compFile = os.path.join(refDir, "coreProject_1_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_2_nwProject.nwx") compFile = os.path.join(refDir, "coreProject_2_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_3_nwProject.nwx") compFile = os.path.join(refDir, "coreProject_3_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_4_nwProject.nwx") compFile = os.path.join(refDir, "coreProject_4_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_5_nwProject.nwx") compFile = os.path.join(refDir, "coreProject_5_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