Merge branch 'main' into release_1.0rc2

This commit is contained in:
Veronica K. B. Olsen
2020-12-12 11:32:22 +01:00
70 changed files with 5514 additions and 1851 deletions
+8 -16
View File
@@ -5,20 +5,12 @@ title: ""
labels: bug
---
**Describe the Bug**
A clear and concise description of what the bug is.
**Note**: Issues without a description will not be considered.
Please also check if an issue already exists on this problem.
Please provide a description covering the following points, if applicable:
**To Reproduce**
Steps to reproduce the behaviour:
**Expected Behaviour**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Error Message**
If the error dialog popped up, copy/paste the content here.
**Additional Context**
Add any other context about the problem here.
* A clear and concise description of what the bug is.
* Steps to reproduce the behaviour
* A clear and concise description of what you expected to happen.
* If applicable, add screenshots to help explain your problem.
* If the error dialog popped up, copy/paste the content here.
+7 -10
View File
@@ -5,14 +5,11 @@ title: ""
labels: enhancement
---
**Is your feature request related to a problem? Please describe:**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Note**: Feature requests without a description will not be considered.
Please also check if an issue already exists on this feature.
Please provide a description covering the following points, if applicable:
**Describe the solution you'd like:**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered:**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context:**
Add any other context or screenshots about the feature request here.
* A clear and concise description of what the problem is. Ex. I'm always frustrated when ...
* A clear and concise description of what you want to happen.
* A clear and concise description of any alternative solutions or features you've considered.
* If applicable, add screenshots to help explain your request.
+1 -1
View File
@@ -1,4 +1,4 @@
# novelWriter ChangeLog
# novelWriter Change Log
## Version 1.0 Release Candidate 2 [2020-11-29]
+20 -18
View File
@@ -58,20 +58,21 @@ from nw.config import Config
#
__package__ = "nw"
__copyright__ = "Copyright 20182020, Veronica Berglyd Olsen"
__license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 20182020, Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__version__ = "1.0rc2"
__hexversion__ = "0x010000c2"
__date__ = "2020-11-29"
__date__ = "2020-12-13"
__status__ = "Beta"
__isstable__ = False
__domain__ = "novelwriter.io"
__url__ = "https://novelwriter.io"
__sourceurl__ = "https://github.com/vkbo/novelWriter"
__issuesurl__ = "https://github.com/vkbo/novelWriter/issues"
__domain__ = "novelwriter.io"
__helpurl__ = "https://github.com/vkbo/novelWriter/discussions"
__releaseurl__ = "https://github.com/vkbo/novelWriter/releases/latest"
__docurl__ = "https://novelwriter.readthedocs.io"
__credits__ = [
"Veronica Berglyd Olsen (developer)",
@@ -246,19 +247,20 @@ def main(sysArgs=None):
errorCode |= 32
if errorData:
if not testMode:
errApp = QApplication([])
errMsg = QErrorMessage()
errMsg.resize(500, 300)
errMsg.showMessage((
"<h3>A critical error has been encountered</h3>"
"<p>novelWriter cannot start due to the following issues:<p>"
"<p>&nbsp;-&nbsp;%s</p>"
"<p>Shutting down ...</p>"
) % (
"<br>&nbsp;-&nbsp;".join(errorData)
))
errApp.exec_()
errApp = QApplication([])
errMsg = QErrorMessage()
errMsg.resize(500, 300)
errMsg.showMessage((
"<h3>A critical error has been encountered</h3>"
"<p>novelWriter cannot start due to the following issues:<p>"
"<p>&nbsp;-&nbsp;%s</p>"
"<p>Shutting down ...</p>"
) % (
"<br>&nbsp;-&nbsp;".join(errorData)
))
for errMsg in errorData:
logger.critical(errMsg)
errApp.exec_()
sys.exit(errorCode)
# Finish initialising config
@@ -293,4 +295,4 @@ def main(sysArgs=None):
nwGUI = GuiMain()
sys.exit(nwApp.exec_())
return
# END Function main
Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+207
View File
@@ -0,0 +1,207 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
id="svg8"
version="1.1"
viewBox="0 0 270.93333 270.93334"
height="1024"
width="1024">
<defs
id="defs2">
<linearGradient
id="linearGradient2515">
<stop
style="stop-color:#f7f7f7;stop-opacity:1"
offset="0"
id="stop2511" />
<stop
style="stop-color:#e5e5e5;stop-opacity:1"
offset="1"
id="stop2513" />
</linearGradient>
<rect
x="60.484347"
y="205.79962"
width="130.57787"
height="65.785888"
id="rect852" />
<rect
id="rect1474"
height="28.631245"
width="177.70083"
y="18.019813"
x="13.463703" />
<rect
id="rect852-7"
height="65.785889"
width="130.57787"
y="205.79962"
x="60.484348" />
<rect
id="rect1544"
height="65.785889"
width="130.57787"
y="205.79962"
x="60.484348" />
<rect
x="60.484348"
y="205.79962"
width="130.57787"
height="65.785889"
id="rect1570" />
<rect
x="13.463703"
y="18.019813"
width="177.70083"
height="28.631245"
id="rect1587" />
<filter
id="filter1444"
style="color-interpolation-filters:sRGB">
<feFlood
id="feFlood1434"
result="flood"
flood-color="rgb(0,0,0)"
flood-opacity="0.498039" />
<feComposite
id="feComposite1436"
result="composite1"
operator="in"
in2="SourceGraphic"
in="flood" />
<feGaussianBlur
id="feGaussianBlur1438"
result="blur"
stdDeviation="3"
in="composite1" />
<feOffset
id="feOffset1440"
result="offset"
dy="6"
dx="6" />
<feComposite
id="feComposite1442"
result="composite2"
operator="over"
in2="offset"
in="SourceGraphic" />
</filter>
<filter
id="filter1456"
style="color-interpolation-filters:sRGB">
<feFlood
id="feFlood1446"
result="flood"
flood-color="rgb(0,0,0)"
flood-opacity="0.498039" />
<feComposite
id="feComposite1448"
result="composite1"
operator="in"
in2="SourceGraphic"
in="flood" />
<feGaussianBlur
id="feGaussianBlur1450"
result="blur"
stdDeviation="3"
in="composite1" />
<feOffset
id="feOffset1452"
result="offset"
dy="6"
dx="6" />
<feComposite
id="feComposite1454"
result="composite2"
operator="over"
in2="offset"
in="SourceGraphic" />
</filter>
<radialGradient
xlink:href="#linearGradient2515"
id="radialGradient2517"
cx="605.42725"
cy="623.97186"
fx="605.42725"
fy="623.97186"
r="410"
gradientTransform="matrix(2.4110274,1.7617596,-0.719517,0.98463462,-776.23441,-1372.7537)"
gradientUnits="userSpaceOnUse" />
</defs>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer3"
style="display:inline">
<path
clip-path="none"
transform="scale(0.26458333)"
d="m 192,22 c -44.32,0 -80,35.680002 -80,80 v 820 c 0,44.32002 35.68,80 80,80 h 640 c 44.31999,0 80,-35.67998 80,-80 V 213.44727 L 720.55469,22 Z"
style="fill:url(#radialGradient2517);fill-opacity:1;stroke:#999999;stroke-width:20;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect1481" />
<path
id="path1490"
style="display:inline;fill:#e0e0e0;fill-opacity:1;stroke:#999999;stroke-width:5.29167;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 241.30037,56.474215 190.64713,5.8204593 V 35.307549 c 0,11.726333 9.44034,21.166666 21.16667,21.166666 z" />
<g
aria-label=".nwx"
transform="matrix(1.7893481,0,0,1.782571,-45.41919,-191.90571)"
id="text850"
style="font-size:5.64444px;line-height:1.25;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans, Normal';letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect852);display:inline;fill:#666666;fill-opacity:1">
<path
d="m 63.088865,241.76928 q 0,-1.36426 0.909505,-2.27376 0.930175,-0.93018 2.480467,-0.93018 1.550291,0 2.459796,0.93018 0.909504,0.9095 0.909504,2.27376 0,1.34359 -0.909504,2.27376 -0.909505,0.9095 -2.459796,0.9095 -1.550292,0 -2.480467,-0.9095 -0.909505,-0.93017 -0.909505,-2.27376 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
id="path856" />
<path
d="m 84.75161,226.78312 q -2.542479,0 -3.782712,2.14974 v 15.79231 h -5.973791 v -22.36554 h 5.601721 l 0.206705,2.56314 q 2.397785,-2.97656 6.428544,-2.97656 3.162595,0 5.105627,1.88103 1.943033,1.88102 1.963703,6.55256 v 14.34537 h -5.994461 v -14.26269 q 0,-2.14973 -0.930175,-2.91454 -0.909505,-0.76482 -2.625161,-0.76482 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
id="path858" />
<path
d="m 126.96089,222.35963 -5.68441,22.36554 h -5.00227 l -4.25814,-14.07665 -4.23746,14.07665 h -4.9816 l -5.705078,-22.36554 h 5.767088 l 2.95589,14.22134 4.05143,-14.22134 h 4.32014 l 4.09277,14.20067 2.91455,-14.20067 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
id="path860" />
<path
d="m 134.77436,222.35963 3.65869,6.65592 3.76204,-6.65592 h 6.3872 l -6.36653,10.93472 6.63525,11.43082 h -6.3872 l -4.01009,-7.04866 -3.94808,7.04866 h -6.42854 l 6.63525,-11.43082 -6.36654,-10.93472 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:42.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;fill-opacity:1"
id="path862" />
</g>
</g>
<g
id="g2509"
transform="matrix(1.2371037,0,0,1.2371037,206.3182,37.341857)">
<path
transform="matrix(0.16895991,0,0,0.18329422,-134.6942,-19.491243)"
id="rect1406-1"
style="display:inline;fill:#a11212;fill-opacity:1;stroke:#7e1b1b;stroke-width:9.99999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter1444)"
d="m 145.18984,56.414257 c -44.32,0 -79.999998,35.68 -79.999998,80.000003 v 199.99805 a 279.99999,279.99999 0 0 0 0,0.002 279.99999,279.99999 0 0 0 0,0.0195 v 199.98242 c 0,44.31999 35.679998,80 79.999998,80 44.32,0 80,-35.68001 80,-80 v -200 a 120,120 0 0 1 120,-120 120,120 0 0 1 120,120 v 282.83592 h 160 V 336.41621 a 279.99999,279.99999 0 0 0 -280,-280.000003 279.99999,279.99999 0 0 0 -0.81055,0 279.99999,279.99999 0 0 0 -133.46484,34.29688 c -14.4245,-20.75262 -38.42829,-34.29883 -65.72461,-34.29883 z M 376.65078,619.64082 c -0.62174,0.0972 -1.2172,0.22317 -1.7793,0.37696 0.5621,-0.15379 1.15756,-0.27973 1.7793,-0.37696 z m -1.7793,0.37696 c -0.5621,0.15378 -1.09071,0.33534 -1.58008,0.53906 0.48937,-0.20372 1.01798,-0.38528 1.58008,-0.53906 z m 338.85742,-0.37696 c 0.62174,0.0972 1.2172,0.22317 1.7793,0.37696 -0.5621,-0.15379 -1.15756,-0.27973 -1.7793,-0.37696 z m 1.7793,0.37696 c 0.5621,0.15378 1.09071,0.33534 1.58008,0.53906 -0.48938,-0.20372 -1.01798,-0.38528 -1.58008,-0.53906 z" />
<path
transform="matrix(0.16895991,0,0,0.18329422,-134.6942,-19.491243)"
id="path897"
style="display:inline;fill:#730d0d;fill-opacity:1;stroke:#590d0d;stroke-width:9.99999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter1456)"
d="m 465.18984,619.25215 v 0.16406 h -85.58398 c -5.49302,0 -9.91602,2.2329 -9.91602,5.00586 0,0.52294 0.15913,1.02696 0.45117,1.5 h -0.44531 c 0.30916,2.03607 1.24102,3.99791 2.81641,5.57227 l 165.59961,165.49414 c 3.92061,3.91808 10.23371,3.91812 14.15429,0 L 717.86757,631.49434 c 1.57569,-1.57463 2.50725,-3.53578 2.81641,-5.57227 h -0.44531 c 0.29216,-0.47304 0.45117,-0.97706 0.45117,-1.5 0,-2.77296 -4.42304,-5.00586 -9.91602,-5.00586 h -85.58398 v -0.16406 z" />
<rect
style="display:inline;fill:#c32222;fill-opacity:1;stroke:none;stroke-width:1.75982;stroke-linejoin:round;stroke-opacity:1"
id="rect914"
width="6.7583966"
height="82.482399"
x="-118.27303"
y="1.2969339"
rx="3.3791983"
ry="3.6658845" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.7 KiB

+35
View File
@@ -0,0 +1,35 @@
<!DOCTYPE html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<body>
<h2>Release Notes for 1.0 RC1</h2>
<p>This is the first release candidate for the upcoming release of novelWriter 1.0.</p>
<p>Since the fifth beta release about four weeks ago, not much has been changed in novelWriter. A
few minor tweaks have been made to the GUI.</p>
<p>A number of features and tools are now automatically switched off when there is no project or
document open for those features to act upon. Previously, this was a bit inconsistent, although no
serious bugs have been reported or encountered.</p>
<p>Most of the minor changes in this release should not be noticeable to most users. However, there
are a couple of noticeable changes.</p>
<h3>Typewriter Mode</h3>
<p>The "Typewriter Mode" of the editor has been improved. Essentially, this feature is a sort of
smart scroll. It tries to keep the cursor stationary in the vertical direction, and will try to
scroll the document up when the cursor skips to a new line while typing (or down in case of
backspaces). This is similar to the way a typewriter scrolls the paper when hitting the return key.
It improves the writing experience as the current active line will stay at the same eye height
level on the screen.</p>
<p>Previously, the feature would lock the cursor to a given vertical position defined by the user.
Now, instead, the cursor will remain stationary in the vertical direction at any position the user
sets it to by mouse click or keyboard navigation. The user can define a minimum distance from the
top where this feature is activated. These changes makes it more flexible in terms of where the
focus is in the editor. The feature can be controlled from the main Preferences.</p>
<h3>Switching Syntax Theme</h3>
<p>It is now possible to switch syntax highlighting theme without restarting novelWriter.
Previously, changing the theme would only half-way update the document, header and footer
background and text colours. The new settings would not be fully applied until the application was
shut down and started again, thus making it a bit tedious to look through syntax themes to find the
one you want.</p>
<p>Switching main GUI theme still requires a restart.</p>
<p><i>The full changelog is available
<a href="https://github.com/vkbo/novelWriter/blob/main/CHANGELOG.md">here</a>.</i></p>
</body>
</html>
+1 -1
View File
@@ -10,7 +10,7 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
background = 255, 255, 255
text = 0, 0, 0
link = 0, 0, 200
headertext = 0, 100, 00
headertext = 0, 100, 0
headertag = 50, 100, 50
emphasis = 150, 110, 30
straightquotes = 200, 0, 0
+25
View File
@@ -0,0 +1,25 @@
[Main]
name = Grey Dark
author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen
url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Syntax]
background = 54, 54, 54
text = 200, 200, 200
link = 200, 200, 200
headertext = 225, 225, 225
headertag = 225, 225, 225
emphasis = 200, 200, 200
straightquotes = 200, 200, 200
doublequotes = 200, 200, 200
singlequotes = 200, 200, 200
hidden = 150, 150, 150
keyword = 225, 225, 225
value = 200, 200, 200
spellcheckline = 200, 46, 0
tagerror = 46, 200, 0
replacetag = 225, 225, 225
modifier = 225, 225, 225
+25
View File
@@ -0,0 +1,25 @@
[Main]
name = Grey Light
author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen
url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Syntax]
background = 255, 255, 255
text = 20, 20, 20
link = 20, 20, 20
headertext = 0, 0, 0
headertag = 0, 0, 0
emphasis = 20, 20, 20
straightquotes = 20, 20, 20
doublequotes = 20, 20, 20
singlequotes = 20, 20, 20
hidden = 100, 100, 100
keyword = 0, 0, 0
value = 20, 20, 20
spellcheckline = 200, 0, 0
tagerror = 0, 150, 0
replacetag = 0, 0, 0
modifier = 0, 0, 0
+56 -40
View File
@@ -27,12 +27,12 @@
import logging
import configparser
import shutil
import json
import sys
import os
from time import time
from shutil import which
from PyQt5.Qt import PYQT_VERSION_STR
from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths, QSysInfo
@@ -44,10 +44,11 @@ logger = logging.getLogger(__name__)
class Config:
CNF_STR = 0
CNF_INT = 1
CNF_BOOL = 2
CNF_LIST = 3
CNF_STR = 0
CNF_INT = 1
CNF_BOOL = 2
CNF_S_LST = 3
CNF_I_LST = 4
def __init__(self):
@@ -91,6 +92,7 @@ class Config:
self.guiFont = "" # Defaults to system default font
self.guiFontSize = 11
self.guiScale = 1.0 # Set automatically by Theme class
self.lastNotes = "" # The latest release notes that have been shown
## Sizes
self.winGeometry = [1200, 650]
@@ -210,12 +212,8 @@ class Config:
self.osUnknown = True
# Other System Info
if self.verQtValue >= 50600:
self.hostName = QSysInfo.machineHostName()
self.kernelVer = QSysInfo.kernelVersion()
else:
self.hostName = "Unknown"
self.kernelVer = "Unknown"
self.hostName = "Unknown"
self.kernelVer = "Unknown"
# Packages
self.hasEnchant = False # The pyenchant package
@@ -320,6 +318,11 @@ class Config:
self.errData.append(str(e))
self.dataPath = None
# Host and Kernel
if self.verQtValue >= 50600:
self.hostName = QSysInfo.machineHostName()
self.kernelVer = QSysInfo.kernelVersion()
# Load recent projects cache
self.loadRecentCache()
@@ -380,29 +383,32 @@ class Config:
self.guiFontSize = self._parseLine(
cnfParse, cnfSec, "guifontsize", self.CNF_INT, self.guiFontSize
)
self.lastNotes = self._parseLine(
cnfParse, cnfSec, "lastnotes", self.CNF_STR, self.lastNotes
)
## Sizes
cnfSec = "Sizes"
self.winGeometry = self._parseLine(
cnfParse, cnfSec, "geometry", self.CNF_LIST, self.winGeometry
cnfParse, cnfSec, "geometry", self.CNF_I_LST, self.winGeometry
)
self.treeColWidth = self._parseLine(
cnfParse, cnfSec, "treecols", self.CNF_LIST, self.treeColWidth
cnfParse, cnfSec, "treecols", self.CNF_I_LST, self.treeColWidth
)
self.projColWidth = self._parseLine(
cnfParse, cnfSec, "projcols", self.CNF_LIST, self.projColWidth
cnfParse, cnfSec, "projcols", self.CNF_I_LST, self.projColWidth
)
self.mainPanePos = self._parseLine(
cnfParse, cnfSec, "mainpane", self.CNF_LIST, self.mainPanePos
cnfParse, cnfSec, "mainpane", self.CNF_I_LST, self.mainPanePos
)
self.docPanePos = self._parseLine(
cnfParse, cnfSec, "docpane", self.CNF_LIST, self.docPanePos
cnfParse, cnfSec, "docpane", self.CNF_I_LST, self.docPanePos
)
self.viewPanePos = self._parseLine(
cnfParse, cnfSec, "viewpane", self.CNF_LIST, self.viewPanePos
cnfParse, cnfSec, "viewpane", self.CNF_I_LST, self.viewPanePos
)
self.outlnPanePos = self._parseLine(
cnfParse, cnfSec, "outlinepane", self.CNF_LIST, self.outlnPanePos
cnfParse, cnfSec, "outlinepane", self.CNF_I_LST, self.outlnPanePos
)
self.isFullScreen = self._parseLine(
cnfParse, cnfSec, "fullscreen", self.CNF_BOOL, self.isFullScreen
@@ -480,10 +486,10 @@ class Config:
cnfParse, cnfSec, "autoscrollpos", self.CNF_INT, self.autoScrollPos
)
self.fmtSingleQuotes = self._parseLine(
cnfParse, cnfSec, "fmtsinglequote", self.CNF_LIST, self.fmtSingleQuotes
cnfParse, cnfSec, "fmtsinglequote", self.CNF_S_LST, self.fmtSingleQuotes
)
self.fmtDoubleQuotes = self._parseLine(
cnfParse, cnfSec, "fmtdoublequote", self.CNF_LIST, self.fmtDoubleQuotes
cnfParse, cnfSec, "fmtdoublequote", self.CNF_S_LST, self.fmtDoubleQuotes
)
self.spellTool = self._parseLine(
cnfParse, cnfSec, "spelltool", self.CNF_STR, self.spellTool
@@ -584,6 +590,7 @@ class Config:
cnfParse.set(cnfSec, "guidark", str(self.guiDark))
cnfParse.set(cnfSec, "guifont", str(self.guiFont))
cnfParse.set(cnfSec, "guifontsize", str(self.guiFontSize))
cnfParse.set(cnfSec, "lastnotes", str(self.lastNotes))
## Sizes
cnfSec = "Sizes"
@@ -896,23 +903,28 @@ class Config:
# Internal Functions
##
def _unpackList(self, inStr, listLen, listDefault, castTo=int):
"""Unpack a comma separated string of items into a list.
"""
inData = inStr.split(",")
outData = []
for i in range(listLen):
try:
outData.append(castTo(inData[i]))
except Exception:
outData.append(listDefault[i])
return outData
def _packList(self, inData):
"""Pack a list of items into a comma separated string.
"""Pack a list of items into a comma-separated string.
"""
return ", ".join([str(inVal) for inVal in inData])
def _unpackList(self, inStr, listDefault, cnfType):
"""Unpack a comma-separated string of items into a list.
"""
inData = inStr.split(",")
outData = listDefault.copy()
for i in range(min(len(inData), len(listDefault))):
try:
if cnfType == self.CNF_S_LST:
outData[i] = inData[i].strip()
elif cnfType == self.CNF_I_LST:
outData[i] = int(inData[i].strip())
else:
continue
except Exception:
continue
return outData
def _parseLine(self, cnfParse, cnfSec, cnfName, cnfType, cnfDefault):
"""Parse a line and return the correct datatype.
"""
@@ -925,18 +937,24 @@ class Config:
return cnfParse.getint(cnfSec, cnfName)
elif cnfType == self.CNF_BOOL:
return cnfParse.getboolean(cnfSec, cnfName)
elif cnfType == self.CNF_LIST:
elif cnfType == self.CNF_I_LST:
return self._unpackList(
cnfParse.get(cnfSec, cnfName), len(cnfDefault), cnfDefault
cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_I_LST
)
elif cnfType == self.CNF_S_LST:
return self._unpackList(
cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_S_LST
)
except ValueError as e:
logger.error("Failed to load value from config file.")
logger.error(str(e))
return cnfDefault
return cnfDefault
def _checkNone(self, checkVal):
"""Convert a string to a none type.
"""Return a NoneType if the value correspomds to None, otherwise
return the value unchanged.
"""
if checkVal is None:
return None
@@ -956,10 +974,8 @@ class Config:
self.hasEnchant = False
logger.debug("Checking package 'pyenchant': Missing")
try:
self.hasAssistant = which("assistant")
except Exception:
self.hasAssistant = False
assistPath = shutil.which("assistant")
self.hasAssistant = assistPath is not None
if self.hasAssistant:
logger.debug("Checking executable 'assistant': Ok")
else:
+2 -1
View File
@@ -168,7 +168,8 @@ class NWDoc():
os.unlink(docPath)
os.rename(docTemp, docPath)
self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName)
if self._theItem is not None:
self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName)
return True
+10 -6
View File
@@ -43,7 +43,7 @@ class NWItem():
self.itemName = ""
self.itemHandle = None
self.itemParent = None
self.itemOrder = None
self.itemOrder = 0
self.itemType = nwItemType.NO_TYPE
self.itemClass = nwItemClass.NO_CLASS
self.itemLayout = nwItemLayout.NO_LAYOUT
@@ -96,20 +96,21 @@ class NWItem():
return False
if "handle" in xItem.attrib:
self.itemHandle = xItem.attrib["handle"]
self.setHandle(xItem.attrib["handle"])
else:
logger.error("XML item entry does not have a handle")
return False
if "parent" in xItem.attrib:
self.itemParent = xItem.attrib["parent"]
self.setParent(xItem.attrib["parent"])
if "order" in xItem.attrib:
self.setOrder(xItem.attrib["order"])
retStatus = True
for xValue in xItem:
if xValue.tag == "name":
self.setName(xValue.text)
elif xValue.tag == "order":
self.setOrder(xValue.text)
elif xValue.tag == "type":
self.setType(xValue.text)
elif xValue.tag == "class":
@@ -156,7 +157,10 @@ class NWItem():
def setName(self, theName):
"""Set the item name.
"""
self.itemName = theName.strip()
if isinstance(theName, str):
self.itemName = theName.strip()
else:
self.itemName = ""
return
def setHandle(self, theHandle):
+3 -15
View File
@@ -176,11 +176,7 @@ class OptionState():
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return self.theState[getGroup][getName]
except Exception as e:
logger.warning(str(e))
return defaultValue
return self.theState[getGroup][getName]
return defaultValue
def getString(self, getGroup, getName, defaultValue):
@@ -189,11 +185,7 @@ class OptionState():
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return str(self.theState[getGroup][getName])
except Exception as e:
logger.warning(str(e))
return defaultValue
return str(self.theState[getGroup][getName])
return defaultValue
def getInt(self, getGroup, getName, defaultValue):
@@ -228,11 +220,7 @@ class OptionState():
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return bool(self.theState[getGroup][getName])
except Exception as e:
logger.warning(str(e))
return defaultValue
return bool(self.theState[getGroup][getName])
return defaultValue
##
+67 -70
View File
@@ -28,12 +28,10 @@
import nw
import logging
import os
import shutil
from lxml import etree
from time import time
from shutil import make_archive, unpack_archive, copyfile
from PyQt5.QtWidgets import QMessageBox
from nw.core.tree import NWTree
from nw.core.item import NWItem
@@ -436,26 +434,15 @@ class NWProject():
xRoot = nwXML.getroot()
nwxRoot = xRoot.tag
appVersion = "Unknown"
hexVersion = "0x0"
fileVersion = "Unknown"
self.saveCount = 0
self.autoCount = 0
if "appVersion" in xRoot.attrib:
appVersion = xRoot.attrib["appVersion"]
if "hexVersion" in xRoot.attrib:
hexVersion = xRoot.attrib["hexVersion"]
if "fileVersion" in xRoot.attrib:
fileVersion = xRoot.attrib["fileVersion"]
appVersion = xRoot.attrib.get("appVersion", "Unknown")
hexVersion = xRoot.attrib.get("hexVersion", "0x0")
fileVersion = xRoot.attrib.get("fileVersion", "Unknown")
# The following are deprecated and will be removed
if "saveCount" in xRoot.attrib:
self.saveCount = checkInt(xRoot.attrib["saveCount"], 0, False)
if "autoCount" in xRoot.attrib:
self.autoCount = checkInt(xRoot.attrib["autoCount"], 0, False)
if "editTime" in xRoot.attrib:
self.editTime = checkInt(xRoot.attrib["editTime"], 0, False)
# The settings have been moved to the <project> tag
self.saveCount = checkInt(xRoot.attrib.get("saveCount", 0), 0, False)
self.autoCount = checkInt(xRoot.attrib.get("autoCount", 0), 0, False)
self.editTime = checkInt(xRoot.attrib.get("editTime", 0), 0, False)
logger.verbose("XML root is %s" % nwxRoot)
logger.verbose("File version is %s" % fileVersion)
@@ -483,20 +470,19 @@ class NWProject():
# parser will lose the autoReplace settings if allowed to
# read the file. Introduced in version 0.10.
if fileVersion == "1.0" and self.mainConf.showGUI:
msgBox = QMessageBox()
msgRes = msgBox.question(self.theParent, "Old Project Version", (
if fileVersion == "1.0":
msgRes = self.theParent.askQuestion("Old Project Version", (
"The project file and data is created by a novelWriter version "
"lower than 0.7. Do you want to upgrade the project to the "
"most recent format?<br><br>Note that after the upgrade, you "
"cannot open the project with an older version of novelWriter "
"any more, so make sure you have a recent backup."
))
if msgRes != QMessageBox.Yes:
if not msgRes:
self.clearProject()
return False
elif fileVersion != "1.1" and fileVersion != "1.2" and self.mainConf.showGUI:
elif fileVersion != "1.1" and fileVersion != "1.2":
self.makeAlert((
"Unknown or unsupported novelWriter project file format. "
"The project cannot be opened by this version of novelWriter. "
@@ -510,9 +496,8 @@ class NWProject():
# Check novelWriter Version
# =========================
if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI:
msgBox = QMessageBox()
msgRes = msgBox.question(self.theParent, "Version Conflict", (
if int(hexVersion, 16) > int(nw.__hexversion__, 16):
msgRes = self.theParent.askQuestion("Version Conflict", (
"This project was saved by a newer version of novelWriter, version %s. "
"This is version %s. If you continue to open the project, some attributes "
"and settings may not be preserved, but the overall project should be fine. "
@@ -520,7 +505,7 @@ class NWProject():
) % (
appVersion, nw.__version__
))
if msgRes != QMessageBox.Yes:
if not msgRes:
self.clearProject()
return False
@@ -835,15 +820,15 @@ class NWProject():
try:
self._clearLockFile()
make_archive(baseName, "zip", self.projPath, ".")
shutil.make_archive(baseName, "zip", self.projPath, ".")
self._writeLockFile()
logger.info("Backup written to: %s" % archName)
if doNotify:
self.theParent.makeAlert(
"Backup archive file written to: %s.zip" % os.path.join(cleanName, archName),
nwAlert.INFO
)
else:
logger.info("Backup written to: %s" % archName)
except Exception as e:
self.theParent.makeAlert(
["Could not write backup archive.", str(e)],
@@ -874,7 +859,7 @@ class NWProject():
self.setProjectPath(projPath, newProject=True)
try:
unpack_archive(pkgSample, projPath)
shutil.unpack_archive(pkgSample, projPath)
isSuccess = True
except Exception as e:
self.makeAlert(
@@ -887,14 +872,14 @@ class NWProject():
try:
srcProj = os.path.join(srcSample, nwFiles.PROJ_FILE)
dstProj = os.path.join(projPath, nwFiles.PROJ_FILE)
copyfile(srcProj, dstProj)
shutil.copyfile(srcProj, dstProj)
srcContent = os.path.join(srcSample, "content")
dstContent = os.path.join(projPath, "content")
for srcFile in os.listdir(srcContent):
srcDoc = os.path.join(srcContent, srcFile)
dstDoc = os.path.join(dstContent, srcFile)
copyfile(srcDoc, dstDoc)
shutil.copyfile(srcDoc, dstDoc)
isSuccess = True
@@ -998,11 +983,15 @@ class NWProject():
"You must set a valid backup path in preferences to use "
"the automatic project backup feature."
), nwAlert.WARN)
return False
if self.projName == "":
self.theParent.makeAlert((
"You must set a valid project name in project settings to "
"use the automatic project backup feature."
), nwAlert.WARN)
return False
return True
def setSpellCheck(self, theMode):
@@ -1011,12 +1000,15 @@ class NWProject():
if self.spellCheck != theMode:
self.spellCheck = theMode
self.setProjectChanged(True)
return True
return self.spellCheck
def setSpellLang(self, theLang):
"""Set the project-specific spell check language.
"""
self.projLang = checkString(theLang, None, True)
theLang = checkString(theLang, None, True)
if self.projLang != theLang:
self.projLang = theLang
self.setProjectChanged(True)
return True
def setAutoOutline(self, theMode):
@@ -1025,7 +1017,7 @@ class NWProject():
if self.autoOutline != theMode:
self.autoOutline = theMode
self.setProjectChanged(True)
return True
return self.autoOutline
def setTreeOrder(self, newOrder):
"""A list representing the linear/flattened order of project
@@ -1072,7 +1064,7 @@ class NWProject():
if nwItem.itemStatus in replaceMap.keys():
nwItem.setStatus(replaceMap[nwItem.itemStatus])
self.setProjectChanged(True)
return
return True
def setImportColours(self, newCols):
"""Update the list of note file importance flags. Also iterate
@@ -1084,14 +1076,15 @@ class NWProject():
if nwItem.itemStatus in replaceMap.keys():
nwItem.setStatus(replaceMap[nwItem.itemStatus])
self.setProjectChanged(True)
return
return True
def setAutoReplace(self, autoReplace):
"""Update the auto-replace dictionary. This replaces the entire
dictionary, so alterations have to be made in a copy.
"""
self.autoReplace = autoReplace
return
self.setProjectChanged(True)
return True
def setTitleFormat(self, titleFormat):
"""Set the formatting of titles in the project.
@@ -1099,7 +1092,7 @@ class NWProject():
for valKey, valEntry in titleFormat.items():
if valKey in self.titleFormat:
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey], False)
return
return True
def setProjectChanged(self, bValue):
"""Toggle the project changed flag, and propagate the
@@ -1132,13 +1125,11 @@ class NWProject():
sentItems = []
iterItems = self.projTree.handles()
n = 0
nMax = len(iterItems)
nMax = min(len(iterItems), 10000)
while n < nMax:
tHandle = iterItems[n]
tItem = self.projTree[tHandle]
n += 1
if n > 10000:
return # Just in case
if tItem is None:
# Technically a bug since treeOrder is built from the
# same data as projTree
@@ -1154,10 +1145,11 @@ class NWProject():
yield tItem
elif tItem.itemParent in iterItems:
# Item's parent exists, but hasn't been sent yet, so add
# it again to the end
# it again to the end, but make sure this doesn't get
# out hand, so we cap at 10000 items
logger.warning("Item %s found before its parent" % tHandle)
iterItems.append(tHandle)
nMax = len(iterItems)
nMax = min(len(iterItems), 10000)
else:
# Item is orphaned
logger.error("Item %s has no parent in current tree" % tHandle)
@@ -1196,13 +1188,12 @@ class NWProject():
if not os.path.isfile(lockFile):
return []
theLines = []
try:
with open(lockFile, mode="r", encoding="utf8") as inFile:
theData = inFile.read()
theLines = theData.splitlines()
if len(theLines) == 4:
return theLines
else:
if len(theLines) != 4:
return ["ERROR"]
except Exception as e:
@@ -1210,7 +1201,7 @@ class NWProject():
logger.error(str(e))
return ["ERROR"]
return ["ERROR"]
return theLines
def _writeLockFile(self):
"""Writes a lock file to the project folder.
@@ -1243,13 +1234,12 @@ class NWProject():
if os.path.isfile(lockFile):
try:
os.unlink(lockFile)
return True
except Exception as e:
logger.error("Failed to remove project lockfile")
logger.error(str(e))
return False
return None
return True
def _checkFolder(self, thePath):
"""Check if a folder exists, and if it doesn't, create it.
@@ -1292,7 +1282,7 @@ class NWProject():
back into the project tree.
"""
if self.projPath is None:
return
return False
# Then check the files in the data folder
logger.debug("Checking files in project content folder")
@@ -1352,7 +1342,7 @@ class NWProject():
orphItem.setLayout(oLayout)
self.projTree.append(oHandle, None, orphItem)
return
return True
def _appendSessionStats(self):
"""Append session statistics to the sessions log file.
@@ -1363,21 +1353,27 @@ class NWProject():
sessionFile = os.path.join(self.projMeta, nwFiles.SESS_STATS)
isFile = os.path.isfile(sessionFile)
with open(sessionFile, mode="a+", encoding="utf8") as outFile:
if not isFile:
# It's a new file, so add a header
if self.lastWCount > 0:
outFile.write("# Offset %d\n" % self.lastWCount)
outFile.write("# %-17s %-19s %8s %8s\n" % (
"Start Time", "End Time", "Novel", "Notes"
try:
with open(sessionFile, mode="a+", encoding="utf8") as outFile:
if not isFile:
# It's a new file, so add a header
if self.lastWCount > 0:
outFile.write("# Offset %d\n" % self.lastWCount)
outFile.write("# %-17s %-19s %8s %8s\n" % (
"Start Time", "End Time", "Novel", "Notes"
))
outFile.write("%-19s %-19s %8d %8d\n" % (
formatTimeStamp(self.projOpened),
formatTimeStamp(time()),
self.novelWCount,
self.notesWCount,
))
outFile.write("%-19s %-19s %8d %8d\n" % (
formatTimeStamp(self.projOpened),
formatTimeStamp(time()),
self.novelWCount,
self.notesWCount,
))
except Exception as e:
logger.error("Failed to write session stats file")
logger.error(str(e))
return False
return True
@@ -1490,7 +1486,8 @@ class NWProject():
os.unlink(rmFile)
except Exception as e:
logger.error(str(e))
return False
return
return True
# END Class NWProject
+18 -14
View File
@@ -28,8 +28,7 @@
import nw
import logging
import os
from difflib import get_close_matches
import difflib
from nw.constants import nwConst, isoLanguage
@@ -70,14 +69,16 @@ class NWSpellCheck():
"""
if self.projectDict is not None and newWord not in self.projDict:
newWord = newWord.strip()
self.projDict.append(newWord)
try:
with open(self.projectDict, mode="a+", encoding="utf-8") as outFile:
outFile.write("%s\n" % newWord)
self.projDict.append(newWord)
except Exception as e:
logger.error("Failed to add word to project word list %s" % str(self.projectDict))
logger.error(str(e))
return
return False
return True
return False
def listDictionaries(self):
"""Dummy function.
@@ -109,9 +110,12 @@ class NWSpellCheck():
"""
self.projDict = []
if projectDict is not None:
self.projectDict = projectDict
if not os.path.isfile(projectDict):
return
self.projectDict = None
return False
else:
self.projectDict = projectDict
try:
logger.debug("Loading project word list")
with open(projectDict, mode="r", encoding="utf-8") as wordsFile:
@@ -123,7 +127,9 @@ class NWSpellCheck():
except Exception as e:
logger.error("Failed to load project word list")
logger.error(str(e))
return
return False
return True
# END Class NWSpellCheck
@@ -287,7 +293,7 @@ class NWSpellSimple(NWSpellCheck):
if len(theWord) == 0:
return []
theMatches = get_close_matches(theWord.lower(), self.WORDS, n=10, cutoff=0.75)
theMatches = difflib.get_close_matches(theWord.lower(), self.WORDS, n=10, cutoff=0.75)
theOptions = []
for aWord in theMatches:
if len(aWord) == 0:
@@ -314,14 +320,12 @@ class NWSpellSimple(NWSpellCheck):
retList = []
for dictFile in os.listdir(self.mainConf.dictPath):
theBits = os.path.splitext(dictFile)
if len(theBits) != 2:
continue
if theBits[1] != ".dict":
fRoot, fExt = os.path.splitext(dictFile)
if fExt != ".dict":
continue
spName = "%s [%s]" % (self.expandLanguage(theBits[0]), nwConst.SP_INTERNAL)
retList.append((theBits[0], spName))
spName = "%s [%s]" % (self.expandLanguage(fRoot), nwConst.SP_INTERNAL)
retList.append((fRoot, spName))
return retList
+6 -13
View File
@@ -79,6 +79,7 @@ class NWStatus():
theStatus = checkInt(theStatus, 0, False)
if theStatus >= 0 and theStatus < self._theLength:
return self._theLabels[theStatus]
return self._theLabels[0]
def setNewEntries(self, newList):
"""Update the list of entries after they have been modified by
@@ -108,7 +109,8 @@ class NWStatus():
return
def countEntry(self, theLabel):
"""Lookup the usage count of a given entry.
"""Increment the counter for a given label. This should be used
together with resetCounts in a loop over project items.
"""
theIndex = self.lookupEntry(theLabel)
if theIndex is not None:
@@ -136,18 +138,9 @@ class NWStatus():
for xChild in xParent:
theLabels.append(xChild.text)
if "red" in xChild.attrib:
cR = checkInt(xChild.attrib["red"], 0, False)
else:
cR = 0
if "green" in xChild.attrib:
cG = checkInt(xChild.attrib["green"], 0, False)
else:
cG = 0
if "blue" in xChild.attrib:
cB = checkInt(xChild.attrib["blue"], 0, False)
else:
cB = 0
cR = checkInt(xChild.attrib.get("red", 0), 0, False)
cG = checkInt(xChild.attrib.get("green", 0), 0, False)
cB = checkInt(xChild.attrib.get("blue", 0), 0, False)
theColours.append((cR, cG, cB))
if len(theLabels) > 0:
+11 -9
View File
@@ -68,16 +68,16 @@ class ToHtml(Tokenizer):
# Setters
##
def setPreview(self, forPreview, doComments, doSynopsis):
def setPreview(self, doComments, doSynopsis):
"""If we're using this class to generate markdown preview, we
need to make a few changes to formatting, which is managed by
these flags.
"""
if forPreview:
self.genMode = self.M_PREVIEW
self.doKeywords = True
self.doComments = doComments
self.doSynopsis = doSynopsis
self.genMode = self.M_PREVIEW
self.doKeywords = True
self.doComments = doComments
self.doSynopsis = doSynopsis
return
def setStyles(self, cssStyles):
@@ -141,11 +141,13 @@ class ToHtml(Tokenizer):
# For novel files for export, we bump the titles one level
# up as this is more useful for printing and word processor
# imports.
h1 = "h1 class='title'"
h1Cl = " class='title'"
h1 = "h1"
h2 = "h1"
h3 = "h2"
h4 = "h3"
else:
h1Cl = ""
h1 = "h1"
h2 = "h2"
h3 = "h3"
@@ -193,7 +195,7 @@ class ToHtml(Tokenizer):
else:
aNm = ""
# Process TextType
# Process Text Type
if tType == self.T_EMPTY:
if parStyle is None:
parStyle = ""
@@ -214,7 +216,7 @@ class ToHtml(Tokenizer):
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "<br/>")
tmpResult.append("<%s%s>%s%s</%s>\n" % (h1, hStyle, aNm, tHead, h1))
tmpResult.append("<%s%s%s>%s%s</%s>\n" % (h1, h1Cl, hStyle, aNm, tHead, h1))
elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "<br/>")
+8 -7
View File
@@ -180,7 +180,7 @@ class Tokenizer():
##
def addRootHeading(self, theHandle):
"""Add a heading at the start if a new root folder.
"""Add a heading at the start of a new root folder.
"""
theItem = self.theProject.projTree[theHandle]
if theItem is None:
@@ -205,7 +205,7 @@ class Tokenizer():
self.theHandle = theHandle
self.theItem = self.theProject.projTree[theHandle]
if self.theItem is None:
return
return False
if theText is not None:
# If the text is set, just use that
@@ -234,7 +234,7 @@ class Tokenizer():
self.isNote = self.theItem.itemLayout == nwItemLayout.NOTE
self.isNovel = self.isBook or self.isUnNum or self.isChap or self.isScene
return
return True
def getResult(self):
"""Return the result from the conversion.
@@ -244,6 +244,8 @@ class Tokenizer():
def getResultSize(self):
"""Return the size of the result from the conversion.
"""
if self.theResult is None:
return 0
return len(self.theResult)
def getFilteredMarkdown(self):
@@ -445,7 +447,7 @@ class Tokenizer():
"""
# No special header formatting for notes and no-layout files
if self.isNone or self.isNote:
return
return False
# For novel files, we need to handle chapter numbering, scene
# numbering, and scene breaks
@@ -479,8 +481,7 @@ class Tokenizer():
if self.isUnNum:
tTemp = self._formatHeading(self.fmtUnNum, tToken[2])
elif tToken[2].startswith("*"):
tTemp = self._formatHeading(self.fmtUnNum, tToken[2])
tTemp = tTemp[1:].lstrip()
tTemp = self._formatHeading(self.fmtUnNum, tToken[2][1:].lstrip())
else:
self.numChapter += 1
tTemp = self._formatHeading(self.fmtChapter, tToken[2])
@@ -663,7 +664,7 @@ class Tokenizer():
self.A_LEFT
)
return
return True
##
# Internal Functions
+15 -5
View File
@@ -141,6 +141,15 @@ def _numberToWordEN(numVal):
tenWord = ""
hunWord = ""
if not isinstance(numVal, int):
return "[NaN]"
if numVal < 0:
return "[Negative]"
if numVal > 999:
return "[Out of Range]"
if numVal == 0:
return "Zero"
@@ -166,19 +175,20 @@ def _numberToWordEN(numVal):
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine",
}
retVale = ""
hunWord = theHundreds.get(hunVal, "")
if tenVal == 10:
oneWord = theTeens.get(oneVal, "")
return f"{hunWord} {oneWord}".strip()
retVale = f"{hunWord} {oneWord}".strip()
else:
oneWord = theOnes.get(oneVal, "")
if tenVal == 0:
return f"{hunWord} {oneWord}".strip()
retVale = f"{hunWord} {oneWord}".strip()
else:
tenWord = theTens.get(tenVal, "")
if oneVal == 0:
return f"{hunWord} {tenWord}".strip()
retVale = f"{hunWord} {tenWord}".strip()
else:
return f"{hunWord} {tenWord}-{oneWord}".strip()
retVale = f"{hunWord} {tenWord}-{oneWord}".strip()
return ""
return retVale
+16 -7
View File
@@ -48,8 +48,10 @@ class NWTree():
self._treeOrder = [] # The order of the tree items on the tree view
self._treeRoots = [] # The root items of the tree
self._trashRoot = None # The handle of the trash root folder
self._archRoot = None # The handle of the archive root folder
self._theIndex = 0 # The current iterator index
self._treeChanged = False # True if tree structure has changed
self._handleSeed = None # Used for generating handles for testing
return
@@ -83,14 +85,15 @@ class NWTree():
if tHandle is None:
tHandle = self._makeHandle()
if tHandle in self._projTree:
logger.warning("Duplicate handle %s detected, skipping" % tHandle)
return False
logger.verbose("Adding item %s with parent %s" % (str(tHandle), str(pHandle)))
nwItem.setHandle(tHandle)
nwItem.setParent(pHandle)
self._projTree[tHandle] = nwItem
self._treeOrder.append(tHandle)
if nwItem.itemType == nwItemType.ROOT:
logger.verbose("Item %s is a root item" % str(tHandle))
self._treeRoots.append(tHandle)
@@ -104,10 +107,13 @@ class NWTree():
self._trashRoot = tHandle
else:
logger.error("Only one trash folder allowed")
return False
self._projTree[tHandle] = nwItem
self._treeOrder.append(tHandle)
self._setTreeChanged(True)
return
return True
def packXML(self, xParent):
"""Pack the content of the tree into the provided XML object. In
@@ -176,8 +182,9 @@ class NWTree():
except Exception as e:
logger.error(str(e))
return False
return
return True
def sumWords(self):
"""Loops over all entries and adds up the word counts.
@@ -244,6 +251,8 @@ class NWTree():
return True
for aRoot in self._treeRoots:
tItem = self.__getitem__(aRoot)
if tItem is None:
continue
if theClass == tItem.itemClass:
return False
return True
@@ -397,7 +406,7 @@ class NWTree():
del self._projTree[tHandle]
else:
logger.warning("Failed to delete item %s: item not found" % tHandle)
return False
return
if tHandle in self._treeRoots:
self._treeRoots.remove(tHandle)
@@ -408,7 +417,7 @@ class NWTree():
self._setTreeChanged(True)
return True
return
def __contains__(self, tHandle):
"""Checks if a handle exists in the tree.
+3 -8
View File
@@ -135,10 +135,9 @@ class NWErrorMessage(QDialog):
# END Class NWErrorMessage
def exceptionHandler(exType, exValue, exTrace, testMode=False):
def exceptionHandler(exType, exValue, exTrace):
"""Function to catch unhandled global exceptions.
"""
import nw
import logging
from traceback import print_tb
from PyQt5.QtWidgets import qApp
@@ -160,8 +159,7 @@ def exceptionHandler(exType, exValue, exTrace, testMode=False):
errMsg = NWErrorMessage(nwGUI)
errMsg.setMessage(exType, exValue, exTrace)
if nw.CONFIG.showGUI:
errMsg.exec_()
errMsg.exec_()
try:
# Try a controlled shudown
@@ -173,10 +171,7 @@ def exceptionHandler(exType, exValue, exTrace, testMode=False):
logger.critical("Could not close the project before exiting")
logger.critical(str(e))
if testMode:
return errMsg.msgBody.toPlainText()
else:
qApp.exit(1)
qApp.exit(1)
except Exception as e:
logger.critical(str(e))
+40 -9
View File
@@ -32,8 +32,9 @@ import os
from datetime import datetime
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QCursor
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QTabWidget,
qApp, QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QTabWidget,
QTextBrowser, QLabel
)
@@ -80,6 +81,10 @@ class GuiAbout(QDialog):
self.pageAbout.setOpenExternalLinks(True)
self.pageAbout.document().setDocumentMargin(self.mainConf.pxInt(16))
self.pageNotes = QTextBrowser()
self.pageNotes.setOpenExternalLinks(True)
self.pageNotes.document().setDocumentMargin(self.mainConf.pxInt(16))
self.pageLicense = QTextBrowser()
self.pageLicense.setOpenExternalLinks(True)
self.pageLicense.document().setDocumentMargin(self.mainConf.pxInt(16))
@@ -87,6 +92,7 @@ class GuiAbout(QDialog):
# Main Tab Area
self.tabBox = QTabWidget()
self.tabBox.addTab(self.pageAbout, "About")
self.tabBox.addTab(self.pageNotes, "Release")
self.tabBox.addTab(self.pageLicense, "License")
self.innerBox.addWidget(self.tabBox)
@@ -98,14 +104,27 @@ class GuiAbout(QDialog):
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
self._setStyleSheet()
self._fillAboutPage()
self._fillLicensePage()
logger.debug("GuiAbout initialisation complete")
return
def populateGUI(self):
"""Populate tabs with text.
"""
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self._setStyleSheet()
self._fillAboutPage()
self._fillNotesPage()
self._fillLicensePage()
qApp.restoreOverrideCursor()
return
def showReleaseNotes(self):
"""Show the release notes.
"""
self.tabBox.setCurrentWidget(self.pageNotes)
return
##
# Internal Functions
##
@@ -121,14 +140,14 @@ class GuiAbout(QDialog):
"<p>novelWriter is a markdown-like text editor designed for "
"organising and writing novels. It is written in Python 3 with a "
"Qt5 GUI, using PyQt5.</p>"
"<p>novelWriter is free software: you can redistribute it and/or "
"<p>novelWriter is free software: you can redistribute it and "
"modify it under the terms of the GNU General Public License as "
"published by the Free Software Foundation, either version 3 of "
"the License, or (at your option) any later version.</p>"
"<p>novelWriter is distributed in the hope that it will be useful, "
"but WITHOUT ANY WARRANTY; without even the implied warranty of "
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.</p>"
"<p>See the License tab for the full text, or visit the GNU website "
"<p>See the License tab for the full license text, or visit the GNU website "
"at <a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a> "
"for more details.</p>"
"<h3>Credits</h3>"
@@ -192,11 +211,22 @@ class GuiAbout(QDialog):
return
def _fillNotesPage(self):
"""Load the content for the Release Notes page.
"""
docPath = os.path.join(self.mainConf.assetPath, "text", "release_notes.htm")
if os.path.isfile(docPath):
with open(docPath, mode="r", encoding="utf8") as inFile:
helpText = inFile.read()
self.pageNotes.setHtml(helpText)
else:
self.pageNotes.setHtml("Error loading release notes text ...")
return
def _fillLicensePage(self):
"""Load the content for the License page.
"""
docName = "gplv3_%s.htm" % self.mainConf.guiLang
docPath = os.path.join(self.mainConf.assetPath, "text", docName)
docPath = os.path.join(self.mainConf.assetPath, "text", "gplv3_en.htm")
if os.path.isfile(docPath):
with open(docPath, mode="r", encoding="utf8") as inFile:
helpText = inFile.read()
@@ -221,6 +251,7 @@ class GuiAbout(QDialog):
hColB = self.theParent.theTheme.colHead[2],
)
self.pageAbout.document().setDefaultStyleSheet(styleSheet)
self.pageNotes.document().setDefaultStyleSheet(styleSheet)
self.pageLicense.document().setDefaultStyleSheet(styleSheet)
return
+4 -3
View File
@@ -262,10 +262,10 @@ class GuiDocEditor(QTextEdit):
# If we have a document open, we should reload it in case the
# font changed, otherwise we just clear the editor entirely,
# which makes it read only.
if self.theHandle is not None:
self.redrawText()
else:
if self.theHandle is None:
self.clearEditor()
else:
self.redrawText()
return True
@@ -355,6 +355,7 @@ class GuiDocEditor(QTextEdit):
"""Redraw the text by marking the document content as "dirty".
"""
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
self.updateDocMargins()
return
def replaceText(self, theText):
+3 -2
View File
@@ -149,7 +149,7 @@ class GuiDocViewer(QTextBrowser):
# If we have a document open, we should reload it in case the font changed
if self.theHandle is not None:
self.redrawText()
self.reloadText()
return True
@@ -169,7 +169,7 @@ class GuiDocViewer(QTextBrowser):
sPos = self.verticalScrollBar().value()
aDoc = ToHtml(self.theProject, self.theParent)
aDoc.setPreview(True, self.mainConf.viewComments, self.mainConf.viewSynopsis)
aDoc.setPreview(self.mainConf.viewComments, self.mainConf.viewSynopsis)
aDoc.setLinkHeaders(True)
# Be extra careful here to prevent crashes when first opening a
@@ -233,6 +233,7 @@ class GuiDocViewer(QTextBrowser):
"""Redraw the text by marking the document content as "dirty".
"""
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
self.updateDocMargins()
return
def loadFromTag(self, theTag):
+26 -11
View File
@@ -598,8 +598,8 @@ class GuiMainMenu(QMenuBar):
# Insert > Separator
self.insertMenu.addSeparator()
# Insert > Keywords and Tags
self.mInsKeywords = self.insertMenu.addMenu("Keywords and Tags")
# Insert > Tags and References
self.mInsKeywords = self.insertMenu.addMenu("Tags and References")
self.mInsKWItems = {}
self.mInsKWItems[nwKeyWords.TAG_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, G")
self.mInsKWItems[nwKeyWords.POV_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, V")
@@ -908,7 +908,7 @@ class GuiMainMenu(QMenuBar):
self.helpMenu.addAction(self.aHelpLoc)
self.aHelpWeb = QAction("Documentation (Online)", self)
self.aHelpWeb.setStatusTip("View online documentation")
self.aHelpWeb.setStatusTip("View online documentation at %s" % nw.__docurl__)
self.aHelpWeb.triggered.connect(lambda: self._openWebsite(nw.__docurl__))
if self.mainConf.hasHelp and self.mainConf.hasAssistant:
self.aHelpWeb.setShortcut("Shift+F1")
@@ -916,18 +916,33 @@ class GuiMainMenu(QMenuBar):
self.aHelpWeb.setShortcuts(["F1", "Shift+F1"])
self.helpMenu.addAction(self.aHelpWeb)
# Document > Go to Website
self.aWebsite = QAction("Open the novelWriter Website", self)
self.aWebsite.setStatusTip("View the main website")
self.aWebsite.triggered.connect(lambda: self._openWebsite(nw.__url__))
self.helpMenu.addAction(self.aWebsite)
# Help > Separator
self.helpMenu.addSeparator()
# Document > Report Issue
self.aIssue = QAction("Report an Issue", self)
self.aIssue.setStatusTip("Report a bug or issue on GitHub")
# Document > Report an Issue
self.aIssue = QAction("Report an Issue (GitHub)", self)
self.aIssue.setStatusTip("Report a bug or issue on GitHub at %s" % nw.__issuesurl__)
self.aIssue.triggered.connect(lambda: self._openWebsite(nw.__issuesurl__))
self.helpMenu.addAction(self.aIssue)
# Document > Ask a Question
self.aQuestion = QAction("Ask a Question (GitHub)", self)
self.aQuestion.setStatusTip("Ask a question on GitHub at %s" % nw.__helpurl__)
self.aQuestion.triggered.connect(lambda: self._openWebsite(nw.__helpurl__))
self.helpMenu.addAction(self.aQuestion)
# Document > Latest Release
self.aRelease = QAction("Latest Release (GitHub)", self)
self.aRelease.setStatusTip("Open the Releases page on GitHub at %s" % nw.__releaseurl__)
self.aRelease.triggered.connect(lambda: self._openWebsite(nw.__releaseurl__))
self.helpMenu.addAction(self.aRelease)
# Document > Main Website
self.aWebsite = QAction("The novelWriter Website", self)
self.aWebsite.setStatusTip("Open the novelWriter website at %s" % nw.__url__)
self.aWebsite.triggered.connect(lambda: self._openWebsite(nw.__url__))
self.helpMenu.addAction(self.aWebsite)
return
# END Class GuiMainMenu
+47 -35
View File
@@ -227,7 +227,7 @@ class GuiConfigEditGeneralTab(QWidget):
self.mainForm.addRow(
"Show full path in document header",
self.showFullPath,
"Shows the document title and parent folder names."
"Add the parent folder names to the header."
)
self.hideVScroll = QSwitch()
@@ -235,7 +235,7 @@ class GuiConfigEditGeneralTab(QWidget):
self.mainForm.addRow(
"Hide vertical scroll bars in main windows",
self.hideVScroll,
"Scrolling with mouse wheel and keys only."
"Scrolling available with mouse wheel and keys only."
)
self.hideHScroll = QSwitch()
@@ -243,7 +243,7 @@ class GuiConfigEditGeneralTab(QWidget):
self.mainForm.addRow(
"Hide horizontal scroll bars in main windows",
self.hideHScroll,
"Scrolling with mouse wheel and keys only."
"Scrolling available with mouse wheel and keys only."
)
return
@@ -325,8 +325,9 @@ class GuiConfigEditProjectsTab(QWidget):
self.autoSaveDoc.setSingleStep(1)
self.autoSaveDoc.setValue(self.mainConf.autoSaveDoc)
self.backupPathRow = self.mainForm.addRow(
"Save interval for the currently open document",
"Save document interval",
self.autoSaveDoc,
"How often the open document is automatically saved.",
theUnit="seconds"
)
@@ -337,8 +338,9 @@ class GuiConfigEditProjectsTab(QWidget):
self.autoSaveProj.setSingleStep(1)
self.autoSaveProj.setValue(self.mainConf.autoSaveProj)
self.backupPathRow = self.mainForm.addRow(
"Save interval for the currently open project",
"Save project interval",
self.autoSaveProj,
"How often the open project is automatically saved.",
theUnit="seconds"
)
@@ -361,9 +363,9 @@ class GuiConfigEditProjectsTab(QWidget):
self.backupOnClose.setChecked(self.mainConf.backupOnClose)
self.backupOnClose.toggled.connect(self._toggledBackupOnClose)
self.mainForm.addRow(
"Run backup when closing project",
"Run backup when the project is closed",
self.backupOnClose,
"This option can be overridden in project settings."
"Can be overridden for individual projects in project settings."
)
## Ask before backup
@@ -373,7 +375,8 @@ class GuiConfigEditProjectsTab(QWidget):
self.askBeforeBackup.setEnabled(self.mainConf.backupOnClose)
self.mainForm.addRow(
"Ask before running backup",
self.askBeforeBackup
self.askBeforeBackup,
"Disabling this will cause backups to run in the background."
)
return
@@ -426,7 +429,7 @@ class GuiConfigEditProjectsTab(QWidget):
def _toggledBackupOnClose(self, theState):
"""Enable or disable switch that depends on the backup on close
switch,
switch.
"""
self.askBeforeBackup.setEnabled(theState)
return
@@ -449,7 +452,7 @@ class GuiConfigEditLayoutTab(QWidget):
# Text Style
# ==========
self.mainForm.addGroupLabel("Text Style")
self.mainForm.addGroupLabel("Document Text Style")
## Font Family
self.textStyleFont = QLineEdit()
@@ -475,12 +478,13 @@ class GuiConfigEditLayoutTab(QWidget):
self.mainForm.addRow(
"Font size",
self.textStyleSize,
"Font size for the document editor and viewer.",
theUnit = "pt"
)
# Text Flow
# =========
self.mainForm.addGroupLabel("Text Flow")
self.mainForm.addGroupLabel("Document Text Flow")
## Max Text Width in Normal Mode
self.textFlowMax = QSpinBox(self)
@@ -491,6 +495,7 @@ class GuiConfigEditLayoutTab(QWidget):
self.mainForm.addRow(
"Maximum text width in \"Normal Mode\"",
self.textFlowMax,
"Horizontal margins are scaled automatically.",
theUnit="px"
)
@@ -503,6 +508,7 @@ class GuiConfigEditLayoutTab(QWidget):
self.mainForm.addRow(
"Maximum text width in \"Focus Mode\"",
self.focusDocWidth,
"Horizontal margins are scaled automatically.",
theUnit="px"
)
@@ -512,7 +518,7 @@ class GuiConfigEditLayoutTab(QWidget):
self.mainForm.addRow(
"Disable maximum text width in \"Normal Mode\"",
self.textFlowFixed,
"If disabled, minimum text width is defined by the margin setting."
"If disabled, minimum text width is defined by the margin."
)
## Focus Mode Footer
@@ -520,7 +526,8 @@ class GuiConfigEditLayoutTab(QWidget):
self.hideFocusFooter.setChecked(self.mainConf.hideFocusFooter)
self.mainForm.addRow(
"Hide document footer in \"Focus Mode\"",
self.hideFocusFooter
self.hideFocusFooter,
"Hide the information bar at the bottom of the document."
)
## Justify Text
@@ -528,7 +535,8 @@ class GuiConfigEditLayoutTab(QWidget):
self.textJustify.setChecked(self.mainConf.textFixedW)
self.mainForm.addRow(
"Justify the text margins in editor and viewer",
self.textJustify
self.textJustify,
"Lay out text with straight edges in the editor and viewer."
)
## Document Margins
@@ -538,9 +546,9 @@ class GuiConfigEditLayoutTab(QWidget):
self.textMargin.setSingleStep(1)
self.textMargin.setValue(self.mainConf.textMargin)
self.mainForm.addRow(
"Document text margin",
"Text margin",
self.textMargin,
"If max width is enabled, this is the minimum margin.",
"If maximum width is set, this becomes the minimum margin.",
theUnit="px"
)
@@ -551,8 +559,9 @@ class GuiConfigEditLayoutTab(QWidget):
self.tabWidth.setSingleStep(1)
self.tabWidth.setValue(self.mainConf.tabWidth)
self.mainForm.addRow(
"Document tab width",
"Tab width",
self.tabWidth,
"The width of a tab key press in the editor and viewer.",
theUnit="px"
)
@@ -566,7 +575,7 @@ class GuiConfigEditLayoutTab(QWidget):
self.mainForm.addRow(
"Scroll past end of the document",
self.scrollPastEnd,
"Allows scrolling until last line is at the top."
"Allow scrolling until the last line is centred in the editor."
)
## Typewriter Scrolling
@@ -575,7 +584,7 @@ class GuiConfigEditLayoutTab(QWidget):
self.mainForm.addRow(
"Typewriter style scrolling when you type",
self.autoScroll,
"Tries to keep the cursor at a fixed vertical position."
"Try to keep the cursor at a fixed vertical position."
)
## Font Size
@@ -678,7 +687,7 @@ class GuiConfigEditEditingTab(QWidget):
self.mainForm.addRow(
"Highlight theme",
self.selectSyntax,
""
"Colour theme to apply to the editor and viewer."
)
self.highlightQuotes = QSwitch()
@@ -686,7 +695,7 @@ class GuiConfigEditEditingTab(QWidget):
self.mainForm.addRow(
"Highlight text wrapped in quotes",
self.highlightQuotes,
helpText="Applies to single, double and straight quotes."
"Applies to single, double and straight quotes."
)
self.highlightEmph = QSwitch()
@@ -694,7 +703,7 @@ class GuiConfigEditEditingTab(QWidget):
self.mainForm.addRow(
"Add highlight colour to emphasised text",
self.highlightEmph,
helpText="Applies to emphasis, strong and strikethrough."
"Applies to emphasis, strong and strikethrough."
)
# Spell Checking
@@ -724,7 +733,8 @@ class GuiConfigEditEditingTab(QWidget):
)
self.mainForm.addRow(
"Spell check language",
self.spellLangList
self.spellLangList,
"Available languages are determined by your system."
)
## Big Document Size Limit
@@ -736,7 +746,7 @@ class GuiConfigEditEditingTab(QWidget):
self.mainForm.addRow(
"Big document limit",
self.bigDocLimit,
"Disables full spell checking over the size limit.",
"Full spell checking is disabled above this limit.",
theUnit="kB"
)
@@ -749,7 +759,8 @@ class GuiConfigEditEditingTab(QWidget):
self.showTabsNSpaces.setChecked(self.mainConf.showTabsNSpaces)
self.mainForm.addRow(
"Show tabs and spaces",
self.showTabsNSpaces
self.showTabsNSpaces,
"Add symbols to indicate tabs and spaces in the editor."
)
## Show Line Endings
@@ -757,7 +768,8 @@ class GuiConfigEditEditingTab(QWidget):
self.showLineEndings.setChecked(self.mainConf.showLineEndings)
self.mainForm.addRow(
"Show line endings",
self.showLineEndings
self.showLineEndings,
"Add a symbol to indicate line endings in the editor."
)
return
@@ -866,7 +878,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Auto-replace text as you type",
self.autoReplaceMain,
"Apply formatting to word under cursor if no selection is made."
"Allow the editor to replace symbols as you type."
)
# Auto-Replace
@@ -880,7 +892,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Auto-replace single quotes",
self.autoReplaceSQ,
"The feature will try to guess opening or closing single quote."
"Try to guess which is an opening or a closing single quote."
)
## Auto-Replace Double Quotes
@@ -890,7 +902,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Auto-replace double quotes",
self.autoReplaceDQ,
"The feature will try to guess opening or closing quote quote."
"Try to guess which is an opening or a closing double quote."
)
## Auto-Replace Hyphens
@@ -900,7 +912,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Auto-replace dashes",
self.autoReplaceDash,
"Auto-replace double and triple hyphens with short and long dash."
"Double and triple hyphens become short and long dashes."
)
## Auto-Replace Dots
@@ -910,7 +922,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Auto-replace dots",
self.autoReplaceDots,
"Auto-replace three dots with ellipsis."
"Three consecutive dots becomes ellipsis."
)
# Quotation Style
@@ -934,7 +946,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Single quote open style",
self.quoteSym["SO"],
"Auto-replaces apostrophe before words.",
"The symbol to use for a leading single quote.",
theButton=self.btnSingleStyleO
)
@@ -950,7 +962,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Single quote close style",
self.quoteSym["SC"],
"Auto-replaces apostrophe after words.",
"The symbol to use for a trailing single quote.",
theButton=self.btnSingleStyleC
)
@@ -967,7 +979,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Double quote open style",
self.quoteSym["DO"],
"Auto-replaces straight quotes before words.",
"The symbol to use for a leading double quote.",
theButton=self.btnDoubleStyleO
)
@@ -983,7 +995,7 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addRow(
"Double quote close style",
self.quoteSym["DC"],
"Auto-replaces straight quotes after words.",
"The symbol to use for a trailing double quote.",
theButton=self.btnDoubleStyleC
)
+47 -43
View File
@@ -286,36 +286,36 @@ class GuiProjectTree(QTreeWidget):
logger.error("No project open")
return False
hasFocus = qApp.focusWidget() == self or not self.mainConf.showGUI
if hasFocus and self.theParent.hasProject:
if qApp.focusWidget() != self and self.mainConf.showGUI:
return False
tHandle = self.getSelectedHandle()
tItem = self._getTreeItem(tHandle)
pItem = tItem.parent()
if pItem is None:
tIndex = self.indexOfTopLevelItem(tItem)
nChild = self.topLevelItemCount()
nIndex = tIndex + nStep
if nIndex < 0 or nIndex >= nChild:
return False
cItem = self.takeTopLevelItem(tIndex)
self.insertTopLevelItem(nIndex, cItem)
tHandle = self.getSelectedHandle()
tItem = self._getTreeItem(tHandle)
if tItem is None:
return False
else:
tIndex = pItem.indexOfChild(tItem)
nChild = pItem.childCount()
nIndex = tIndex + nStep
if nIndex < 0 or nIndex >= nChild:
return False
cItem = pItem.takeChild(tIndex)
pItem.insertChild(nIndex, cItem)
self.clearSelection()
cItem.setSelected(True)
self._setTreeChanged(True)
pItem = tItem.parent()
if pItem is None:
tIndex = self.indexOfTopLevelItem(tItem)
nChild = self.topLevelItemCount()
nIndex = tIndex + nStep
if nIndex < 0 or nIndex >= nChild:
return False
cItem = self.takeTopLevelItem(tIndex)
self.insertTopLevelItem(nIndex, cItem)
else:
return False
tIndex = pItem.indexOfChild(tItem)
nChild = pItem.childCount()
nIndex = tIndex + nStep
if nIndex < 0 or nIndex >= nChild:
return False
cItem = pItem.takeChild(tIndex)
pItem.insertChild(nIndex, cItem)
self.clearSelection()
cItem.setSelected(True)
self._setTreeChanged(True)
return True
@@ -431,7 +431,7 @@ class GuiProjectTree(QTreeWidget):
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.projTree[tHandle]
if nwItemS is None:
if trItemS is None or nwItemS is None:
return False
wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole))
@@ -582,18 +582,23 @@ class GuiProjectTree(QTreeWidget):
properly reported to the function.
"""
tItem = self._getTreeItem(tHandle)
if tItem is not None:
tItem.setText(self.C_COUNT, f"{theCount:n}")
tItem.setData(self.C_COUNT, Qt.UserRole, int(theCount))
pItem = tItem.parent()
if pItem is not None:
pCount = 0
for i in range(pItem.childCount()):
pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
pHandle = pItem.data(self.C_NAME, Qt.UserRole)
if tItem is None:
return
if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "":
self.propagateCount(pHandle, pCount, nDepth+1)
tItem.setText(self.C_COUNT, f"{theCount:n}")
tItem.setData(self.C_COUNT, Qt.UserRole, int(theCount))
pItem = tItem.parent()
if pItem is None:
return
pCount = 0
for i in range(pItem.childCount()):
pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
pHandle = pItem.data(self.C_NAME, Qt.UserRole)
if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "":
self.propagateCount(pHandle, pCount, nDepth+1)
return
@@ -1024,12 +1029,12 @@ class GuiProjectTreeMenu(QMenu):
return
def filterActions(self, theItem):
"""Update item settings from the nwItem.
"""Filter the menu entries available based on the properties of
the item the menu was activated on.
"""
self.theItem = theItem
theRoot = self.theTree.theProject.projTree.getRootItem(theItem.itemHandle)
if theItem is None or theRoot is None:
if theItem is None:
logger.error("Failed to extract information to build tree context menu")
return False
@@ -1038,14 +1043,13 @@ class GuiProjectTreeMenu(QMenu):
inTrash = theItem.itemParent == trashHandle and trashHandle is not None
isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
isFile = theItem.itemType == nwItemType.FILE
isArch = theRoot.itemClass == nwItemClass.ARCHIVE
isOrph = isFile and theItem.itemParent is None
showOpen = isFile
showView = isFile
showEdit = not isTrash and not isOrph
showExport = isFile
showNewFile = not (isTrash or inTrash or isOrph or isArch)
showNewFile = not (isTrash or inTrash or isOrph)
showNewFolder = not (isTrash or inTrash or isOrph)
showDelete = not isTrash
showEmpty = isTrash
+7 -7
View File
@@ -281,11 +281,11 @@ class GuiTheme:
cnfSec = "Main"
if confParser.has_section(cnfSec):
self.themeName = self._parseLine(confParser, cnfSec, "name", "")
self.themeDescription = self._parseLine(confParser, cnfSec, "description", "")
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "")
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "")
self.themeDescription = self._parseLine(confParser, cnfSec, "description", "N/A")
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "N/A")
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "N/A")
self.themeUrl = self._parseLine(confParser, cnfSec, "url", "")
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "")
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "N/A")
self.themeLicenseUrl = self._parseLine(confParser, cnfSec, "licenseurl", "")
## Palette
@@ -638,10 +638,10 @@ class GuiIcons:
if confParser.has_section(cnfSec):
self.themeName = self._parseLine(confParser, cnfSec, "name", "")
self.themeDescription = self._parseLine(confParser, cnfSec, "description", "")
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "")
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "")
self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "N/A")
self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "N/A")
self.themeUrl = self._parseLine(confParser, cnfSec, "url", "")
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "")
self.themeLicense = self._parseLine(confParser, cnfSec, "license", "N/A")
self.themeLicenseUrl = self._parseLine(confParser, cnfSec, "licenseurl", "")
## Palette
+26 -6
View File
@@ -81,14 +81,14 @@ class GuiMain(QMainWindow):
# Core Classes
# ============
# Core Classes and settings
# Core Classes and Settings
self.theTheme = GuiTheme(self)
self.theProject = NWProject(self)
self.theIndex = NWIndex(self.theProject, self)
self.hasProject = False
self.isFocusMode = False
# Prepare main window
# Prepare Main Window
self.resize(*self.mainConf.getWinSize())
self._setWindowTitle()
self.setWindowIcon(QIcon(self.mainConf.appIcon))
@@ -238,6 +238,12 @@ class GuiMain(QMainWindow):
if self.mainConf.showGUI:
self.showProjectLoadDialog()
# Show the latest release notes, if they haven't been shown before
if self.mainConf.lastNotes != nw.__version__:
if self.mainConf.showGUI:
self.showAboutNWDialog(showNotes=True)
self.mainConf.lastNotes = nw.__version__
logger.debug("novelWriter is ready ...")
self.setStatus("novelWriter is ready ...")
@@ -915,11 +921,18 @@ class GuiMain(QMainWindow):
return
def showAboutNWDialog(self):
def showAboutNWDialog(self, showNotes=False):
"""Show the about dialog for novelWriter.
"""
dlgAbout = GuiAbout(self)
dlgAbout.exec_()
dlgAbout.setModal(True)
dlgAbout.show()
qApp.processEvents()
dlgAbout.populateGUI()
if showNotes:
dlgAbout.showReleaseNotes()
return
def showAboutQtDialog(self):
@@ -969,6 +982,13 @@ class GuiMain(QMainWindow):
return
def askQuestion(self, theTitle, theQuestion):
"""Ask the user a Yes/No question.
"""
msgBox = QMessageBox()
msgRes = msgBox.question(self, theTitle, theQuestion)
return msgRes == QMessageBox.Yes
def reportConfErr(self):
"""Checks if the Config module has any errors to report, and let
the user know if this is the case. The Config module caches
@@ -1060,10 +1080,10 @@ class GuiMain(QMainWindow):
self.isFocusMode = not self.isFocusMode
if self.isFocusMode:
logger.debug("Activating Focus mode")
logger.debug("Activating Focus Mode")
self.tabWidget.setCurrentWidget(self.splitDocs)
else:
logger.debug("Deactivating Focus mode")
logger.debug("Deactivating Focus Mode")
isVisible = not self.isFocusMode
self.treePane.setVisible(isVisible)
+1 -2
View File
@@ -1,7 +1,6 @@
[pytest]
markers =
project: Project classes tests
error: Test various error handling scenarios
base: Base functionality tests
core: Core functionality tests
gui: Qt5 GUI tests
serial
Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+9 -1
View File
@@ -27,11 +27,12 @@ UsedUserAreasWarning=no
;PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
OutputDir={#nwAppDir}
OutputBaseFilename=novelwriter_{#nwAppVersion}_win_amd64_setup
OutputBaseFilename=novelwriter-{#nwAppVersion}-win10-amd64-setup
Compression=lzma
SolidCompression=yes
WizardStyle=modern
ArchitecturesInstallIn64BitMode=x64
ChangesAssociations=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
@@ -50,3 +51,10 @@ Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#nwAppName}"; Fil
[Run]
Filename: "{app}\{#nwAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(nwAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[Registry]
Root: HKA; Subkey: "Software\Classes\.nwx\OpenWithProgids"; ValueType: string; ValueName: "novelWriterProject.nwx"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx"; ValueType: string; ValueName: ""; ValueData: "novelWriter Project File"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\assets\icons\x-novelwriter-project.ico"
Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\novelWriter.exe"" ""%1"""
Root: HKA; Subkey: "Software\Classes\Applications\novelWriter.exe\SupportedTypes"; ValueType: string; ValueName: ".nwx"; ValueData: ""
+78
View File
@@ -0,0 +1,78 @@
# novelWriter Tests
The test suite uses PyTest for testing.
## Dependencies
* `python3-pytest` for the basic framework (required)
* `python3-pytestqt` for Qt support (required)
* `python3-pytest-cov` for code coverage reports (optional)
* `python3-pytest-xvfb` for headless tests (optional)
## HowTo
### Basic Usage
To run all tests, type:
```bash
pytest-3 -v
```
The `-v` switch enables verbose mode, with one test per line.
For a more compact view, omit this switch.
### Headless
To run tests in headless mode, either use the Qt `offscreen` mode:
```bash
export QT_QPA_PLATFORM=offscreen
```
or run with `xvfb`:
```bash
xvfb-run pytest-3 -v
```
### Test Coverage
To add test coverage, run the following:
```bash
pytest-3 -v --cov=nw --cov-report=html
```
The `--cov-report` switch generates an html report, omit it to print a coverage summary to the terminal.
The html coverage report will be available in the `htmlcov` folder.
### Test Markers (Categories)
To run with specific test markers, add the `-m` switch:
```bash
pytest-3 -v -m core
```
Available markers are:
* '`core`' for unit tests covering the classes in the `nw/core` folder
## Tests
To filter specific groups of tests, use the `-k` switch.
The commands for the respective test categories are listed below.
| Type | Test Target | Source File(s) | Marker | Filter |
| :--- | :----------------- | :-------------------- | :-------- | :-------------------- |
| Unit | Main function | nw/\_\_init\_\_.py | `-m base` | `-k testBaseInit` |
| Unit | Common functions | nw/common.py | `-m base` | `-k testBaseCommon` |
| Unit | Config class | nw/config.py | `-m base` | `-k testBaseConfig` |
| Unit | Error handlers | nw/error.py | `-m base` | `-k testBaseError` |
| Unit | Core functions | nw/core/tools.py | `-m core` | `-k testCoreTools` |
| Unit | NWDoc class | nw/core/document.py | `-m core` | `-k testCoreDocument` |
| Unit | NWIndex class | nw/core/index.py | `-m core` | `-k testCoreIndex` |
| Unit | NWItem class | nw/core/item.py | `-m core` | `-k testCoreItem` |
| Unit | NWProject class | nw/core/project.py | `-m core` | `-k testCoreProject` |
| Unit | NWSpell* classes | nw/core/spellcheck.py | `-m core` | `-k testCoreSpell` |
| Unit | NWStatus class | nw/core/status.py | `-m core` | `-k testCoreStatus` |
| Unit | NWTree class | nw/core/tree.py | `-m core` | `-k testCoreTree` |
| Unit | OptionsState class | nw/core/options.py | `-m core` | `-k testCoreOptions` |
| Unit | ToHtml class | nw/core/tohtml.py | `-m core` | `-k testCoreToHtml` |
| Unit | Tokenizer class | nw/core/tokenizer.py | `-m core` | `-k testCoreToken` |
+76 -78
View File
@@ -7,7 +7,7 @@ import pytest
import shutil
import os
from nwdummy import DummyMain
from dummy import DummyMain
from PyQt5.QtWidgets import QMessageBox
@@ -20,94 +20,42 @@ from nw.config import Config # noqa: E402
##
@pytest.fixture(scope="session")
def nwTemp():
def tmpDir():
"""A temporary folder for the test session. This folder is
presistent after the test so that the status of generated files can
be checked. The folder is instead cleared before a new test session.
"""
testDir = os.path.dirname(__file__)
tempDir = os.path.join(testDir, "temp")
if os.path.isdir(tempDir):
shutil.rmtree(tempDir)
if not os.path.isdir(tempDir):
os.mkdir(tempDir)
return tempDir
theDir = os.path.join(testDir, "temp")
if os.path.isdir(theDir):
shutil.rmtree(theDir)
if not os.path.isdir(theDir):
os.mkdir(theDir)
return theDir
@pytest.fixture(scope="session")
def nwRef():
def refDir():
"""The folder where all the reference files are stored for verifying
the results of tests.
"""
testDir = os.path.dirname(__file__)
refDir = os.path.join(testDir, "reference")
return refDir
##
# novelWriter Objects
##
theDir = os.path.join(testDir, "reference")
return theDir
@pytest.fixture(scope="session")
def tmpConf(nwTemp):
"""Create a temporary novelWriter configuration object.
def outDir(tmpDir):
"""An output folder for test results
"""
theConf = Config()
theConf.initConfig(nwTemp, nwTemp)
theConf.setLastPath("")
return theConf
@pytest.fixture(scope="session")
def nwConf(nwRef, nwTemp):
"""Temporary novelWriter configuration used for the dummy instance
of novelWriter's main GUI.
"""
theConf = Config()
theConf.initConfig(nwRef, nwTemp)
return theConf
@pytest.fixture(scope="session")
def nwDummy(nwRef, nwTemp, nwConf):
"""Create a dummy instance of novelWriter's main GUI class.
"""
theDummy = DummyMain()
theDummy.mainConf = nwConf
return theDummy
##
# Temporary Test Folders
##
@pytest.fixture(scope="session")
def nwTempProj(nwTemp):
"""A temporary folder for project tests.
"""
projDir = os.path.join(nwTemp, "proj")
if not os.path.isdir(projDir):
os.mkdir(projDir)
return projDir
@pytest.fixture(scope="session")
def nwTempGUI(nwTemp):
"""A temporary folder for GUI tests.
"""
guiDir = os.path.join(nwTemp, "gui")
if not os.path.isdir(guiDir):
os.mkdir(guiDir)
return guiDir
@pytest.fixture(scope="session")
def nwTempBuild(nwTemp):
"""A temporary folder for build tests.
"""
buildDir = os.path.join(nwTemp, "build")
if not os.path.isdir(buildDir):
os.mkdir(buildDir)
return buildDir
theDir = os.path.join(tmpDir, "results")
if not os.path.isdir(theDir):
os.mkdir(theDir)
return theDir
@pytest.fixture(scope="function")
def nwFuncTemp(nwTemp):
def fncDir(tmpDir):
"""A temporary folder for a single test function.
"""
funcDir = os.path.join(nwTemp, "ftemp")
funcDir = os.path.join(tmpDir, "ftemp")
if os.path.isdir(funcDir):
shutil.rmtree(funcDir)
if not os.path.isdir(funcDir):
@@ -118,16 +66,40 @@ def nwFuncTemp(nwTemp):
return
##
# Temp Folders for Projects
# novelWriter Objects
##
@pytest.fixture(scope="function")
def nwMinimal(nwTemp):
def tmpConf(tmpDir):
"""Create a temporary novelWriter configuration object.
"""
confFile = os.path.join(tmpDir, "novelwriter.conf")
if os.path.isfile(confFile):
os.unlink(confFile)
theConf = Config()
theConf.initConfig(tmpDir, tmpDir)
theConf.setLastPath("")
return theConf
@pytest.fixture(scope="function")
def dummyGUI(tmpConf):
"""Create a dummy instance of novelWriter's main GUI class.
"""
theDummy = DummyMain()
theDummy.mainConf = tmpConf
return theDummy
##
# Temp Project Folders
##
@pytest.fixture(scope="function")
def nwMinimal(tmpDir):
"""A minimal novelWriter example project.
"""
testDir = os.path.dirname(__file__)
minimalStore = os.path.join(testDir, "minimal")
minimalDir = os.path.join(nwTemp, "minimal")
minimalDir = os.path.join(tmpDir, "minimal")
if os.path.isdir(minimalDir):
shutil.rmtree(minimalDir)
shutil.copytree(minimalStore, minimalDir)
@@ -143,13 +115,13 @@ def nwMinimal(nwTemp):
return
@pytest.fixture(scope="function")
def nwLipsum(nwTemp):
def nwLipsum(tmpDir):
"""A medium sized novelWriter example project with a lot of Lorem
Ipsum dummy text.
"""
testDir = os.path.dirname(__file__)
lipsumStore = os.path.join(testDir, "lipsum")
lipsumDir = os.path.join(nwTemp, "lipsum")
lipsumDir = os.path.join(tmpDir, "lipsum")
if os.path.isdir(lipsumDir):
shutil.rmtree(lipsumDir)
shutil.copytree(lipsumStore, lipsumDir)
@@ -165,12 +137,12 @@ def nwLipsum(nwTemp):
return
@pytest.fixture(scope="function")
def nwOldProj(nwTemp):
def nwOldProj(tmpDir):
"""A minimal movelWriter project using the old folder structure.
"""
testDir = os.path.dirname(__file__)
oldProjStore = os.path.join(testDir, "oldproj")
oldProjDir = os.path.join(nwTemp, "oldproj")
oldProjDir = os.path.join(tmpDir, "oldproj")
if os.path.isdir(oldProjDir):
shutil.rmtree(oldProjDir)
shutil.copytree(oldProjStore, oldProjDir)
@@ -199,4 +171,30 @@ def yesToAll(monkeypatch):
monkeypatch.setattr(
QMessageBox, "critical", lambda *args, **kwargs: QMessageBox.Yes
)
yield
monkeypatch.undo()
return
# =============================================================================================== #
##
# Temporary Test Folders
##
@pytest.fixture(scope="session")
def nwTempGUI(tmpDir):
"""A temporary folder for GUI tests.
"""
guiDir = os.path.join(tmpDir, "gui")
if not os.path.isdir(guiDir):
os.mkdir(guiDir)
return guiDir
@pytest.fixture(scope="session")
def nwTempBuild(tmpDir):
"""A temporary folder for build tests.
"""
buildDir = os.path.join(tmpDir, "build")
if not os.path.isdir(buildDir):
os.mkdir(buildDir)
return buildDir
+82
View File
@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
"""novelWriter Test Dummy GUI Classes
"""
# =========================================================================== #
# Mock GUI
# =========================================================================== #
class DummyMain():
def __init__(self):
self.mainConf = None
self.hasProject = True
self.theIndex = None
self.theProject = None
self.statusBar = DummyStatusBar()
# Test Variables
self.askResponse = True
self.lastAlert = ""
return
def makeAlert(self, theMessage, theLevel):
print("%s: %s" % (str(theLevel), theMessage))
self.lastAlert = str(theMessage)
return
def askQuestion(self, theTitle, theQustion):
print("Question: %s" % theQustion)
return self.askResponse
def setStatus(self, theMessage):
return
def setProjectStatus(self, isChanged):
return
def openProject(self, projPath):
return
def rebuildIndex(self):
return
def closeMain(self):
return "closeMain"
def close(self):
return "close"
# Test Functions
def undo(self):
self.askResponse = True
return
def clear(self):
self.lastAlert = ""
return
# END Class DummyMain
class DummyStatusBar():
def __init__(self):
return
def setStatus(self, theText):
return
# END Class DummyStatusBar
# =========================================================================== #
# Error Functions
# Dummy functions that will raise errors instead.
# =========================================================================== #
def causeOSError(*args, **kwargs):
raise OSError
def causeException(*args, **kwargs):
raise Exception
-77
View File
@@ -1,77 +0,0 @@
[
[
"content/04468803b92e1.nwd",
"WORLD",
"Ancient Europe"
],
[
"content/2426c6f0ca922.nwd",
"PLOT",
"Main"
],
[
"content/441420a886d82.nwd",
"NOVEL",
"Chapter Two"
],
[
"content/47666c91c7ccf.nwd",
"NOVEL",
"Scene Five"
],
[
"content/4c4f28287af27.nwd",
"CHARACTER",
"Mr. Nobody"
],
[
"content/7a992350f3eb6.nwd",
"NOVEL",
"Lorem Ipsum"
],
[
"content/846352075de7d.nwd",
"NOVEL",
"Interlude"
],
[
"content/88243afbe5ed8.nwd",
"NOVEL",
"Scene One"
],
[
"content/88d59a277361b.nwd",
"NOVEL",
"Prologue"
],
[
"content/8c58a65414c23.nwd",
"NOVEL",
"Front Matter"
],
[
"content/db7e733775d4d.nwd",
"NOVEL",
"Act One"
],
[
"content/eb103bc70c90c.nwd",
"NOVEL",
"Scene Three"
],
[
"content/f8c0562e50f1b.nwd",
"NOVEL",
"Scene Four"
],
[
"content/f96ec11c6a3da.nwd",
"NOVEL",
"Scene Two"
],
[
"content/fb609cd8319dc.nwd",
"NOVEL",
"Chapter One"
]
]
-17
View File
@@ -1,17 +0,0 @@
[
[
"content/8c659a11cd429.nwd",
"NOVEL",
"New Scene"
],
[
"content/a35baf2e93843.nwd",
"NOVEL",
"Title Page"
],
[
"content/f5ab3e30151e1.nwd",
"NOVEL",
"New Chapter"
]
]
-39
View File
@@ -1,39 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Test Dummy GUI Classes
"""
class DummyMain():
def __init__(self):
self.mainConf = None
self.hasProject = True
self.statusBar = StatusBar()
return
def makeAlert(self, theMessage, theLevel):
print("%s: %s" % (str(theLevel), theMessage))
return
def setStatus(self, theMessage):
return
def setProjectStatus(self, isChanged):
return
def openProject(self, projPath):
return
def rebuildIndex(self):
return
# END Class GuiMain
class StatusBar():
def __init__(self):
return
def setStatus(self, theText):
return
# END Class StatusBar
@@ -6,6 +6,7 @@ icons = typicons_colour_light
guidark = False
guifont =
guifontsize = 11
lastnotes = 1.0
[Sizes]
geometry = 1200, 650
@@ -41,56 +41,56 @@
</importance>
</settings>
<content count="23">
<item handle="73475cb40a568" order="None" parent="None">
<item handle="73475cb40a568" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="44cb730c42048" order="None" parent="None">
<item handle="44cb730c42048" order="0" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="71ee45a3c0db9" order="None" parent="None">
<item handle="71ee45a3c0db9" order="0" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="811786ad1ae74" order="None" parent="None">
<item handle="811786ad1ae74" order="0" parent="None">
<name>Locations</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="25fc0e7096fc6" order="None" parent="None">
<item handle="25fc0e7096fc6" order="0" parent="None">
<name>Timeline</name>
<type>ROOT</type>
<class>TIMELINE</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="31489056e0916" order="None" parent="None">
<item handle="31489056e0916" order="0" parent="None">
<name>Objects</name>
<type>ROOT</type>
<class>OBJECT</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="98010bd9270f9" order="None" parent="None">
<item handle="98010bd9270f9" order="0" parent="None">
<name>Entity</name>
<type>ROOT</type>
<class>ENTITY</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="0e17daca5f3e1" order="None" parent="73475cb40a568">
<item handle="0e17daca5f3e1" order="0" parent="73475cb40a568">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -102,14 +102,14 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="1a6562590ef19" order="None" parent="73475cb40a568">
<item handle="1a6562590ef19" order="0" parent="73475cb40a568">
<name>Chapter 1</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="031b4af5197ec" order="None" parent="1a6562590ef19">
<item handle="031b4af5197ec" order="0" parent="1a6562590ef19">
<name>Chapter 1</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -121,7 +121,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="41cfc0d1f2d12" order="None" parent="1a6562590ef19">
<item handle="41cfc0d1f2d12" order="0" parent="1a6562590ef19">
<name>Scene 1.1</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -133,7 +133,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="2858dcd1057d3" order="None" parent="1a6562590ef19">
<item handle="2858dcd1057d3" order="0" parent="1a6562590ef19">
<name>Scene 1.2</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -145,7 +145,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="2fca346db6561" order="None" parent="1a6562590ef19">
<item handle="2fca346db6561" order="0" parent="1a6562590ef19">
<name>Scene 1.3</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -157,14 +157,14 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="02d20bbd7e394" order="None" parent="73475cb40a568">
<item handle="02d20bbd7e394" order="0" parent="73475cb40a568">
<name>Chapter 2</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="7688b6ef52555" order="None" parent="02d20bbd7e394">
<item handle="7688b6ef52555" order="0" parent="02d20bbd7e394">
<name>Chapter 2</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -176,7 +176,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="c837649cce43f" order="None" parent="02d20bbd7e394">
<item handle="c837649cce43f" order="0" parent="02d20bbd7e394">
<name>Scene 2.1</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -188,7 +188,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="6208ef0f7750c" order="None" parent="02d20bbd7e394">
<item handle="6208ef0f7750c" order="0" parent="02d20bbd7e394">
<name>Scene 2.2</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -200,7 +200,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="3e1e967e9b793" order="None" parent="02d20bbd7e394">
<item handle="3e1e967e9b793" order="0" parent="02d20bbd7e394">
<name>Scene 2.3</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -212,14 +212,14 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="39fa9ec190eee" order="None" parent="73475cb40a568">
<item handle="39fa9ec190eee" order="0" parent="73475cb40a568">
<name>Chapter 3</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="d029fa3a95e17" order="None" parent="39fa9ec190eee">
<item handle="d029fa3a95e17" order="0" parent="39fa9ec190eee">
<name>Chapter 3</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -231,7 +231,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="81b8a03f97e87" order="None" parent="39fa9ec190eee">
<item handle="81b8a03f97e87" order="0" parent="39fa9ec190eee">
<name>Scene 3.1</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -243,7 +243,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="da4ea2a5506f2" order="None" parent="39fa9ec190eee">
<item handle="da4ea2a5506f2" order="0" parent="39fa9ec190eee">
<name>Scene 3.2</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -255,7 +255,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="a68b412c42825" order="None" parent="39fa9ec190eee">
<item handle="a68b412c42825" order="0" parent="39fa9ec190eee">
<name>Scene 3.3</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -41,56 +41,56 @@
</importance>
</settings>
<content count="14">
<item handle="73475cb40a568" order="None" parent="None">
<item handle="73475cb40a568" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="44cb730c42048" order="None" parent="None">
<item handle="44cb730c42048" order="0" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="71ee45a3c0db9" order="None" parent="None">
<item handle="71ee45a3c0db9" order="0" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="811786ad1ae74" order="None" parent="None">
<item handle="811786ad1ae74" order="0" parent="None">
<name>Locations</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="25fc0e7096fc6" order="None" parent="None">
<item handle="25fc0e7096fc6" order="0" parent="None">
<name>Timeline</name>
<type>ROOT</type>
<class>TIMELINE</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="31489056e0916" order="None" parent="None">
<item handle="31489056e0916" order="0" parent="None">
<name>Objects</name>
<type>ROOT</type>
<class>OBJECT</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="98010bd9270f9" order="None" parent="None">
<item handle="98010bd9270f9" order="0" parent="None">
<name>Entity</name>
<type>ROOT</type>
<class>ENTITY</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="0e17daca5f3e1" order="None" parent="73475cb40a568">
<item handle="0e17daca5f3e1" order="0" parent="73475cb40a568">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -102,7 +102,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="1a6562590ef19" order="None" parent="73475cb40a568">
<item handle="1a6562590ef19" order="0" parent="73475cb40a568">
<name>Scene 1</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -114,7 +114,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="031b4af5197ec" order="None" parent="73475cb40a568">
<item handle="031b4af5197ec" order="0" parent="73475cb40a568">
<name>Scene 2</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -126,7 +126,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="41cfc0d1f2d12" order="None" parent="73475cb40a568">
<item handle="41cfc0d1f2d12" order="0" parent="73475cb40a568">
<name>Scene 3</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -138,7 +138,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="2858dcd1057d3" order="None" parent="73475cb40a568">
<item handle="2858dcd1057d3" order="0" parent="73475cb40a568">
<name>Scene 4</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -150,7 +150,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="2fca346db6561" order="None" parent="73475cb40a568">
<item handle="2fca346db6561" order="0" parent="73475cb40a568">
<name>Scene 5</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -162,7 +162,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="02d20bbd7e394" order="None" parent="73475cb40a568">
<item handle="02d20bbd7e394" order="0" parent="73475cb40a568">
<name>Scene 6</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -39,35 +39,35 @@
</importance>
</settings>
<content count="10">
<item handle="73475cb40a568" order="None" parent="None">
<item handle="73475cb40a568" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="44cb730c42048" order="None" parent="None">
<item handle="44cb730c42048" order="0" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="71ee45a3c0db9" order="None" parent="None">
<item handle="71ee45a3c0db9" order="0" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="811786ad1ae74" order="None" parent="None">
<item handle="811786ad1ae74" order="0" parent="None">
<name>World</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="25fc0e7096fc6" order="None" parent="73475cb40a568">
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -79,14 +79,14 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="31489056e0916" order="None" parent="73475cb40a568">
<item handle="31489056e0916" order="0" parent="73475cb40a568">
<name>New Chapter</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="98010bd9270f9" order="None" parent="31489056e0916">
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
<name>New Chapter</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -98,7 +98,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="0e17daca5f3e1" order="None" parent="31489056e0916">
<item handle="0e17daca5f3e1" order="0" parent="31489056e0916">
<name>New Scene</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -110,7 +110,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="1a6562590ef19" order="None" parent="31489056e0916">
<item handle="1a6562590ef19" order="0" parent="31489056e0916">
<name>Hello</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -122,7 +122,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="031b4af5197ec" order="None" parent="71ee45a3c0db9">
<item handle="031b4af5197ec" order="0" parent="71ee45a3c0db9">
<name>Jane</name>
<type>FILE</type>
<class>CHARACTER</class>
@@ -39,35 +39,35 @@
</importance>
</settings>
<content count="8">
<item handle="73475cb40a568" order="None" parent="None">
<item handle="73475cb40a568" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="44cb730c42048" order="None" parent="None">
<item handle="44cb730c42048" order="0" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="71ee45a3c0db9" order="None" parent="None">
<item handle="71ee45a3c0db9" order="0" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="811786ad1ae74" order="None" parent="None">
<item handle="811786ad1ae74" order="0" parent="None">
<name>World</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="25fc0e7096fc6" order="None" parent="73475cb40a568">
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -79,14 +79,14 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="31489056e0916" order="None" parent="73475cb40a568">
<item handle="31489056e0916" order="0" parent="73475cb40a568">
<name>New Chapter</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="98010bd9270f9" order="None" parent="31489056e0916">
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
<name>New Chapter</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -98,7 +98,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="0e17daca5f3e1" order="None" parent="31489056e0916">
<item handle="0e17daca5f3e1" order="0" parent="31489056e0916">
<name>New Scene</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -39,35 +39,35 @@
</importance>
</settings>
<content count="12">
<item handle="73475cb40a568" order="None" parent="None">
<item handle="73475cb40a568" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="44cb730c42048" order="None" parent="None">
<item handle="44cb730c42048" order="0" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="71ee45a3c0db9" order="None" parent="None">
<item handle="71ee45a3c0db9" order="0" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="811786ad1ae74" order="None" parent="None">
<item handle="811786ad1ae74" order="0" parent="None">
<name>World</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="25fc0e7096fc6" order="None" parent="73475cb40a568">
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -79,14 +79,14 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="31489056e0916" order="None" parent="73475cb40a568">
<item handle="31489056e0916" order="0" parent="73475cb40a568">
<name>New Chapter</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="98010bd9270f9" order="None" parent="31489056e0916">
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
<name>New Chapter</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -98,7 +98,7 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="0e17daca5f3e1" order="None" parent="31489056e0916">
<item handle="0e17daca5f3e1" order="0" parent="31489056e0916">
<name>New Scene</name>
<type>FILE</type>
<class>NOVEL</class>
@@ -110,28 +110,28 @@
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="1a6562590ef19" order="None" parent="None">
<item handle="1a6562590ef19" order="0" parent="None">
<name>Timeline</name>
<type>ROOT</type>
<class>TIMELINE</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="031b4af5197ec" order="None" parent="None">
<item handle="031b4af5197ec" order="0" parent="None">
<name>Object</name>
<type>ROOT</type>
<class>OBJECT</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="41cfc0d1f2d12" order="None" parent="None">
<item handle="41cfc0d1f2d12" order="0" parent="None">
<name>Custom1</name>
<type>ROOT</type>
<class>CUSTOM</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="2858dcd1057d3" order="None" parent="None">
<item handle="2858dcd1057d3" order="0" parent="None">
<name>Custom2</name>
<type>ROOT</type>
<class>CUSTOM</class>
+1
View File
@@ -6,6 +6,7 @@ icons = typicons_colour_light
guidark = True
guifont = Cantarell
guifontsize = 12
lastnotes = 1.0
[Sizes]
geometry = 1100, 650
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""novelWriter Common Class Tester
"""novelWriter Common Functions Tester
"""
import time
@@ -9,10 +9,12 @@ from nw.common import (
checkString, checkBool, checkInt, colRange, formatInt, transferCase,
fuzzyTime, checkHandle, formatTimeStamp, formatTime
)
from nwtools import cmpList
from tools import cmpList
@pytest.mark.core
def testCheckString():
@pytest.mark.base
def testBaseCommon_CheckString():
"""Test the checkString function.
"""
assert checkString(None, "NotNone", True) is None
assert checkString("None", "NotNone", True) is None
assert checkString("None", "NotNone", False) == "None"
@@ -21,8 +23,12 @@ def testCheckString():
assert checkString(1.0, "NotNone", False) == "NotNone"
assert checkString(True, "NotNone", False) == "NotNone"
@pytest.mark.core
def testCheckInt():
# END Test testBaseCommon_CheckString
@pytest.mark.base
def testBaseCommon_CheckInt():
"""Test the checkInt function.
"""
assert checkInt(None, 3, True) is None
assert checkInt("None", 3, True) is None
assert checkInt(None, 3, False) == 3
@@ -30,8 +36,12 @@ def testCheckInt():
assert checkInt(1.0, 3, False) == 1
assert checkInt(True, 3, False) == 1
@pytest.mark.core
def testCheckBool():
# END Test testBaseCommon_CheckInt
@pytest.mark.base
def testBaseCommon_CheckBool():
"""Test the checkBool function.
"""
assert checkBool(None, 3, True) is None
assert checkBool("None", 3, True) is None
assert checkBool("True", False, False)
@@ -44,8 +54,12 @@ def testCheckBool():
assert checkBool(1.0, None, False) is None
assert checkBool(2.0, None, False) is None
@pytest.mark.core
def testCheckHandle():
# END Test testBaseCommon_CheckBool
@pytest.mark.base
def testBaseCommon_CheckHandle():
"""Test the checkHandle function.
"""
assert checkHandle("None", 1, True) is None
assert checkHandle("None", 1, False) == 1
assert checkHandle(None, 1, True) is None
@@ -53,8 +67,12 @@ def testCheckHandle():
assert checkHandle("47666c91c7ccf", None, False) == "47666c91c7ccf"
assert checkHandle("h7666c91c7ccf", None, False) is None
@pytest.mark.core
def testColRange():
# END Test testBaseCommon_CheckHandle
@pytest.mark.base
def testBaseCommon_ColRange():
"""Test the colRange function.
"""
assert colRange([0, 0], [0, 0], 0) is None
assert cmpList(
colRange([200, 50, 0], [50, 200, 0], 1),
@@ -77,14 +95,22 @@ def testColRange():
[[200, 50, 0], [162, 87, 0], [124, 124, 0], [86, 161, 0], [50, 200, 0]]
)
@pytest.mark.core
def testFormatTimeStamp():
# END Test testBaseCommon_ColRange
@pytest.mark.base
def testBaseCommon_FormatTimeStamp():
"""Test the formatTimeStamp function.
"""
tTime = time.mktime(time.gmtime(0))
assert formatTimeStamp(tTime, False) == "1970-01-01 00:00:00"
assert formatTimeStamp(tTime, True) == "1970-01-01 00.00.00"
@pytest.mark.core
def testFormatTime():
# END Test testBaseCommon_FormatTimeStamp
@pytest.mark.base
def testBaseCommon_FormatTime():
"""Test the formatTime function.
"""
assert formatTime("1") == "ERROR"
assert formatTime(1.0) == "ERROR"
assert formatTime(1) == "00:00:01"
@@ -101,8 +127,12 @@ def testFormatTime():
assert formatTime(86400) == "1-00:00:00"
assert formatTime(360000) == "4-04:00:00"
@pytest.mark.core
def testFormatInt():
# END Test testBaseCommon_FormatTime
@pytest.mark.base
def testBaseCommon_FormatInt():
"""Test the formatInt function.
"""
assert formatInt(1000) == "1000"
assert formatInt(1234) == "1.23\u2009k"
assert formatInt(12345) == "12.3\u2009k"
@@ -112,8 +142,12 @@ def testFormatInt():
assert formatInt(123456789) == "123\u2009M"
assert formatInt(1234567890) == "1.23\u2009G"
@pytest.mark.core
def testTransferCase():
# END Test testBaseCommon_FormatInt
@pytest.mark.base
def testBaseCommon_TransferCase():
"""Test the transferCase function.
"""
assert transferCase(1, "TaRgEt") == "TaRgEt"
assert transferCase("source", 1) == 1
assert transferCase("", "TaRgEt") == "TaRgEt"
@@ -122,8 +156,12 @@ def testTransferCase():
assert transferCase("SOURCE", "target") == "TARGET"
assert transferCase("source", "TARGET") == "target"
@pytest.mark.core
def testFuzzyTime():
# END Test testBaseCommon_TransferCase
@pytest.mark.base
def testBaseCommon_FuzzyTime():
"""Test the fuzzyTime function.
"""
assert fuzzyTime(-1) == "in the future"
assert fuzzyTime(0) == "just now"
assert fuzzyTime(29) == "just now"
@@ -152,3 +190,5 @@ def testFuzzyTime():
assert fuzzyTime(29808000) == "a year ago"
assert fuzzyTime(47336399) == "a year ago"
assert fuzzyTime(47336400) == "2 years ago"
# END Test testBaseCommon_FuzzyTime
+505
View File
@@ -0,0 +1,505 @@
# -*- coding: utf-8 -*-
"""novelWriter Config Class Tester
"""
import pytest
import sys
import os
import configparser
from shutil import copyfile
from dummy import causeOSError
from tools import cmpFiles
from nw.config import Config
from nw.constants import nwConst, nwFiles
@pytest.mark.base
def testBaseConfig_Constructor(monkeypatch):
"""Test config contructor.
"""
# Linux
monkeypatch.setattr("sys.platform", "linux")
tstConf = Config()
assert tstConf.osLinux is True
assert tstConf.osDarwin is False
assert tstConf.osWindows is False
assert tstConf.osUnknown is False
monkeypatch.undo()
# macOS
monkeypatch.setattr("sys.platform", "darwin")
tstConf = Config()
assert tstConf.osLinux is False
assert tstConf.osDarwin is True
assert tstConf.osWindows is False
assert tstConf.osUnknown is False
monkeypatch.undo()
# Windows
monkeypatch.setattr("sys.platform", "win32")
tstConf = Config()
assert tstConf.osLinux is False
assert tstConf.osDarwin is False
assert tstConf.osWindows is True
assert tstConf.osUnknown is False
monkeypatch.undo()
# Cygwin
monkeypatch.setattr("sys.platform", "cygwin")
tstConf = Config()
assert tstConf.osLinux is False
assert tstConf.osDarwin is False
assert tstConf.osWindows is True
assert tstConf.osUnknown is False
monkeypatch.undo()
# Other
monkeypatch.setattr("sys.platform", "some_ther_os")
tstConf = Config()
assert tstConf.osLinux is False
assert tstConf.osDarwin is False
assert tstConf.osWindows is False
assert tstConf.osUnknown is True
monkeypatch.undo()
# END Test testBaseConfig_Constructor
@pytest.mark.base
def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir):
"""Test config intialisation.
"""
tstConf = Config()
confFile = os.path.join(tmpDir, "novelwriter.conf")
testFile = os.path.join(outDir, "baseConfig_novelwriter.conf")
compFile = os.path.join(refDir, "baseConfig_novelwriter.conf")
# Make sure we don't have any old conf file
if os.path.isfile(confFile):
os.unlink(confFile)
# Let the config class figure out the path
monkeypatch.setattr("PyQt5.QtCore.QStandardPaths.writableLocation", lambda *args: fncDir)
tstConf.verQtValue = 50600
tstConf.initConfig()
assert tstConf.confPath == os.path.join(fncDir, tstConf.appHandle)
assert tstConf.dataPath == os.path.join(fncDir, tstConf.appHandle)
assert not os.path.isfile(confFile)
tstConf.verQtValue = 50000
tstConf.initConfig()
assert tstConf.confPath == os.path.join(fncDir, tstConf.appHandle)
assert tstConf.dataPath == os.path.join(fncDir, tstConf.appHandle)
assert not os.path.isfile(confFile)
monkeypatch.undo()
# Fail to make folders
monkeypatch.setattr("os.mkdir", causeOSError)
tstConfDir = os.path.join(fncDir, "test_conf")
tstConf.initConfig(confPath=tstConfDir, dataPath=tmpDir)
assert tstConf.confPath is None
assert tstConf.dataPath == tmpDir
assert not os.path.isfile(confFile)
tstDataDir = os.path.join(fncDir, "test_data")
tstConf.initConfig(confPath=tmpDir, dataPath=tstDataDir)
assert tstConf.confPath == tmpDir
assert tstConf.dataPath is None
assert os.path.isfile(confFile)
os.unlink(confFile)
monkeypatch.undo()
# Test load/save with no path
tstConf.confPath = None
assert not tstConf.loadConfig()
assert not tstConf.saveConfig()
# Run again and set the paths directly and correctly
# This should create a config file as well
monkeypatch.setattr("os.path.expanduser", lambda *args: "")
tstConf.spellTool = nwConst.SP_INTERNAL
tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir)
assert tstConf.confPath == tmpDir
assert tstConf.dataPath == tmpDir
assert os.path.isfile(confFile)
copyfile(confFile, testFile)
assert cmpFiles(testFile, compFile, [2, 9])
monkeypatch.undo()
# Load and save with OSError
monkeypatch.setattr("builtins.open", causeOSError)
assert not tstConf.loadConfig()
assert tstConf.hasError is True
assert tstConf.errData != []
assert tstConf.getErrData().startswith("Could not")
assert tstConf.hasError is False
assert tstConf.errData == []
assert not tstConf.saveConfig()
assert tstConf.hasError is True
assert tstConf.errData != []
assert tstConf.getErrData().startswith("Could not")
assert tstConf.hasError is False
assert tstConf.errData == []
monkeypatch.undo()
assert tstConf.loadConfig()
assert tstConf.saveConfig()
copyfile(confFile, testFile)
assert cmpFiles(testFile, compFile, [2, 9])
# END Test testBaseConfig_Init
@pytest.mark.base
def testBaseConfig_RecentCache(monkeypatch, tmpConf, tmpDir, fncDir):
"""Test recent cache file.
"""
# Check failing
tmpConf.dataPath = None
assert not tmpConf.loadRecentCache()
assert not tmpConf.saveRecentCache()
tmpConf.dataPath = tmpDir
# Add a couple of values
pathOne = os.path.join(fncDir, "projPathOne", nwFiles.PROJ_FILE)
pathTwo = os.path.join(fncDir, "projPathTwo", nwFiles.PROJ_FILE)
assert tmpConf.updateRecentCache(pathOne, "Proj One", 100, 1600002000)
assert tmpConf.updateRecentCache(pathTwo, "Proj Two", 200, 1600005600)
assert tmpConf.recentProj == {
pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200},
}
# Fail to Save
monkeypatch.setattr("builtins.open", causeOSError)
assert not tmpConf.saveRecentCache()
monkeypatch.undo()
# Save Proper
cacheFile = os.path.join(tmpDir, nwFiles.RECENT_FILE)
assert tmpConf.saveRecentCache()
assert tmpConf.saveRecentCache()
assert os.path.isfile(cacheFile)
# Fail to Load
monkeypatch.setattr("builtins.open", causeOSError)
tmpConf.recentProj = {}
assert not tmpConf.loadRecentCache()
assert tmpConf.recentProj == {}
monkeypatch.undo()
# Load Proper
tmpConf.recentProj = {}
assert tmpConf.loadRecentCache()
assert tmpConf.recentProj == {
pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200},
}
# Remove Non-Existent Entry
assert not tmpConf.removeFromRecentCache("dummy")
assert tmpConf.recentProj == {
pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200},
}
# Remove Second Entry
assert tmpConf.removeFromRecentCache(pathTwo)
assert tmpConf.recentProj == {
pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
}
# END Test testBaseConfig_RecentCache
@pytest.mark.base
def testBaseConfig_SetPath(tmpConf, tmpDir):
"""Test path setters.
"""
# Conf Path
assert tmpConf.setConfPath(None)
assert not tmpConf.setConfPath(os.path.join("somewhere", "over", "the", "rainbow"))
assert tmpConf.setConfPath(os.path.join(tmpDir, "novelwriter.conf"))
assert tmpConf.confPath == tmpDir
assert tmpConf.confFile == "novelwriter.conf"
assert not tmpConf.confChanged
# Data Path
assert tmpConf.setDataPath(None)
assert not tmpConf.setDataPath(os.path.join("somewhere", "over", "the", "rainbow"))
assert tmpConf.setDataPath(tmpDir)
assert tmpConf.dataPath == tmpDir
assert not tmpConf.confChanged
# Last Path
assert tmpConf.setLastPath(None)
assert tmpConf.lastPath == ""
assert tmpConf.setLastPath(os.path.join(tmpDir, "file.tmp"))
assert tmpConf.lastPath == tmpDir
assert tmpConf.setLastPath("")
assert tmpConf.lastPath == ""
# END Test testBaseConfig_SetPath
@pytest.mark.base
def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir):
"""Set various sizes and positions
"""
confFile = os.path.join(tmpDir, "novelwriter.conf")
testFile = os.path.join(outDir, "baseConfig_novelwriter.conf")
compFile = os.path.join(refDir, "baseConfig_novelwriter.conf")
# GUI Scaling
# ===========
tmpConf.guiScale = 1.0
assert tmpConf.pxInt(10) == 10
assert tmpConf.pxInt(13) == 13
assert tmpConf.rpxInt(10) == 10
assert tmpConf.rpxInt(13) == 13
tmpConf.guiScale = 2.0
assert tmpConf.pxInt(10) == 20
assert tmpConf.pxInt(13) == 26
assert tmpConf.rpxInt(10) == 5
assert tmpConf.rpxInt(13) == 6
# Setter + Getter Combos
# ======================
# Window Size
tmpConf.guiScale = 1.0
assert tmpConf.setWinSize(1205, 655)
assert not tmpConf.confChanged
tmpConf.guiScale = 2.0
assert tmpConf.setWinSize(70, 70)
assert tmpConf.getWinSize() == [70, 70]
assert tmpConf.winGeometry == [35, 35]
tmpConf.guiScale = 1.0
assert tmpConf.setWinSize(70, 70)
assert tmpConf.getWinSize() == [70, 70]
assert tmpConf.winGeometry == [70, 70]
assert tmpConf.setWinSize(1200, 650)
# Project Tree Columns
tmpConf.guiScale = 2.0
assert tmpConf.setTreeColWidths([10, 20, 25])
assert tmpConf.getTreeColWidths() == [10, 20, 24]
assert tmpConf.treeColWidth == [5, 10, 12]
tmpConf.guiScale = 1.0
assert tmpConf.setTreeColWidths([10, 20, 25])
assert tmpConf.getTreeColWidths() == [10, 20, 25]
assert tmpConf.treeColWidth == [10, 20, 25]
assert tmpConf.setTreeColWidths([200, 50, 30])
# Project Settings Tree Columns
tmpConf.guiScale = 2.0
assert tmpConf.setProjColWidths([10, 20, 30])
assert tmpConf.getProjColWidths() == [10, 20, 30]
assert tmpConf.projColWidth == [5, 10, 15]
tmpConf.guiScale = 1.0
assert tmpConf.setProjColWidths([10, 20, 30])
assert tmpConf.getProjColWidths() == [10, 20, 30]
assert tmpConf.projColWidth == [10, 20, 30]
assert tmpConf.setProjColWidths([200, 60, 140])
# Main Pane Splitter
tmpConf.guiScale = 2.0
assert tmpConf.setMainPanePos([200, 700])
assert tmpConf.getMainPanePos() == [200, 700]
assert tmpConf.mainPanePos == [100, 350]
tmpConf.guiScale = 1.0
assert tmpConf.setMainPanePos([200, 700])
assert tmpConf.getMainPanePos() == [200, 700]
assert tmpConf.mainPanePos == [200, 700]
assert tmpConf.setMainPanePos([300, 800])
# Doc Pane Splitter
tmpConf.guiScale = 2.0
assert tmpConf.setDocPanePos([300, 300])
assert tmpConf.getDocPanePos() == [300, 300]
assert tmpConf.docPanePos == [150, 150]
tmpConf.guiScale = 1.0
assert tmpConf.setDocPanePos([300, 300])
assert tmpConf.getDocPanePos() == [300, 300]
assert tmpConf.docPanePos == [300, 300]
assert tmpConf.setDocPanePos([400, 400])
# View Pane Splitter
tmpConf.guiScale = 2.0
assert tmpConf.setViewPanePos([400, 250])
assert tmpConf.getViewPanePos() == [400, 250]
assert tmpConf.viewPanePos == [200, 125]
tmpConf.guiScale = 1.0
assert tmpConf.setViewPanePos([400, 250])
assert tmpConf.getViewPanePos() == [400, 250]
assert tmpConf.viewPanePos == [400, 250]
assert tmpConf.setViewPanePos([500, 150])
# Outline Pane Splitter
tmpConf.guiScale = 2.0
assert tmpConf.setOutlinePanePos([400, 250])
assert tmpConf.getOutlinePanePos() == [400, 250]
assert tmpConf.outlnPanePos == [200, 125]
tmpConf.guiScale = 1.0
assert tmpConf.setOutlinePanePos([400, 250])
assert tmpConf.getOutlinePanePos() == [400, 250]
assert tmpConf.outlnPanePos == [400, 250]
assert tmpConf.setOutlinePanePos([500, 150])
# Getters Only
# ============
tmpConf.guiScale = 1.0
assert tmpConf.getTextWidth() == 600
assert tmpConf.getTextMargin() == 40
assert tmpConf.getTabWidth() == 40
assert tmpConf.getFocusWidth() == 800
tmpConf.guiScale = 2.0
assert tmpConf.getTextWidth() == 1200
assert tmpConf.getTextMargin() == 80
assert tmpConf.getTabWidth() == 80
assert tmpConf.getFocusWidth() == 1600
# Flag Setters
# ============
assert not tmpConf.setShowRefPanel(False)
assert not tmpConf.showRefPanel
assert tmpConf.setShowRefPanel(True)
assert not tmpConf.setViewComments(False)
assert not tmpConf.viewComments
assert tmpConf.setViewComments(True)
assert not tmpConf.setViewSynopsis(False)
assert not tmpConf.viewSynopsis
assert tmpConf.setViewSynopsis(True)
# Check Final File
# ================
assert tmpConf.confChanged
assert tmpConf.saveConfig()
assert not tmpConf.confChanged
copyfile(confFile, testFile)
assert cmpFiles(testFile, compFile, [2, 9])
# END Test testBaseConfig_SettersGetters
@pytest.mark.base
def testBaseConfig_Internal(monkeypatch, tmpConf):
"""Check internal functions.
"""
# Function _packList
assert tmpConf._packList(["A", 1, 2.0, None, False]) == "A, 1, 2.0, None, False"
# Function _unpackList
assert tmpConf._unpackList("1, 2, 3", [0, 0, 0], tmpConf.CNF_I_LST) == [1, 2, 3]
assert tmpConf._unpackList("1, 2 ", [0, 0, 0], tmpConf.CNF_I_LST) == [1, 2, 0]
assert tmpConf._unpackList("A, B, C", [0, 0, 0], tmpConf.CNF_I_LST) == [0, 0, 0]
assert tmpConf._unpackList("1, 2, 3", ["X", "Y", "Z"], tmpConf.CNF_S_LST) == ["1", "2", "3"]
assert tmpConf._unpackList("A, B ", ["X", "Y", "Z"], tmpConf.CNF_S_LST) == ["A", "B", "Z"]
assert tmpConf._unpackList("A, B, C", ["X", "Y", "Z"], tmpConf.CNF_S_LST) == ["A", "B", "C"]
assert tmpConf._unpackList("A, B, C", ["X", "Y", "Z"], tmpConf.CNF_STR) == ["X", "Y", "Z"]
# Function _parseLine
cnfParse = configparser.ConfigParser()
cnfParse.read_string(
"[Main]\n"
"val_string = dummy\n"
"val_int = 123\n"
"val_bool = True\n"
"val_list_string = A, B, C\n"
"val_list_int = 1, 2, 3\n"
)
assert tmpConf._parseLine(
cnfParse, "Main", "val_string", tmpConf.CNF_STR, "default"
) == "dummy"
assert tmpConf._parseLine(
cnfParse, "Main", "nope", tmpConf.CNF_STR, "default"
) == "default"
assert tmpConf._parseLine(
cnfParse, "Main", "val_int", tmpConf.CNF_INT, "0"
) == 123
assert tmpConf._parseLine(
cnfParse, "Main", "nope", tmpConf.CNF_INT, 0
) == 0
assert tmpConf._parseLine(
cnfParse, "Main", "val_string", tmpConf.CNF_INT, 0
) == 0
assert tmpConf._parseLine(
cnfParse, "Main", "val_bool", tmpConf.CNF_BOOL, False
) is True
assert tmpConf._parseLine(
cnfParse, "Main", "nope", tmpConf.CNF_BOOL, False
) is False
assert tmpConf._parseLine(
cnfParse, "Main", "val_string", tmpConf.CNF_BOOL, False
) is False
assert tmpConf._parseLine(
cnfParse, "Main", "val_list_string", tmpConf.CNF_S_LST, ["W", "X", "Y", "Z"]
) == ["A", "B", "C", "Z"]
assert tmpConf._parseLine(
cnfParse, "Main", "nope", tmpConf.CNF_S_LST, ["W", "X", "Y", "Z"]
) == ["W", "X", "Y", "Z"]
assert tmpConf._parseLine(
cnfParse, "Main", "val_list_int", tmpConf.CNF_I_LST, [6, 7, 8, 9]
) == [1, 2, 3, 9]
assert tmpConf._parseLine(
cnfParse, "Main", "nope", tmpConf.CNF_S_LST, [6, 7, 8, 9]
) == [6, 7, 8, 9]
# Function _checkNone
assert tmpConf._checkNone(None) is None
assert tmpConf._checkNone("None") is None
assert tmpConf._checkNone("stuff") == "stuff"
# Function _checkOptionalPackages
# (Assumes enchant package exists ans is importable)
tmpConf._checkOptionalPackages()
assert tmpConf.hasEnchant is True
monkeypatch.setitem(sys.modules, "enchant", None)
tmpConf._checkOptionalPackages()
assert tmpConf.hasEnchant is False
monkeypatch.undo()
monkeypatch.setattr("shutil.which", lambda *args: "dummy")
tmpConf._checkOptionalPackages()
assert tmpConf.hasAssistant is True
monkeypatch.undo()
monkeypatch.setattr("shutil.which", lambda *args: None)
tmpConf._checkOptionalPackages()
assert tmpConf.hasAssistant is False
monkeypatch.undo()
# END Test testBaseConfig_Internal
+97
View File
@@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
"""novelWriter Error Tester
"""
import nw
import pytest
from PyQt5.QtWidgets import qApp
from dummy import causeException
from nw.error import NWErrorMessage, exceptionHandler
@pytest.mark.base
def testBaseError_Dialog(qtbot, monkeypatch, fncDir, tmpDir):
"""Test the error dialog.
"""
qApp.closeAllWindows()
nwGUI = nw.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
nwErr = NWErrorMessage(nwGUI)
qtbot.addWidget(nwErr)
nwErr.show()
# Invalid Error Message
nwErr.setMessage(Exception, "Faulty Error", 123)
assert nwErr.msgBody.toPlainText() == "Failed to generate error report ..."
# Valid Error Message
monkeypatch.setattr("PyQt5.QtCore.QSysInfo.kernelVersion", lambda: "1.2.3")
nwErr.setMessage(Exception, "Fine Error", None)
theMessage = nwErr.msgBody.toPlainText()
assert theMessage
assert "Fine Error" in theMessage
assert "Exception" in theMessage
assert "(1.2.3)" in theMessage
monkeypatch.undo()
# No kernel version retrieved
monkeypatch.setattr("PyQt5.QtCore.QSysInfo.kernelVersion", causeException)
nwErr.setMessage(Exception, "Almost Fine Error", None)
theMessage = nwErr.msgBody.toPlainText()
assert theMessage
assert "(Unknown)" in theMessage
monkeypatch.undo()
nwErr._doClose()
nwErr.close()
nwGUI.closeMain()
# END Test testBaseError_Dialog
@pytest.mark.base
def testBaseError_Handler(qtbot, monkeypatch, fncDir, tmpDir):
"""Test the error handler. This test doesn'thave any asserts, but it
checks that the error handler handles potential exceptions. The test
will fail if excpetions are not handled.
"""
qApp.closeAllWindows()
nwGUI = nw.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
# Normal shutdown
monkeypatch.setattr(NWErrorMessage, "exec_", lambda *args: None)
monkeypatch.setattr("PyQt5.QtWidgets.qApp.exit", lambda *args: None)
exceptionHandler(Exception, "Error Message", None)
monkeypatch.undo()
# Should not crash when no GUI is found
monkeypatch.setattr(NWErrorMessage, "exec_", lambda *args: None)
monkeypatch.setattr("PyQt5.QtWidgets.qApp.exit", lambda *args: None)
monkeypatch.setattr("PyQt5.QtWidgets.qApp.topLevelWidgets", lambda: [])
exceptionHandler(Exception, "Error Message", None)
monkeypatch.undo()
# Should handle qApp failing
monkeypatch.setattr(NWErrorMessage, "exec_", lambda *args: None)
monkeypatch.setattr("PyQt5.QtWidgets.qApp.exit", lambda *args: None)
monkeypatch.setattr("PyQt5.QtWidgets.qApp.topLevelWidgets", causeException)
exceptionHandler(Exception, "Error Message", None)
monkeypatch.undo()
# Should handle failing to close main GUI
monkeypatch.setattr(NWErrorMessage, "exec_", lambda *args: None)
monkeypatch.setattr("PyQt5.QtWidgets.qApp.exit", lambda *args: None)
monkeypatch.setattr(nwGUI, "closeMain", causeException)
exceptionHandler(Exception, "Error Message", None)
monkeypatch.undo()
nwGUI.closeMain()
# END Test testBaseError_Handler
+163
View File
@@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
"""novelWriter Main Init Tester
"""
import nw
import pytest
import logging
import sys
from dummy import DummyMain
@pytest.mark.base
def testBaseInit_Launch(caplog, monkeypatch, tmpDir):
"""Check launching the main GUI.
"""
monkeypatch.setattr("nw.guimain.GuiMain", DummyMain)
# Testmode launch
nwGUI = nw.main(
["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
)
assert isinstance(nwGUI, DummyMain)
# Darwin launch
monkeypatch.setitem(sys.modules, "Foundation", None)
osDarwin = nw.CONFIG.osDarwin
nw.CONFIG.osDarwin = True
nwGUI = nw.main(
["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
)
assert isinstance(nwGUI, DummyMain)
assert "Foundation" in caplog.messages[1]
nw.CONFIG.osDarwin = osDarwin
# Normal launch
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *args: None)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setApplicationName", lambda *args: None)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setApplicationVersion", lambda *args: None)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setWindowIcon", lambda *args: None)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setOrganizationDomain", lambda *args: None)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec_", lambda *args: 0)
with pytest.raises(SystemExit) as ex:
nw.main(["--config=%s" % tmpDir, "--data=%s" % tmpDir])
assert ex.value.code == 0
monkeypatch.undo()
# END Test testBaseInit_Launch
@pytest.mark.base
def testBaseInit_Options(monkeypatch, tmpDir):
"""Test command line options for logging level.
"""
monkeypatch.setattr("nw.guimain.GuiMain", DummyMain)
monkeypatch.setattr(sys, "argv", [
"novelWriter.py", "--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir
])
# Defaults w/None Args
nwGUI = nw.main()
assert nw.logger.getEffectiveLevel() == logging.WARNING
assert nw.CONFIG.debugInfo is False
assert nw.CONFIG.showGUI is False
assert nwGUI.closeMain() == "closeMain"
# Defaults
nwGUI = nw.main(
["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir, "--style=Fusion"]
)
assert nw.logger.getEffectiveLevel() == logging.WARNING
assert nw.CONFIG.debugInfo is False
assert nwGUI.closeMain() == "closeMain"
# Log Levels
nwGUI = nw.main(
["--testmode", "--info", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
)
assert nw.logger.getEffectiveLevel() == logging.INFO
assert nw.CONFIG.debugInfo is False
assert nwGUI.closeMain() == "closeMain"
nwGUI = nw.main(
["--testmode", "--debug", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
)
assert nw.logger.getEffectiveLevel() == logging.DEBUG
assert nw.CONFIG.debugInfo is True
assert nwGUI.closeMain() == "closeMain"
nwGUI = nw.main(
["--testmode", "--verbose", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
)
assert nw.logger.getEffectiveLevel() == 5
assert nw.CONFIG.debugInfo is True
assert nwGUI.closeMain() == "closeMain"
# Help and Version
with pytest.raises(SystemExit) as ex:
nwGUI = nw.main(
["--testmode", "--help", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
)
assert nwGUI.closeMain() == "closeMain"
assert ex.value.code == 0
with pytest.raises(SystemExit) as ex:
nwGUI = nw.main(
["--testmode", "--version", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
)
assert nwGUI.closeMain() == "closeMain"
assert ex.value.code == 0
# Invalid options
with pytest.raises(SystemExit) as ex:
nwGUI = nw.main(
["--testmode", "--invalid", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
)
assert nwGUI.closeMain() == "closeMain"
assert ex.value.code == 2
# Project Path
nwGUI = nw.main(
["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir, "sample/"]
)
assert nw.CONFIG.cmdOpen == "sample/"
assert nwGUI.closeMain() == "closeMain"
monkeypatch.undo()
# END Test testBaseInit_Options
@pytest.mark.base
def testBaseInit_Imports(caplog, monkeypatch, tmpDir):
"""Check import error handling.
"""
monkeypatch.setattr("nw.guimain.GuiMain", DummyMain)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *args: None)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec_", lambda *args: 0)
monkeypatch.setattr("PyQt5.QtWidgets.QErrorMessage.__init__", lambda *args: None)
monkeypatch.setattr("PyQt5.QtWidgets.QErrorMessage.resize", lambda *args: None)
monkeypatch.setattr("PyQt5.QtWidgets.QErrorMessage.showMessage", lambda *args: None)
monkeypatch.setitem(sys.modules, "lxml", None)
monkeypatch.setattr("sys.hexversion", 0x0)
monkeypatch.setattr("nw.CONFIG.verQtValue", 50000)
monkeypatch.setattr("nw.CONFIG.verPyQtValue", 50000)
with pytest.raises(SystemExit) as ex:
_ = nw.main(
["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]
)
assert ex.value.code & 4 == 4 # Python version not satisfied
assert ex.value.code & 8 == 8 # Qt version not satisfied
assert ex.value.code & 16 == 16 # PyQt version not satisfied
assert ex.value.code & 32 == 32 # lxml package missing
assert "At least Python" in caplog.messages[0]
assert "At least Qt5" in caplog.messages[1]
assert "At least PyQt5" in caplog.messages[2]
assert "lxml" in caplog.messages[3]
monkeypatch.undo()
# END Test testBaseInit_Imports
-165
View File
@@ -1,165 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Config Class Tester
"""
import pytest
import os
from nwtools import cmpFiles
@pytest.mark.core
def testConfigCore(tmpConf, nwTemp, nwRef):
refConf = os.path.join(nwRef, "novelwriter.conf")
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
assert tmpConf.confPath == nwTemp
assert tmpConf.saveConfig()
assert cmpFiles(testConf, refConf, [2])
assert not tmpConf.confChanged
assert tmpConf.loadConfig()
assert not tmpConf.confChanged
@pytest.mark.core
def testConfigSetConfPath(tmpConf, nwTemp):
assert tmpConf.setConfPath(None)
assert not tmpConf.setConfPath(os.path.join("somewhere", "over", "the", "rainbow"))
assert tmpConf.setConfPath(os.path.join(nwTemp, "novelwriter.conf"))
assert tmpConf.confPath == nwTemp
assert tmpConf.confFile == "novelwriter.conf"
assert not tmpConf.confChanged
@pytest.mark.core
def testConfigSetDataPath(tmpConf, nwTemp):
assert tmpConf.setDataPath(None)
assert not tmpConf.setDataPath(os.path.join("somewhere", "over", "the", "rainbow"))
assert tmpConf.setDataPath(nwTemp)
assert tmpConf.dataPath == nwTemp
assert not tmpConf.confChanged
@pytest.mark.core
def testConfigSetWinSize(tmpConf, nwTemp, nwRef):
refConf = os.path.join(nwRef, "novelwriter.conf")
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
tmpConf.guiScale = 1.0
assert tmpConf.confPath == nwTemp
assert tmpConf.setWinSize(1205, 655)
assert not tmpConf.confChanged
assert tmpConf.setWinSize(70, 70)
assert tmpConf.confChanged
assert tmpConf.setWinSize(1200, 650)
assert tmpConf.saveConfig()
assert cmpFiles(testConf, refConf, [2])
assert not tmpConf.confChanged
@pytest.mark.core
def testConfigSetTreeColWidths(tmpConf, nwTemp, nwRef):
refConf = os.path.join(nwRef, "novelwriter.conf")
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
assert tmpConf.confPath == nwTemp
tmpConf.guiScale = 1.0
assert tmpConf.setTreeColWidths([10, 20, 25])
assert tmpConf.treeColWidth == [10, 20, 25]
assert tmpConf.setTreeColWidths([200, 50, 30])
assert tmpConf.setProjColWidths([10, 20, 30])
assert tmpConf.projColWidth == [10, 20, 30]
assert tmpConf.setProjColWidths([200, 60, 140])
assert tmpConf.confChanged
assert tmpConf.saveConfig()
assert cmpFiles(testConf, refConf, [2])
assert not tmpConf.confChanged
@pytest.mark.core
def testConfigSetPanePos(tmpConf, nwTemp, nwRef):
refConf = os.path.join(nwRef, "novelwriter.conf")
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
assert tmpConf.confPath == nwTemp
tmpConf.guiScale = 2.0
assert tmpConf.setMainPanePos([200, 700])
assert tmpConf.mainPanePos == [100, 350]
assert tmpConf.getMainPanePos() == [200, 700]
assert tmpConf.setDocPanePos([300, 300])
assert tmpConf.docPanePos == [150, 150]
assert tmpConf.getDocPanePos() == [300, 300]
assert tmpConf.setViewPanePos([400, 250])
assert tmpConf.viewPanePos == [200, 125]
assert tmpConf.getViewPanePos() == [400, 250]
assert tmpConf.setOutlinePanePos([400, 250])
assert tmpConf.outlnPanePos == [200, 125]
assert tmpConf.getOutlinePanePos() == [400, 250]
tmpConf.guiScale = 1.0
assert tmpConf.setMainPanePos([300, 800])
assert tmpConf.setDocPanePos([400, 400])
assert tmpConf.setViewPanePos([500, 150])
assert tmpConf.setOutlinePanePos([500, 150])
assert tmpConf.confChanged
assert tmpConf.saveConfig()
assert cmpFiles(testConf, refConf, [2])
assert not tmpConf.confChanged
@pytest.mark.core
def testConfigFlags(tmpConf, nwTemp, nwRef):
refConf = os.path.join(nwRef, "novelwriter.conf")
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
assert tmpConf.confPath == nwTemp
assert not tmpConf.setShowRefPanel(False)
assert tmpConf.setShowRefPanel(True)
assert not tmpConf.setViewComments(False)
assert not tmpConf.viewComments
assert tmpConf.setViewComments(True)
assert not tmpConf.setViewSynopsis(False)
assert not tmpConf.viewSynopsis
assert tmpConf.setViewSynopsis(True)
assert tmpConf.confChanged
assert tmpConf.saveConfig()
assert cmpFiles(testConf, refConf, [2])
assert not tmpConf.confChanged
@pytest.mark.core
def testTextSizes(tmpConf, nwTemp, nwRef):
assert tmpConf.confPath == nwTemp
tmpConf.guiScale = 2.0
assert tmpConf.getTextWidth() == 1200
assert tmpConf.getTextMargin() == 80
assert tmpConf.getTabWidth() == 80
assert tmpConf.getFocusWidth() == 1600
tmpConf.guiScale = 1.0
assert not tmpConf.confChanged
@pytest.mark.core
def testConfigErrors(tmpConf):
nonPath = os.path.join("somewhere", "over", "the", "rainbow")
assert tmpConf.initConfig(nonPath, nonPath)
assert tmpConf.hasError
assert not tmpConf.loadConfig()
assert not tmpConf.saveConfig()
assert not tmpConf.loadRecentCache()
assert len(tmpConf.getErrData()) > 0
@pytest.mark.core
def testConfigInternals(tmpConf):
assert tmpConf._checkNone(None) is None
assert tmpConf._checkNone("None") is None
+145
View File
@@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
"""novelWriter NWDoc Class Tester
"""
import os
import pytest
from dummy import causeOSError
from nw.core import NWProject, NWDoc
from nw.core.item import NWItem
from nw.constants import nwItemClass, nwItemLayout
@pytest.mark.core
def testCoreDocument_LoadSave(monkeypatch, dummyGUI, nwMinimal):
"""Test loading and saving a document with the NWDoc class.
"""
theProject = NWProject(dummyGUI)
assert theProject.openProject(nwMinimal)
assert theProject.projPath == nwMinimal
theDoc = NWDoc(theProject, dummyGUI)
sHandle = "8c659a11cd429"
# Not a valid handle
assert theDoc.openDocument("dummy") is None
# Non-existent handle
assert theDoc.openDocument("0000000000000") is None
# Cause open() to fail while loading
def dummyOpen(*args, **kwargs):
raise OSError
monkeypatch.setattr("builtins.open", dummyOpen)
assert theDoc.openDocument(sHandle) is None
monkeypatch.undo()
# Load the text
assert theDoc.openDocument(sHandle) == "### New Scene\n\n"
# Try to open a new (non-existent) file
nHandle = theProject.projTree.findRoot(nwItemClass.NOVEL)
assert nHandle is not None
xHandle = theProject.newFile("New File", nwItemClass.NOVEL, nHandle)
assert theDoc.openDocument(xHandle) == ""
# Check cached item
assert isinstance(theDoc._theItem, NWItem)
assert theDoc.openDocument(xHandle, isOrphan=True) == ""
assert theDoc._theItem is None
# Set handle and save again
theText = "### Test File\n\nText ...\n\n"
assert theDoc.openDocument(xHandle) == ""
assert theDoc.saveDocument(theText)
# Save again to ensure temp file and previous file is handled
assert theDoc.saveDocument(theText)
# Check file content
docPath = os.path.join(nwMinimal, "content", xHandle+".nwd")
with open(docPath, mode="r", encoding="utf8") as inFile:
assert inFile.read() == (
"%%~name: New File\n"
f"%%~path: a508bb932959c/{xHandle}\n"
"%%~kind: NOVEL/SCENE\n"
"### Test File\n\n"
"Text ...\n\n"
)
# Force no meta data
theDoc._theItem = None
assert theDoc.saveDocument(theText)
with open(docPath, mode="r", encoding="utf8") as inFile:
assert inFile.read() == theText
# Cause open() to fail while saving
monkeypatch.setattr("builtins.open", causeOSError)
assert not theDoc.saveDocument(theText)
monkeypatch.undo()
# Saving with no handle
theDoc.clearDocument()
assert not theDoc.saveDocument(theText)
# Delete the last document
assert not theDoc.deleteDocument("dummy")
assert os.path.isfile(docPath)
# Cause the delete to fail
monkeypatch.setattr("os.unlink", causeOSError)
assert not theDoc.deleteDocument(xHandle)
monkeypatch.undo()
# Make the delete pass
assert theDoc.deleteDocument(xHandle)
assert not os.path.isfile(docPath)
# END Test testCoreDocument_Load
@pytest.mark.core
def testCoreDocument_Methods(monkeypatch, dummyGUI, nwMinimal):
"""Test other methods of the NWDoc class.
"""
theProject = NWProject(dummyGUI)
assert theProject.openProject(nwMinimal)
assert theProject.projPath == nwMinimal
theDoc = NWDoc(theProject, dummyGUI)
sHandle = "8c659a11cd429"
docPath = os.path.join(nwMinimal, "content", sHandle+".nwd")
assert theDoc.openDocument(sHandle) == "### New Scene\n\n"
# Check location
assert theDoc.getFileLocation() == docPath
# Check the item
assert theDoc.getCurrentItem() is not None
assert theDoc.getCurrentItem().itemHandle == sHandle
# Check the meta
theName, theParent, theClass, theLayout = theDoc.getMeta()
assert theName == "New Scene"
assert theParent == "a6d311a93600a"
assert theClass == nwItemClass.NOVEL
assert theLayout == nwItemLayout.SCENE
# Add meta data garbage
assert theDoc.saveDocument("%%~ stuff\n### Test File\n\nText ...\n\n")
with open(docPath, mode="r", encoding="utf8") as inFile:
assert inFile.read() == (
"%%~name: New Scene\n"
f"%%~path: a6d311a93600a/{sHandle}\n"
"%%~kind: NOVEL/SCENE\n"
"%%~ stuff\n"
"### Test File\n\n"
"Text ...\n\n"
)
assert theDoc.openDocument(sHandle) == "### Test File\n\nText ...\n\n"
# END Test testCoreDocument_Methods
@@ -8,28 +8,28 @@ import json
from shutil import copyfile
from nwtools import cmpFiles
from tools import cmpFiles
from nw.core.project import NWProject
from nw.core.index import NWIndex
from nw.constants import nwItemClass, nwItemLayout
@pytest.mark.project
def testIndexBuildCheck(monkeypatch, nwLipsum, nwDummy, nwTempProj, nwRef):
@pytest.mark.core
def testCoreIndex_LoadSave(monkeypatch, nwLipsum, dummyGUI, outDir, refDir):
"""Test core functionality of scaning, saving, loading and checking
the index cache file.
"""
projFile = os.path.join(nwLipsum, "meta", "tagsIndex.json")
testFile = os.path.join(nwTempProj, "1_tagsIndex.json")
refFile = os.path.join(nwRef, "proj", "1_tagsIndex.json")
testFile = os.path.join(outDir, "coreIndex_LoadSave_tagsIndex.json")
compFile = os.path.join(refDir, "coreIndex_LoadSave_tagsIndex.json")
theProject = NWProject(nwDummy)
theProject = NWProject(dummyGUI)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwLipsum)
monkeypatch.setattr("nw.core.index.time", lambda: 123.4)
theIndex = NWIndex(theProject, nwDummy)
theIndex = NWIndex(theProject, dummyGUI)
notIndexable = {
"b3643d0f92e32": False, # Novel ROOT
"45e6b01ca35c1": False, # Chapter One FOLDER
@@ -136,17 +136,19 @@ def testIndexBuildCheck(monkeypatch, nwLipsum, nwDummy, nwTempProj, nwRef):
assert theProject.closeProject()
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
assert cmpFiles(testFile, compFile)
@pytest.mark.project
def testIndexScanThis(nwMinimal, nwDummy):
# END Test testCoreIndex_LoadSave
@pytest.mark.core
def testCoreIndex_ScanThis(nwMinimal, dummyGUI):
"""Test the tag scanner function scanThis.
"""
theProject = NWProject(nwDummy)
theProject = NWProject(dummyGUI)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwMinimal)
theIndex = NWIndex(theProject, nwDummy)
theIndex = NWIndex(theProject, dummyGUI)
isValid, theBits, thePos = theIndex.scanThis("tag: this, and this")
assert not isValid
@@ -162,40 +164,42 @@ def testIndexScanThis(nwMinimal, nwDummy):
isValid, theBits, thePos = theIndex.scanThis("@a:")
assert isValid
assert str(theBits) == "['@a']"
assert str(thePos) == "[0]"
assert theBits == ["@a"]
assert thePos == [0]
isValid, theBits, thePos = theIndex.scanThis("@a:b")
assert isValid
assert str(theBits) == "['@a', 'b']"
assert str(thePos) == "[0, 3]"
assert theBits == ["@a", "b"]
assert thePos == [0, 3]
isValid, theBits, thePos = theIndex.scanThis("@a:b,c,d")
assert isValid
assert str(theBits) == "['@a', 'b', 'c', 'd']"
assert str(thePos) == "[0, 3, 5, 7]"
assert theBits == ["@a", "b", "c", "d"]
assert thePos == [0, 3, 5, 7]
isValid, theBits, thePos = theIndex.scanThis("@a : b , c , d")
assert isValid
assert str(theBits) == "['@a', 'b', 'c', 'd']"
assert str(thePos) == "[0, 5, 9, 13]"
assert theBits == ["@a", "b", "c", "d"]
assert thePos == [0, 5, 9, 13]
isValid, theBits, thePos = theIndex.scanThis("@tag: this, and this")
assert isValid
assert str(theBits) == "['@tag', 'this', 'and this']"
assert str(thePos) == "[0, 6, 12]"
assert theBits == ["@tag", "this", "and this"]
assert thePos == [0, 6, 12]
assert theProject.closeProject()
@pytest.mark.project
def testIndexCheckThese(nwMinimal, nwDummy):
# END Test testCoreIndex_ScanThis
@pytest.mark.core
def testCoreIndex_CheckThese(nwMinimal, dummyGUI):
"""Test the tag checker function checkThese.
"""
theProject = NWProject(nwDummy)
theProject = NWProject(dummyGUI)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwMinimal)
theIndex = NWIndex(theProject, nwDummy)
theIndex = NWIndex(theProject, dummyGUI)
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
nItem = theProject.projTree[nHandle]
@@ -209,29 +213,32 @@ def testIndexCheckThese(nwMinimal, nwDummy):
"# Hello World!\n"
"@pov: Jane"
))
assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle
assert theIndex.tagIndex == {"Jane": [2, cHandle, "CHARACTER", "T000001"]}
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
assert str(theIndex.checkThese(["@tag", "Jane"], cItem)) == "[True, True]"
assert str(theIndex.checkThese(["@tag", "John"], cItem)) == "[True, True]"
assert str(theIndex.checkThese(["@tag", "Jane"], nItem)) == "[True, False]"
assert str(theIndex.checkThese(["@tag", "John"], nItem)) == "[True, True]"
assert str(theIndex.checkThese(["@pov", "John"], nItem)) == "[True, False]"
assert str(theIndex.checkThese(["@pov", "Jane"], nItem)) == "[True, True]"
assert str(theIndex.checkThese(["@ pov", "Jane"], nItem)) == "[False, False]"
assert str(theIndex.checkThese(["@what", "Jane"], nItem)) == "[False, False]"
assert theIndex.checkThese([], cItem) == []
assert theIndex.checkThese(["@tag", "Jane"], cItem) == [True, True]
assert theIndex.checkThese(["@tag", "John"], cItem) == [True, True]
assert theIndex.checkThese(["@tag", "Jane"], nItem) == [True, False]
assert theIndex.checkThese(["@tag", "John"], nItem) == [True, True]
assert theIndex.checkThese(["@pov", "John"], nItem) == [True, False]
assert theIndex.checkThese(["@pov", "Jane"], nItem) == [True, True]
assert theIndex.checkThese(["@ pov", "Jane"], nItem) == [False, False]
assert theIndex.checkThese(["@what", "Jane"], nItem) == [False, False]
assert theProject.closeProject()
@pytest.mark.project
def testIndexScanText(nwMinimal, nwDummy):
"""Check the index data extraction functions.
# END Test testCoreIndex_CheckThese
@pytest.mark.core
def testCoreIndex_ScanText(nwMinimal, dummyGUI):
"""Check the index text scanner.
"""
theProject = NWProject(nwDummy)
theProject = NWProject(dummyGUI)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwMinimal)
theIndex = NWIndex(theProject, nwDummy)
theIndex = NWIndex(theProject, dummyGUI)
# Some items for fail to scan tests
dHandle = theProject.newFolder("Folder", nwItemClass.NOVEL, "a508bb932959c")
@@ -392,21 +399,23 @@ def testIndexScanText(nwMinimal, nwDummy):
"% synopsis: Synopsis One.\n\n"
"Paragraph One.\n\n"
))
assert str(theIndex.refIndex[sHandle]["T000001"]["tags"]) == (
"[[3, '@pov', 'One'], [5, '@char', 'Two']]"
assert theIndex.refIndex[sHandle]["T000001"]["tags"] == (
[[3, "@pov", "One"], [5, "@char", "Two"]]
)
assert theProject.closeProject()
@pytest.mark.project
def testIndexExtractData(nwMinimal, nwDummy):
# END Test testCoreIndex_ScanText
@pytest.mark.core
def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
"""Check the index data extraction functions.
"""
theProject = NWProject(nwDummy)
theProject = NWProject(dummyGUI)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwMinimal)
theIndex = NWIndex(theProject, nwDummy)
theIndex = NWIndex(theProject, dummyGUI)
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
@@ -424,7 +433,13 @@ def testIndexExtractData(nwMinimal, nwDummy):
))
# The novel structure should contain the pointer to the novel file header
assert str(theIndex.getNovelStructure()) == "['%s:T000001']" % nHandle
assert theIndex.getNovelStructure() == ["%s:T000001" % nHandle]
# Check that excluded files can be skipped
theProject.projTree[nHandle].setExported(False)
assert theIndex.getNovelStructure(skipExcluded=False) == ["%s:T000001" % nHandle]
assert theIndex.getNovelStructure(skipExcluded=True) == []
assert theIndex.getNovelStructure() == []
# The novel file should have the correct counts
cC, wC, pC = theIndex.getCounts(nHandle)
@@ -443,8 +458,8 @@ def testIndexExtractData(nwMinimal, nwDummy):
# The novel file should now refer to Jane as @pov and @char
theRefs = theIndex.getReferences(nHandle)
assert str(theRefs["@pov"]) == "['Jane']"
assert str(theRefs["@char"]) == "['Jane']"
assert theRefs["@pov"] == ["Jane"]
assert theRefs["@char"] == ["Jane"]
##
# getBackReferenceList
@@ -455,7 +470,7 @@ def testIndexExtractData(nwMinimal, nwDummy):
# The character file should have a record of the reference from the novel file
theRefs = theIndex.getBackReferenceList(cHandle)
assert str(theRefs) == "{'%s': 'T000001'}" % nHandle
assert theRefs == {nHandle: "T000001"}
##
# getTagSource
@@ -535,3 +550,5 @@ def testIndexExtractData(nwMinimal, nwDummy):
assert pC == 2
assert theProject.closeProject()
# END Test testCoreIndex_ExtractData
+153 -40
View File
@@ -6,13 +6,15 @@ import pytest
from lxml import etree
from nw.core.project import NWProject, NWItem
from nw.core import NWProject
from nw.core.item import NWItem
from nw.constants import nwItemClass, nwItemType, nwItemLayout
@pytest.mark.project
def testItemSettersSimple(nwDummy):
theProject = NWProject(nwDummy)
@pytest.mark.core
def testCoreItem_Setters(dummyGUI):
"""Test all the simple setters for the NWItem class.
"""
theProject = NWProject(dummyGUI)
theItem = NWItem(theProject)
# Name
@@ -20,12 +22,16 @@ def testItemSettersSimple(nwDummy):
assert theItem.itemName == "A Name"
theItem.setName("\t A Name ")
assert theItem.itemName == "A Name"
theItem.setName(123)
assert theItem.itemName == ""
# Handle
theItem.setHandle(123)
assert theItem.itemHandle is None
theItem.setHandle("0123456789abcdef")
assert theItem.itemHandle is None
theItem.setHandle("0123456789abg")
assert theItem.itemHandle is None
theItem.setHandle("0123456789abc")
assert theItem.itemHandle == "0123456789abc"
@@ -36,6 +42,8 @@ def testItemSettersSimple(nwDummy):
assert theItem.itemParent is None
theItem.setParent("0123456789abcdef")
assert theItem.itemParent is None
theItem.setParent("0123456789abg")
assert theItem.itemParent is None
theItem.setParent("0123456789abc")
assert theItem.itemParent == "0123456789abc"
@@ -59,6 +67,19 @@ def testItemSettersSimple(nwDummy):
theItem.setStatus("Main")
assert theItem.itemStatus == "Main"
# Importance
theItem.itemClass = nwItemClass.NOVEL
theItem.setStatus("Nonsense")
assert theItem.itemStatus == "New"
theItem.setStatus("New")
assert theItem.itemStatus == "New"
theItem.setStatus("Note")
assert theItem.itemStatus == "Note"
theItem.setStatus("Draft")
assert theItem.itemStatus == "Draft"
theItem.setStatus("Finished")
assert theItem.itemStatus == "Finished"
# Expanded
theItem.setExpanded(8)
assert not theItem.isExpanded
@@ -73,6 +94,20 @@ def testItemSettersSimple(nwDummy):
theItem.setExpanded(True)
assert theItem.isExpanded
# Exported
theItem.setExported(8)
assert not theItem.isExported
theItem.setExported(None)
assert not theItem.isExported
theItem.setExported("None")
assert not theItem.isExported
theItem.setExported("What?")
assert not theItem.isExported
theItem.setExported("True")
assert theItem.isExported
theItem.setExported(True)
assert theItem.isExported
# CharCount
theItem.setCharCount(None)
assert theItem.charCount == 0
@@ -105,10 +140,47 @@ def testItemSettersSimple(nwDummy):
theItem.setCursorPos(1)
assert theItem.cursorPos == 1
@pytest.mark.project
def testItemClassSetter(nwDummy):
# Initial Count
theItem.setWordCount(234)
theItem.saveInitialCount()
assert theItem.initCount == 234
theProject = NWProject(nwDummy)
# END Test testCoreItem_Setters
@pytest.mark.core
def testCoreItem_TypeSetter(dummyGUI):
"""Test the setter for all the nwItemType values for the NWItem
class.
"""
theProject = NWProject(dummyGUI)
theItem = NWItem(theProject)
# Type
theItem.setType(None)
assert theItem.itemType == nwItemType.NO_TYPE
theItem.setType("NONSENSE")
assert theItem.itemType == nwItemType.NO_TYPE
theItem.setType("NO_TYPE")
assert theItem.itemType == nwItemType.NO_TYPE
theItem.setType("ROOT")
assert theItem.itemType == nwItemType.ROOT
theItem.setType("FOLDER")
assert theItem.itemType == nwItemType.FOLDER
theItem.setType("FILE")
assert theItem.itemType == nwItemType.FILE
theItem.setType("TRASH")
assert theItem.itemType == nwItemType.TRASH
theItem.setType(nwItemType.ROOT)
assert theItem.itemType == nwItemType.ROOT
# END Test testCoreItem_TypeSetter
@pytest.mark.core
def testCoreItem_ClassSetter(dummyGUI):
"""Test the setter for all the nwItemClass values for the NWItem
class.
"""
theProject = NWProject(dummyGUI)
theItem = NWItem(theProject)
# Class
@@ -138,33 +210,17 @@ def testItemClassSetter(nwDummy):
assert theItem.itemClass == nwItemClass.ARCHIVE
theItem.setClass("TRASH")
assert theItem.itemClass == nwItemClass.TRASH
theItem.setClass(nwItemClass.NOVEL)
assert theItem.itemClass == nwItemClass.NOVEL
@pytest.mark.project
def testItemTypeSetter(nwDummy):
# END Test testCoreItem_ClassSetter
theProject = NWProject(nwDummy)
theItem = NWItem(theProject)
# Type
theItem.setType(None)
assert theItem.itemType == nwItemType.NO_TYPE
theItem.setType("NONSENSE")
assert theItem.itemType == nwItemType.NO_TYPE
theItem.setType("NO_TYPE")
assert theItem.itemType == nwItemType.NO_TYPE
theItem.setType("ROOT")
assert theItem.itemType == nwItemType.ROOT
theItem.setType("FOLDER")
assert theItem.itemType == nwItemType.FOLDER
theItem.setType("FILE")
assert theItem.itemType == nwItemType.FILE
theItem.setType("TRASH")
assert theItem.itemType == nwItemType.TRASH
@pytest.mark.project
def testItemLayoutSetter(nwDummy):
theProject = NWProject(nwDummy)
@pytest.mark.core
def testCoreItem_LayoutSetter(dummyGUI):
"""Test the setter for all the nwItemLayout values for the NWItem
class.
"""
theProject = NWProject(dummyGUI)
theItem = NWItem(theProject)
# Layout
@@ -190,14 +246,22 @@ def testItemLayoutSetter(nwDummy):
assert theItem.itemLayout == nwItemLayout.SCENE
theItem.setLayout("NOTE")
assert theItem.itemLayout == nwItemLayout.NOTE
theItem.setLayout(nwItemLayout.NOTE)
assert theItem.itemLayout == nwItemLayout.NOTE
@pytest.mark.project
def testItemXMLPackUnpack(nwDummy):
# END Test testCoreItem_LayoutSetter
theProject = NWProject(nwDummy)
theItem = NWItem(theProject)
@pytest.mark.core
def testCoreItem_XMLPackUnpack(dummyGUI):
"""Test packing and unpacking XML objects for the NWItem class.
"""
theProject = NWProject(dummyGUI)
nwXML = etree.Element("novelWriterXML")
# File
# ====
theItem = NWItem(theProject)
theItem.setHandle("0123456789abc")
theItem.setParent("0123456789abc")
theItem.setOrder(1)
@@ -206,7 +270,7 @@ def testItemXMLPackUnpack(nwDummy):
theItem.setType("FILE")
theItem.setStatus("Main")
theItem.setLayout("NOTE")
theItem.setExpanded(True)
theItem.setExported(False)
theItem.setParaCount(3)
theItem.setWordCount(5)
theItem.setCharCount(7)
@@ -219,17 +283,18 @@ def testItemXMLPackUnpack(nwDummy):
b"<content>"
b"<item handle=\"0123456789abc\" order=\"1\" parent=\"0123456789abc\">"
b"<name>A Name</name><type>FILE</type><class>NOVEL</class><status>New</status>"
b"<exported>True</exported><layout>NOTE</layout><charCount>7</charCount>"
b"<exported>False</exported><layout>NOTE</layout><charCount>7</charCount>"
b"<wordCount>5</wordCount><paraCount>3</paraCount><cursorPos>11</cursorPos></item>"
b"</content>"
)
# Unpack
theItem = NWItem(theProject)
assert theItem.unpackXML(xContent[0])
assert theItem.itemHandle == "0123456789abc"
assert theItem.itemParent == "0123456789abc"
assert theItem.itemOrder == 1
assert theItem.isExpanded
assert theItem.isExported is False
assert theItem.paraCount == 3
assert theItem.wordCount == 5
assert theItem.charCount == 7
@@ -238,6 +303,52 @@ def testItemXMLPackUnpack(nwDummy):
assert theItem.itemType == nwItemType.FILE
assert theItem.itemLayout == nwItemLayout.NOTE
# Folder
# ======
theItem = NWItem(theProject)
theItem.setHandle("0123456789abc")
theItem.setParent("0123456789abc")
theItem.setOrder(1)
theItem.setName("A Name")
theItem.setClass("NOVEL")
theItem.setType("FOLDER")
theItem.setStatus("Main")
theItem.setLayout("NOTE")
theItem.setExpanded(True)
theItem.setExported(False)
theItem.setParaCount(3)
theItem.setWordCount(5)
theItem.setCharCount(7)
theItem.setCursorPos(11)
# Pack
xContent = etree.SubElement(nwXML, "content")
theItem.packXML(xContent)
assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == (
b"<content>"
b"<item handle=\"0123456789abc\" order=\"1\" parent=\"0123456789abc\">"
b"<name>A Name</name><type>FOLDER</type><class>NOVEL</class><status>New</status>"
b"<expanded>True</expanded></item>"
b"</content>"
)
# Unpack
theItem = NWItem(theProject)
assert theItem.unpackXML(xContent[0])
assert theItem.itemHandle == "0123456789abc"
assert theItem.itemParent == "0123456789abc"
assert theItem.itemOrder == 1
assert theItem.isExpanded is True
assert theItem.isExported is True
assert theItem.paraCount == 0
assert theItem.wordCount == 0
assert theItem.charCount == 0
assert theItem.cursorPos == 0
assert theItem.itemClass == nwItemClass.NOVEL
assert theItem.itemType == nwItemType.FOLDER
assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
# Errors
## Not an Item
@@ -268,3 +379,5 @@ def testItemXMLPackUnpack(nwDummy):
assert etree.tostring(xDummy, pretty_print=False, encoding="utf-8") == (
b"<group/>"
)
# END Test testCoreItem_XMLPackUnpack
+133
View File
@@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
"""novelWriter OptionState Class Tester
"""
import os
import json
import pytest
from dummy import causeOSError
from nw.core import NWProject
from nw.core.options import OptionState
from nw.constants import nwFiles
@pytest.mark.core
def testCoreOptions_LoadSave(monkeypatch, dummyGUI, tmpDir):
"""Test loading and saving from the OptionState class.
"""
theProject = NWProject(dummyGUI)
theOpts = OptionState(theProject)
# Write a test file
optFile = os.path.join(tmpDir, nwFiles.OPTS_FILE)
with open(optFile, mode="w+", encoding="utf8") as outFile:
json.dump({
"GuiBuildNovel": {
"winWidth": 1000,
"winHeight": 700,
"addNovel": True,
"addNotes": False,
"textFont": "Cantarell",
"dummyItem": None,
},
"DummyGroup": {
"dummyItem": None,
},
}, outFile)
# Load and save with no path set
theProject.projMeta = None
assert not theOpts.loadSettings()
assert not theOpts.saveSettings()
# Set path
theProject.projMeta = tmpDir
assert theProject.projMeta == tmpDir
# Cause open() to fail
monkeypatch.setattr("builtins.open", causeOSError)
assert not theOpts.loadSettings()
assert not theOpts.saveSettings()
monkeypatch.undo()
# Load proper
assert theOpts.loadSettings()
# Check that unwanted items have been removed
assert theOpts.theState == {
"GuiBuildNovel": {
"winWidth": 1000,
"winHeight": 700,
"addNovel": True,
"addNotes": False,
"textFont": "Cantarell",
},
}
# Save proper
assert theOpts.saveSettings()
# Load again to check we get the values back
assert theOpts.loadSettings()
assert theOpts.theState == {
"GuiBuildNovel": {
"winWidth": 1000,
"winHeight": 700,
"addNovel": True,
"addNotes": False,
"textFont": "Cantarell",
},
}
# END Test testCoreOptions_LoadSave
@pytest.mark.core
def testCoreOptions_SetGet(monkeypatch, dummyGUI, tmpDir):
"""Test setting and getting values from the OptionState class.
"""
theProject = NWProject(dummyGUI)
theOpts = OptionState(theProject)
# Set invalid values
assert not theOpts.setValue("DummyGroup", "dummyItem", None)
assert not theOpts.setValue("GuiBuildNovel", "dummyItem", None)
# Set valid value
assert theOpts.setValue("GuiBuildNovel", "winWidth", 100)
# Set some values of different types
assert theOpts.setValue("GuiBuildNovel", "winWidth", 100)
assert theOpts.setValue("GuiBuildNovel", "winHeight", 12.34)
assert theOpts.setValue("GuiBuildNovel", "addNovel", True)
assert theOpts.setValue("GuiBuildNovel", "textFont", "Cantarell")
# Generic get, doesn't check type
assert theOpts.getValue("GuiBuildNovel", "winWidth", None) == 100
assert theOpts.getValue("GuiBuildNovel", "winHeight", None) == 12.34
assert theOpts.getValue("GuiBuildNovel", "addNovel", None) is True
assert theOpts.getValue("GuiBuildNovel", "textFont", None) == "Cantarell"
assert theOpts.getValue("GuiBuildNovel", "dummyItem", None) is None
# Get type-specific
assert theOpts.getString("GuiBuildNovel", "winWidth", None) == "100"
assert theOpts.getString("GuiBuildNovel", "dummyItem", None) is None
assert theOpts.getInt("GuiBuildNovel", "winWidth", None) == 100
assert theOpts.getInt("GuiBuildNovel", "textFont", None) is None
assert theOpts.getInt("GuiBuildNovel", "dummyItem", None) is None
assert theOpts.getFloat("GuiBuildNovel", "winWidth", None) == 100.0
assert theOpts.getFloat("GuiBuildNovel", "textFont", None) is None
assert theOpts.getFloat("GuiBuildNovel", "dummyItem", None) is None
assert theOpts.getBool("GuiBuildNovel", "addNovel", None) is True
assert theOpts.getBool("GuiBuildNovel", "dummyItem", None) is None
# Check integer validators
assert theOpts.validIntRange(5, 0, 9, 3) == 5
assert theOpts.validIntRange(5, 0, 4, 3) == 3
assert theOpts.validIntRange(5, 0, 5, 3) == 5
assert theOpts.validIntRange(0, 0, 5, 3) == 0
assert theOpts.validIntTuple(0, (0, 1, 2), 3) == 0
assert theOpts.validIntTuple(5, (0, 1, 2), 3) == 3
# END Test testCoreOptions_SetGet
File diff suppressed because it is too large Load Diff
+170
View File
@@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
"""novelWriter Spell Check Class Tester
"""
import os
import sys
import pytest
from dummy import causeOSError
from tools import readFile, writeFile
from nw.core.spellcheck import NWSpellCheck, NWSpellEnchant, NWSpellSimple
from nw.constants import nwConst
@pytest.mark.core
def testCoreSpell_Super(monkeypatch, tmpDir, tmpConf):
"""Test the spell checker super class
"""
wList = os.path.join(tmpDir, "wordlist.txt")
writeFile(wList, "a_word\nb_word\nc_word\n")
spChk = NWSpellCheck()
spChk.mainConf = tmpConf
# Check that dummy functions return results that reflects that spell
# checking is effectively disabled
assert spChk.setLanguage("", "") is None
assert spChk.checkWord("")
assert spChk.suggestWords("") == []
assert spChk.listDictionaries() == []
assert spChk.describeDict() == ("", "")
# Check language info
assert NWSpellCheck.expandLanguage("en") == "English"
assert NWSpellCheck.expandLanguage("en_GB") == "English (GB)"
# Add a word to the user's dictionary
assert spChk._readProjectDictionary("dummy") is False
monkeypatch.setattr("builtins.open", causeOSError)
assert spChk._readProjectDictionary(wList) is False
monkeypatch.undo()
assert spChk._readProjectDictionary(wList) is True
assert spChk.projectDict == wList
# Cannot write to file
monkeypatch.setattr("builtins.open", causeOSError)
assert spChk.addWord("d_word") is False
monkeypatch.undo()
assert readFile(wList) == "a_word\nb_word\nc_word\n"
# First time, OK
assert spChk.addWord("d_word") is True
assert readFile(wList) == "a_word\nb_word\nc_word\nd_word\n"
# But not added twice
assert spChk.addWord("d_word") is False
assert readFile(wList) == "a_word\nb_word\nc_word\nd_word\n"
# END Test testCoreSpell_Super
@pytest.mark.core
def testCoreSpell_Enchant(monkeypatch, tmpDir, tmpConf):
"""Test the pyenchant spell checker
"""
wList = os.path.join(tmpDir, "wordlist.txt")
writeFile(wList, "a_word\nb_word\nc_word\n")
# Block the enchant package (and trigger the dummy class)
monkeypatch.setitem(sys.modules, "enchant", None)
spChk = NWSpellEnchant()
spChk.setLanguage("en", wList)
assert spChk.setLanguage("", "") is None
assert spChk.checkWord("")
assert spChk.suggestWords("") == []
assert spChk.listDictionaries() == []
assert spChk.describeDict() == ("", "")
monkeypatch.undo()
# Load the proper enchant package
spChk = NWSpellEnchant()
spChk.mainConf = tmpConf
spChk.setLanguage("en", wList)
assert spChk.checkWord("a_word")
assert spChk.checkWord("b_word")
assert spChk.checkWord("c_word")
assert not spChk.checkWord("d_word")
spChk.addWord("d_word")
assert spChk.checkWord("d_word")
wSuggest = spChk.suggestWords("wrod")
assert len(wSuggest) > 0
assert "word" in wSuggest
dList = spChk.listDictionaries()
assert len(dList) > 0
aTag, aName = spChk.describeDict()
assert aTag == "en"
assert aName != ""
# END Test testCoreSpell_Enchant
@pytest.mark.core
def testCoreSpell_Simple(monkeypatch, tmpDir, tmpConf):
"""Test the fallback simple spell checker
"""
wList = os.path.join(tmpDir, "wordlist.txt")
wDict = os.path.join(tmpDir, "en.dict")
writeFile(wList, "a_word\nb_word\nc_word\n")
writeFile(wDict, "# Comment\ne_word\nf_word\ng_word\n")
spChk = NWSpellSimple()
spChk.mainConf = tmpConf
spChk.mainConf.dictPath = tmpDir
# Load dictionary, but fail
monkeypatch.setattr("builtins.open", causeOSError)
spChk.setLanguage("en", wList)
assert spChk.spellLanguage is None
assert spChk.WORDS == spChk.projDict
monkeypatch.undo()
# Load dictionary properly
spChk.setLanguage("en", wList)
assert spChk.projDict == ["a_word", "b_word", "c_word"]
assert spChk.WORDS == ["e_word", "f_word", "g_word", "a_word", "b_word", "c_word"]
# Check words
assert spChk.checkWord("a_word")
assert spChk.checkWord("b_word")
assert spChk.checkWord("c_word")
assert not spChk.checkWord("d_word")
assert spChk.checkWord("e_word")
assert spChk.checkWord("f_word")
assert spChk.checkWord("g_word")
# Add word
spChk.addWord("d_word")
assert spChk.checkWord("d_word")
# Check spelling
assert spChk.suggestWords(" \t") == []
wSuggest = spChk.suggestWords("d_wrod")
assert len(wSuggest) > 0
assert "d_word" in wSuggest
# Break the matching
monkeypatch.setattr("difflib.get_close_matches", lambda *args, **kwargs: [""])
assert spChk.suggestWords("word") == []
monkeypatch.undo()
# Capitalisation
wSuggest = spChk.suggestWords("D_wrod")
assert len(wSuggest) > 0
assert "D_word" in wSuggest
# List dictionaries
assert spChk.listDictionaries() == [("en", "English [%s]" % nwConst.SP_INTERNAL)]
# Description
aTag, aName = spChk.describeDict()
assert aTag == "en"
assert aName == nwConst.SP_INTERNAL
# END Test testCoreSpell_Simple
+126
View File
@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
"""novelWriter Status Class Tester
"""
import pytest
from lxml import etree
from nw.core.status import NWStatus
@pytest.mark.core
def testCoreStatus_Entries():
"""Test all the simple setters for the NWItem class.
"""
theStatus = NWStatus()
# Add entries
theStatus.addEntry("New", (100, 100, 100))
theStatus.addEntry("Minor", (200, 50, 0))
theStatus.addEntry("Major", (200, 150, 0))
theStatus.addEntry("Main", (50, 200, 0))
assert theStatus._theLabels == ["New", "Minor", "Major", "Main"]
assert theStatus._theColours == [(100, 100, 100), (200, 50, 0), (200, 150, 0), (50, 200, 0)]
assert theStatus._theCounts == [0, 0, 0, 0]
assert theStatus._theMap["New"] == 0
assert theStatus._theMap["Minor"] == 1
assert theStatus._theMap["Major"] == 2
assert theStatus._theMap["Main"] == 3
assert theStatus._theLength == 4
# Lookups
assert theStatus.lookupEntry(None) is None
assert theStatus.lookupEntry("dummy") is None
assert theStatus.lookupEntry("Main") == 3
# Checks
assert theStatus.checkEntry(123) == "New"
assert theStatus.checkEntry("Stuff") == "New"
assert theStatus.checkEntry("New ") == "New"
assert theStatus.checkEntry(" Main ") == "Main"
# Set new list
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 theStatus.setNewEntries(None) == {}
assert theStatus.setNewEntries(newList) == {"Main": "Min"}
assert theStatus._theLabels == ["New", "Minor", "Major", "Min", "Max"]
assert theStatus._theColours == [(1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)]
assert theStatus._theCounts == [0, 0, 0, 0, 0]
assert theStatus._theMap["New"] == 0
assert theStatus._theMap["Minor"] == 1
assert theStatus._theMap["Major"] == 2
assert theStatus._theMap["Min"] == 3
assert theStatus._theMap["Max"] == 4
assert theStatus._theLength == 5
# Add counts
countTo = [3, 5, 7, 9, 11]
for i, n in enumerate(countTo):
for _ in range(n):
theStatus.countEntry(theStatus._theLabels[i])
assert theStatus._theCounts == countTo
# Iterate
for i, (sA, sB, sC) in enumerate(theStatus):
assert sA == theStatus._theLabels[i]
assert sB == theStatus._theColours[i]
assert sC == theStatus._theCounts[i]
assert theStatus[9] == (None, None, None)
# Clear counts
theStatus.resetCounts()
assert theStatus._theCounts == [0, 0, 0, 0, 0]
# END Test testCoreStatus_Entries
@pytest.mark.core
def testCoreStatus_XMLPackUnpack():
"""Test all the simple setters for the NWItem class.
"""
theStatus = NWStatus()
theStatus.addEntry("New", (100, 100, 100))
theStatus.addEntry("Minor", (200, 50, 0))
theStatus.addEntry("Major", (200, 150, 0))
theStatus.addEntry("Main", (50, 200, 0))
countTo = [3, 5, 7, 9]
for i, n in enumerate(countTo):
for _ in range(n):
theStatus.countEntry(theStatus._theLabels[i])
nwXML = etree.Element("novelWriterXML")
# Pack
xStatus = etree.SubElement(nwXML, "status")
theStatus.packXML(xStatus)
assert etree.tostring(xStatus, pretty_print=False, encoding="utf-8") == (
b"<status>"
b"<entry blue=\"100\" green=\"100\" red=\"100\">New</entry>"
b"<entry blue=\"0\" green=\"50\" red=\"200\">Minor</entry>"
b"<entry blue=\"0\" green=\"150\" red=\"200\">Major</entry>"
b"<entry blue=\"0\" green=\"200\" red=\"50\">Main</entry>"
b"</status>"
)
# Unpack
theStatus = NWStatus()
assert theStatus.unpackXML(xStatus)
assert theStatus._theLabels == ["New", "Minor", "Major", "Main"]
assert theStatus._theColours == [(100, 100, 100), (200, 50, 0), (200, 150, 0), (50, 200, 0)]
assert theStatus._theCounts == [0, 0, 0, 0]
assert theStatus._theMap["New"] == 0
assert theStatus._theMap["Minor"] == 1
assert theStatus._theMap["Major"] == 2
assert theStatus._theMap["Main"] == 3
assert theStatus._theLength == 4
# END Test testCoreStatus_XMLPackUnpack
+373
View File
@@ -0,0 +1,373 @@
# -*- coding: utf-8 -*-
"""novelWriter ToHtml Class Tester
"""
import pytest
from nw.core import NWProject, NWIndex, ToHtml
@pytest.mark.core
def testCoreToHtml_Format(dummyGUI):
"""Test all the formatters for the ToHtml class.
"""
theProject = NWProject(dummyGUI)
dummyGUI.theIndex = NWIndex(theProject, dummyGUI)
theHtml = ToHtml(theProject, dummyGUI)
# Export Mode
# ===========
assert theHtml._formatSynopsis("synopsis text") == (
"<p class='synopsis'><strong>Synopsis: </strong>synopsis text</p>\n"
)
assert theHtml._formatComments("comment text") == (
"<p class='comment'><strong>Comment: </strong>comment text</p>\n"
)
assert theHtml._formatKeywords("") == ""
assert theHtml._formatKeywords("tag: Jane") == (
"<div><span class='tags'>Tag:</span> <a name='tag_Jane'>Jane</a></div>"
)
assert theHtml._formatKeywords("char: Bod, Jane") == (
"<div>"
"<span class='tags'>Characters:</span> "
"<a href='#tag_Bod'>Bod</a>, "
"<a href='#tag_Jane'>Jane</a>"
"</div>"
)
# Preview Mode
# ============
theHtml.setPreview(True, True)
assert theHtml._formatSynopsis("synopsis text") == (
"<p class='comment'><span class='synopsis'>Synopsis: </span>synopsis text</p>\n"
)
assert theHtml._formatComments("comment text") == (
"<p class='comment'>comment text</p>\n"
)
assert theHtml._formatKeywords("") == ""
assert theHtml._formatKeywords("tag: Jane") == (
"<div><span class='tags'>Tag:</span> <a name='tag_Jane'>Jane</a></div>"
)
assert theHtml._formatKeywords("char: Bod, Jane") == (
"<div>"
"<span class='tags'>Characters:</span> "
"<a href='#char=Bod'>Bod</a>, "
"<a href='#char=Jane'>Jane</a>"
"</div>"
)
# END Test testCoreToHtml_Format
@pytest.mark.core
def testCoreToHtml_Convert(dummyGUI):
"""Test the converter of the ToHtml class.
"""
theProject = NWProject(dummyGUI)
dummyGUI.theIndex = NWIndex(theProject, dummyGUI)
theHtml = ToHtml(theProject, dummyGUI)
# Export Mode
# ===========
theHtml.isNovel = True
# Header 1
theHtml.theText = "# Title\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h1 class='title'>Title</h1>\n"
# Header 2
theHtml.theText = "## Chapter Title\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h1>Chapter Title</h1>\n"
# Header 3
theHtml.theText = "### Scene Title\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h2>Scene Title</h2>\n"
# Header 4
theHtml.theText = "#### Section Title\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h3>Section Title</h3>\n"
theHtml.isNovel = False
theHtml.setLinkHeaders(True)
# Header 1
theHtml.theText = "# Heading One\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h1><a name='T000001'></a>Heading One</h1>\n"
# Header 2
theHtml.theText = "## Heading Two\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h2><a name='T000001'></a>Heading Two</h2>\n"
# Header 3
theHtml.theText = "### Heading Three\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h3><a name='T000001'></a>Heading Three</h3>\n"
# Header 4
theHtml.theText = "#### Heading Four\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == "<h4><a name='T000001'></a>Heading Four</h4>\n"
# Text
theHtml.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
"<p>Some <strong>nested bold and <em>italic</em> and "
"<del>strikethrough</del> text</strong> here</p>\n"
)
# Text w/Hard Break
theHtml.theText = "Line one \nLine two \nLine three\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
"<p class='break'>Line one<br/>Line two<br/>Line three</p>\n"
)
# Synopsis
theHtml.theText = "%synopsis: The synopsis ...\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == ""
theHtml.setSynopsis(True)
theHtml.theText = "%synopsis: The synopsis ...\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
"<p class='synopsis'><strong>Synopsis: </strong>The synopsis ...</p>\n"
)
# Comment
theHtml.theText = "% A comment ...\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == ""
theHtml.setComments(True)
theHtml.theText = "% A comment ...\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
"<p class='comment'><strong>Comment: </strong>A comment ...</p>\n"
)
# Keywords
theHtml.theText = "@char: Bod, Jane\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == ""
theHtml.setKeywords(True)
theHtml.theText = "@char: Bod, Jane\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
"<div><span class='tags'>Characters:</span> "
"<a href='#tag_Bod'>Bod</a>, <a href='#tag_Jane'>Jane</a></div>"
)
# Direct Tests
# ============
theHtml.isNovel = True
# Title
theHtml.theTokens = [
(theHtml.T_TITLE, 1, "A Title", None, theHtml.A_PBB_NO | theHtml.A_CENTRE),
(theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE),
]
theHtml.doConvert()
assert theHtml.theResult == (
"<h1 class='title' style='text-align: center; page-break-before: never;'>"
"<a name='T000001'></a>A Title</h1>\n"
)
# Separator
theHtml.theTokens = [
(theHtml.T_SEP, 1, "* * *", None, theHtml.A_CENTRE),
(theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE),
]
theHtml.doConvert()
assert theHtml.theResult == "<p class='sep'>* * *</p>\n"
# Skip
theHtml.theTokens = [
(theHtml.T_SKIP, 1, "", None, theHtml.A_NONE),
(theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE),
]
theHtml.doConvert()
assert theHtml.theResult == "<p class='skip'>&nbsp;</p>\n"
# Styles
# ======
theHtml.setLinkHeaders(False)
# Align Left
theHtml.setStyles(False)
theHtml.theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_LEFT),
]
theHtml.doConvert()
assert theHtml.theResult == (
"<h1 class='title'>A Title</h1>\n"
)
theHtml.setStyles(True)
# Align Left
theHtml.theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_LEFT),
]
theHtml.doConvert()
assert theHtml.theResult == (
"<h1 class='title' style='text-align: left;'>A Title</h1>\n"
)
# Align Right
theHtml.theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_RIGHT),
]
theHtml.doConvert()
assert theHtml.theResult == (
"<h1 class='title' style='text-align: right;'>A Title</h1>\n"
)
# Align Centre
theHtml.theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_CENTRE),
]
theHtml.doConvert()
assert theHtml.theResult == (
"<h1 class='title' style='text-align: center;'>A Title</h1>\n"
)
# Align Justify
theHtml.theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_JUSTIFY),
]
theHtml.doConvert()
assert theHtml.theResult == (
"<h1 class='title' style='text-align: justify;'>A Title</h1>\n"
)
# Page Break Always
theHtml.theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB | theHtml.A_PBA),
]
theHtml.doConvert()
assert theHtml.theResult == (
"<h1 class='title' "
"style='page-break-before: always; page-break-after: always;'>A Title</h1>\n"
)
# Page Break Avoid
theHtml.theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_AV | theHtml.A_PBA_AV),
]
theHtml.doConvert()
assert theHtml.theResult == (
"<h1 class='title' "
"style='page-break-before: avoid; page-break-after: avoid;'>A Title</h1>\n"
)
# Page Break ANever
theHtml.theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_NO | theHtml.A_PBA_NO),
]
theHtml.doConvert()
assert theHtml.theResult == (
"<h1 class='title' "
"style='page-break-before: never; page-break-after: never;'>A Title</h1>\n"
)
# Preview Mode
# ============
theHtml.setPreview(True, True)
# Text (HTML4)
theHtml.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
"<p>Some <b>nested bold and <i>italic</i> and "
"<span style='text-decoration: line-through;'>strikethrough</span> "
"text</b> here</p>\n"
)
# END Test testCoreToHtml_Convert
@pytest.mark.core
def testCoreToHtml_Methods(dummyGUI):
"""Test all the other methods of the ToHtml class.
"""
theProject = NWProject(dummyGUI)
theHtml = ToHtml(theProject, dummyGUI)
# Auto-Replace
docText = "Text with <brackets> & shortdash, long—dash …\n"
theHtml.theText = docText
theHtml.doAutoReplace()
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
"<p>Text with &lt;brackets&gt; &amp; short&ndash;dash, long&mdash;dash &hellip;</p>\n"
)
# Revert on MD
assert theHtml.theMarkdown == (
"Text with &lt;brackets&gt; &amp; short&ndash;dash, long&mdash;dash &hellip;\n\n"
)
theHtml.doPostProcessing()
assert theHtml.theMarkdown == docText + "\n"
# With Preview, No Revert
theHtml.setPreview(True, True)
theHtml.theText = docText
theHtml.doAutoReplace()
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theMarkdown == (
"Text with &lt;brackets&gt; &amp; short&ndash;dash, long&mdash;dash &hellip;\n\n"
)
theHtml.doPostProcessing()
assert theHtml.theMarkdown == (
"Text with &lt;brackets&gt; &amp; short&ndash;dash, long&mdash;dash &hellip;\n\n"
)
# CSS
# ===
assert len(theHtml.getStyleSheet()) > 1
assert "p {text-align: left;}" in theHtml.getStyleSheet()
assert "p {text-align: justify;}" not in theHtml.getStyleSheet()
theHtml.setJustify(True)
assert "p {text-align: left;}" not in theHtml.getStyleSheet()
assert "p {text-align: justify;}" in theHtml.getStyleSheet()
theHtml.setStyles(False)
assert theHtml.getStyleSheet() == []
# END Test testCoreToHtml_Methods
+644
View File
@@ -0,0 +1,644 @@
# -*- coding: utf-8 -*-
"""novelWriter Tokenizer Class Tester
"""
import pytest
from nw.core import NWProject, NWDoc
from nw.core.tokenizer import Tokenizer
@pytest.mark.core
def testCoreToken_Setters(dummyGUI):
"""Test all the setters for the Tokenizer class.
"""
theProject = NWProject(dummyGUI)
theToken = Tokenizer(theProject, dummyGUI)
# Verify defaults
assert theToken.fmtTitle == "%title%"
assert theToken.fmtChapter == "%title%"
assert theToken.fmtUnNum == "%title%"
assert theToken.fmtScene == "%title%"
assert theToken.fmtSection == "%title%"
assert theToken.hideScene is False
assert theToken.hideSection is False
assert theToken.linkHeaders is False
assert theToken.doBodyText is True
assert theToken.doSynopsis is False
assert theToken.doComments is False
assert theToken.doKeywords is False
assert theToken.doJustify is False
# Set new values
theToken.setTitleFormat("T: %title%")
theToken.setChapterFormat("C: %title%")
theToken.setUnNumberedFormat("U: %title%")
theToken.setSceneFormat("S: %title%", True)
theToken.setSectionFormat("X: %title%", True)
theToken.setLinkHeaders(True)
theToken.setBodyText(False)
theToken.setSynopsis(True)
theToken.setComments(True)
theToken.setKeywords(True)
theToken.setJustify(True)
# Check new values
assert theToken.fmtTitle == "T: %title%"
assert theToken.fmtChapter == "C: %title%"
assert theToken.fmtUnNum == "U: %title%"
assert theToken.fmtScene == "S: %title%"
assert theToken.fmtSection == "X: %title%"
assert theToken.hideScene is True
assert theToken.hideSection is True
assert theToken.linkHeaders is True
assert theToken.doBodyText is False
assert theToken.doSynopsis is True
assert theToken.doComments is True
assert theToken.doKeywords is True
assert theToken.doJustify is True
# END Test testCoreToken_Setters
@pytest.mark.core
def testCoreToken_TextOps(monkeypatch, nwMinimal, dummyGUI):
"""Test handling files and text in the Tokenizer class.
"""
theProject = NWProject(dummyGUI)
theProject.projTree.setSeed(42)
theToken = Tokenizer(theProject, dummyGUI)
assert theProject.openProject(nwMinimal)
sHandle = "8c659a11cd429"
# Set some content to work with
docText = (
"### Scene Six\n\n"
"This is text with _italic text_, some **bold text**, some ~~deleted text~~, "
"and some **_mixed text_** and **some _nested_ text**.\n\n"
"#### Replace\n\n"
"Also, replace <A> and <B>.\n\n"
)
docTextR = docText.replace("<A>", "this").replace("<B>", "that")
nDoc = NWDoc(theProject, dummyGUI)
nDoc.openDocument(sHandle)
nDoc.saveDocument(docText)
nDoc.clearDocument()
theProject.setAutoReplace({"A": "this", "B": "that"})
assert theProject.saveProject()
# Root heading
assert theToken.addRootHeading("dummy") is False
assert theToken.addRootHeading(sHandle) is False
assert theToken.addRootHeading("7695ce551d265") is True
assert theToken.theMarkdown == "# Notes: Plot\n\n"
# Set text
assert theToken.setText("dummy") is False
assert theToken.setText(sHandle) is True
assert theToken.theText == docText
monkeypatch.setattr("nw.constants.nwConst.MAX_DOCSIZE", 100)
assert theToken.setText(sHandle, docText) is True
assert theToken.theText == (
"# ERROR\n\n"
"Document 'New Scene' is too big (0.00 MB). Skipping.\n\n"
)
monkeypatch.undo()
assert theToken.setText(sHandle, docText) is True
assert theToken.theText == docText
assert theToken.isNone is False
assert theToken.isTitle is False
assert theToken.isBook is False
assert theToken.isPage is False
assert theToken.isPart is False
assert theToken.isUnNum is False
assert theToken.isChap is False
assert theToken.isScene is True
assert theToken.isNote is False
assert theToken.isNovel is True
# Auto replace
theToken.doAutoReplace()
assert theToken.theText == docTextR
# Access
assert theToken.getResult() is None
assert theToken.getResultSize() == 0
theToken.theResult = ""
assert theToken.getResultSize() == 0
# Post Processing
theToken.theResult = r"This is text with escapes: \** \~~ \__"
theToken.doPostProcessing()
assert theToken.theResult == "This is text with escapes: ** ~~ __"
# END Test testCoreToken_TextOps
@pytest.mark.core
def testCoreToken_Tokenize(dummyGUI):
"""Test the tokenization of the Tokenizer class.
"""
theProject = NWProject(dummyGUI)
theToken = Tokenizer(theProject, dummyGUI)
# Header 1
theToken.theText = "# Novel Title\n"
theToken.tokenizeText()
assert theToken.theTokens == [
(Tokenizer.T_HEAD1, 1, "Novel Title", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "# Novel Title\n\n"
# Header 2
theToken.theText = "## Chapter One\n"
theToken.tokenizeText()
assert theToken.theTokens == [
(Tokenizer.T_HEAD2, 1, "Chapter One", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "## Chapter One\n\n"
# Header 3
theToken.theText = "### Scene One\n"
theToken.tokenizeText()
assert theToken.theTokens == [
(Tokenizer.T_HEAD3, 1, "Scene One", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "### Scene One\n\n"
# Header 4
theToken.theText = "#### A Section\n"
theToken.tokenizeText()
assert theToken.theTokens == [
(Tokenizer.T_HEAD4, 1, "A Section", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "#### A Section\n\n"
# Comment
theToken.theText = "% A comment\n"
theToken.tokenizeText()
assert theToken.theTokens == [
(Tokenizer.T_COMMENT, 1, "A comment", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "\n"
theToken.setComments(True)
theToken.tokenizeText()
assert theToken.theMarkdown == "% A comment\n\n"
# Symopsis
theToken.theText = "%synopsis: The synopsis\n"
theToken.tokenizeText()
assert theToken.theTokens == [
(Tokenizer.T_SYNOPSIS, 1, "The synopsis", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
theToken.theText = "% synopsis: The synopsis\n"
theToken.tokenizeText()
assert theToken.theTokens == [
(Tokenizer.T_SYNOPSIS, 1, "The synopsis", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "\n"
theToken.setSynopsis(True)
theToken.tokenizeText()
assert theToken.theMarkdown == "% synopsis: The synopsis\n\n"
# Keyword
theToken.theText = "@char: Bod\n"
theToken.tokenizeText()
assert theToken.theTokens == [
(Tokenizer.T_KEYWORD, 1, "char: Bod", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "\n"
theToken.setKeywords(True)
theToken.tokenizeText()
assert theToken.theMarkdown == "@char: Bod\n\n"
# Text
theToken.theText = "Some plain text\non two lines\n\n\n"
theToken.tokenizeText()
assert theToken.theTokens == [
(Tokenizer.T_TEXT, 1, "Some plain text", [], Tokenizer.A_NONE),
(Tokenizer.T_TEXT, 2, "on two lines", [], Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "Some plain text\non two lines\n\n\n\n"
theToken.setBodyText(False)
theToken.tokenizeText()
assert theToken.theTokens == [
(Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "\n\n\n"
theToken.setBodyText(True)
# Text Emphasis
theToken.theText = "Some **bolded text** on this lines\n"
theToken.tokenizeText()
assert theToken.theTokens == [
(
Tokenizer.T_TEXT, 1,
"Some **bolded text** on this lines",
[
[5, 2, Tokenizer.FMT_B_B],
[18, 2, Tokenizer.FMT_B_E],
],
Tokenizer.A_NONE
),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "Some **bolded text** on this lines\n\n"
theToken.theText = "Some _italic text_ on this lines\n"
theToken.tokenizeText()
assert theToken.theTokens == [
(
Tokenizer.T_TEXT, 1,
"Some _italic text_ on this lines",
[
[5, 1, Tokenizer.FMT_I_B],
[17, 1, Tokenizer.FMT_I_E],
],
Tokenizer.A_NONE
),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "Some _italic text_ on this lines\n\n"
theToken.theText = "Some **_bold italic text_** on this lines\n"
theToken.tokenizeText()
assert theToken.theTokens == [
(
Tokenizer.T_TEXT, 1,
"Some **_bold italic text_** on this lines",
[
[5, 2, Tokenizer.FMT_B_B],
[7, 1, Tokenizer.FMT_I_B],
[24, 1, Tokenizer.FMT_I_E],
[25, 2, Tokenizer.FMT_B_E],
],
Tokenizer.A_NONE
),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "Some **_bold italic text_** on this lines\n\n"
theToken.theText = "Some ~~strikethrough text~~ on this lines\n"
theToken.tokenizeText()
assert theToken.theTokens == [
(
Tokenizer.T_TEXT, 1,
"Some ~~strikethrough text~~ on this lines",
[
[5, 2, Tokenizer.FMT_D_B],
[25, 2, Tokenizer.FMT_D_E],
],
Tokenizer.A_NONE
),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "Some ~~strikethrough text~~ on this lines\n\n"
theToken.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theToken.tokenizeText()
assert theToken.theTokens == [
(
Tokenizer.T_TEXT, 1,
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here",
[
[5, 2, Tokenizer.FMT_B_B],
[23, 1, Tokenizer.FMT_I_B],
[30, 1, Tokenizer.FMT_I_E],
[36, 2, Tokenizer.FMT_D_B],
[51, 2, Tokenizer.FMT_D_E],
[58, 2, Tokenizer.FMT_B_E],
],
Tokenizer.A_NONE
),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == (
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n"
)
# Check the markdown function as well
assert theToken.getFilteredMarkdown() == (
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n"
)
# END Test testCoreToken_Tokenize
@pytest.mark.core
def testCoreToken_Headers(dummyGUI):
"""Test the header and page parser of the Tokenizer class.
"""
theProject = NWProject(dummyGUI)
theToken = Tokenizer(theProject, dummyGUI)
# Nothing
theToken.theText = "Some text ...\n"
assert theToken.doHeaders() is True
theToken.isNone = True
assert theToken.doHeaders() is False
theToken.isNone = False
assert theToken.doHeaders() is True
theToken.isNote = True
assert theToken.doHeaders() is False
theToken.isNote = False
##
# Novel
##
theToken.isNovel = True
# Titles
# ======
# H1: Title
theToken.theText = "# Novel Title\n"
theToken.setTitleFormat(r"T: %title%")
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_HEAD1, 1, "T: Novel Title", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# Chapters
# ========
# H2: Chapter
theToken.theText = "## Chapter One\n"
theToken.setChapterFormat(r"C: %title%")
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_HEAD2, 1, "C: Chapter One", None, Tokenizer.A_PBB),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# H2: Unnumbered Chapter
theToken.theText = "## Chapter One\n"
theToken.setUnNumberedFormat(r"U: %title%")
theToken.isUnNum = True
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_HEAD2, 1, "U: Chapter One", None, Tokenizer.A_PBB),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# H2: Unnumbered Chapter with Star
theToken.theText = "## *Prologue\n"
theToken.setUnNumberedFormat(r"U: %title%")
theToken.isUnNum = False
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_HEAD2, 1, "U: Prologue", None, Tokenizer.A_PBB),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# H2: Chapter Word Number
theToken.theText = "## Chapter\n"
theToken.setChapterFormat(r"Chapter %chw%")
theToken.numChapter = 0
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_HEAD2, 1, "Chapter One", None, Tokenizer.A_PBB),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# H2: Chapter Roman Number Upper Case
theToken.theText = "## Chapter\n"
theToken.setChapterFormat(r"Chapter %chI%")
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_HEAD2, 1, "Chapter II", None, Tokenizer.A_PBB),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# H2: Chapter Roman Number Lower Case
theToken.theText = "## Chapter\n"
theToken.setChapterFormat(r"Chapter %chi%")
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_HEAD2, 1, "Chapter iii", None, Tokenizer.A_PBB),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# Scenes
# ======
# H3: Scene w/Title
theToken.theText = "### Scene One\n"
theToken.setSceneFormat(r"S: %title%", False)
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_HEAD3, 1, "S: Scene One", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# H3: Scene Hidden wo/Format
theToken.theText = "### Scene One\n"
theToken.setSceneFormat(r"", True)
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# H3: Scene wo/Format, first
theToken.theText = "### Scene One\n"
theToken.setSceneFormat(r"", False)
theToken.firstScene = True
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# H3: Scene wo/Format, not first
theToken.theText = "### Scene One\n"
theToken.setSceneFormat(r"", False)
theToken.firstScene = False
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# H3: Scene Separator, first
theToken.theText = "### Scene One\n"
theToken.setSceneFormat(r"* * *", False)
theToken.firstScene = True
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# H3: Scene Separator, not first
theToken.theText = "### Scene One\n"
theToken.setSceneFormat(r"* * *", False)
theToken.firstScene = False
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_SEP, 1, "* * *", None, Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# H3: Scene w/Absolute Number
theToken.theText = "### A Scene\n"
theToken.setSceneFormat(r"Scene %sca%", False)
theToken.numAbsScene = 0
theToken.numChScene = 0
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_HEAD3, 1, "Scene 1", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# H3: Scene w/Chapter Number
theToken.theText = "### A Scene\n"
theToken.setSceneFormat(r"Scene %ch%.%sc%", False)
theToken.numAbsScene = 0
theToken.numChScene = 1
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_HEAD3, 1, "Scene 3.2", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# Sections
# ========
# H4: Section Hidden wo/Format
theToken.theText = "#### A Section\n"
theToken.setSectionFormat(r"", True)
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# H4: Section Visible wo/Format
theToken.theText = "#### A Section\n"
theToken.setSectionFormat(r"", False)
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_SKIP, 1, "", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# H4: Section w/Format
theToken.theText = "#### A Section\n"
theToken.setSectionFormat(r"X: %title%", False)
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_HEAD4, 1, "X: A Section", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# H4: Section Separator
theToken.theText = "#### A Section\n"
theToken.setSectionFormat(r"* * *", False)
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_SEP, 1, "* * *", None, Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
# Check the first scene detector
assert theToken.firstScene is False
theToken.firstScene = True
assert theToken.firstScene is True
theToken.theText = "Some text ...\n"
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.firstScene is False
##
# Title or Partition
##
theToken.isNovel = False
# H1: Title
theToken.theText = "# Novel Title\n"
theToken.tokenizeText()
theToken.isTitle = True
theToken.isPart = False
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_TITLE, 1, "Novel Title", None, Tokenizer.A_PBB_NO | Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_PBA | Tokenizer.A_CENTRE),
]
# H1: Partition
theToken.theText = "# Partition Title\n"
theToken.setTitleFormat(r"T: %title%")
theToken.tokenizeText()
theToken.isTitle = False
theToken.isPart = True
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_HEAD1, 1, "Partition Title", None, Tokenizer.A_PBB | Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_PBA | Tokenizer.A_CENTRE),
]
##
# Page
##
theToken.isNovel = False
theToken.isTitle = False
theToken.isPart = False
theToken.isPage = True
# Some Page Text
theToken.theText = "Page text\n\nMore text\n"
theToken.tokenizeText()
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_TEXT, 1, "Page text", [], Tokenizer.A_PBB | Tokenizer.A_LEFT),
(Tokenizer.T_EMPTY, 2, "", None, Tokenizer.A_LEFT),
(Tokenizer.T_TEXT, 3, "More text", [], Tokenizer.A_LEFT),
(Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_LEFT),
]
# END Test testCoreToken_Headers
@@ -7,7 +7,7 @@ import pytest
from nw.core.tools import countWords, numberToRoman, numberToWord
@pytest.mark.core
def testCountWords():
def testCoreTools_CountWords():
"""Test the word counter and the exclusion filers.
"""
testText = (
@@ -26,15 +26,46 @@ def testCountWords():
"\n"
"\n"
"The third paragraph.\n"
"\n"
"Dashes\u2013and even longer\u2014dashes."
)
cC, wC, pC = countWords(testText)
assert cC == 108
assert wC == 17
assert pC == 3
assert cC == 138
assert wC == 22
assert pC == 4
# END Test testCoreTools_CountWords
@pytest.mark.core
def testNumberWords():
def testCoreTools_RomanNumbers():
"""Test conversion of integers to Roman numbers.
"""
assert numberToRoman(None, False) == "NAN"
assert numberToRoman(0, False) == "OOR"
assert numberToRoman(1, False) == "I"
assert numberToRoman(2, False) == "II"
assert numberToRoman(3, False) == "III"
assert numberToRoman(4, False) == "IV"
assert numberToRoman(5, False) == "V"
assert numberToRoman(6, False) == "VI"
assert numberToRoman(7, False) == "VII"
assert numberToRoman(8, False) == "VIII"
assert numberToRoman(9, False) == "IX"
assert numberToRoman(10, False) == "X"
assert numberToRoman(14, False) == "XIV"
assert numberToRoman(42, False) == "XLII"
assert numberToRoman(99, False) == "XCIX"
assert numberToRoman(142, False) == "CXLII"
assert numberToRoman(542, False) == "DXLII"
assert numberToRoman(999, False) == "CMXCIX"
assert numberToRoman(2010, False) == "MMX"
assert numberToRoman(999, True) == "cmxcix"
# END Test testCoreTools_RomanNumbers
@pytest.mark.core
def testCoreTools_NumberWords():
"""Test the conversion of integer to English words.
"""
assert numberToWord(0, "en") == "Zero"
@@ -70,27 +101,9 @@ def testNumberWords():
assert numberToWord(2, "foo") == "Two"
assert numberToWord(3, "foo") == "Three"
@pytest.mark.core
def testRomanNumbers():
"""Test conversion of integers to Roman numbers.
"""
assert numberToRoman(None, False) == "NAN"
assert numberToRoman(0, False) == "OOR"
assert numberToRoman(1, False) == "I"
assert numberToRoman(2, False) == "II"
assert numberToRoman(3, False) == "III"
assert numberToRoman(4, False) == "IV"
assert numberToRoman(5, False) == "V"
assert numberToRoman(6, False) == "VI"
assert numberToRoman(7, False) == "VII"
assert numberToRoman(8, False) == "VIII"
assert numberToRoman(9, False) == "IX"
assert numberToRoman(10, False) == "X"
assert numberToRoman(14, False) == "XIV"
assert numberToRoman(42, False) == "XLII"
assert numberToRoman(99, False) == "XCIX"
assert numberToRoman(142, False) == "CXLII"
assert numberToRoman(542, False) == "DXLII"
assert numberToRoman(999, False) == "CMXCIX"
assert numberToRoman(2010, False) == "MMX"
assert numberToRoman(999, True) == "cmxcix"
# Test out of range values
assert numberToWord(12345, "en") == "[Out of Range]"
assert numberToWord(-2345, "en") == "[Negative]"
assert numberToWord("234", "en") == "[NaN]"
# END Test testCoreTools_NumberWords
+433
View File
@@ -0,0 +1,433 @@
# -*- coding: utf-8 -*-
"""novelWriter NWTree Class Tester
"""
import os
import pytest
from lxml import etree
from nw.core.project import NWProject, NWItem, NWTree
from nw.constants import nwItemClass, nwItemType, nwItemLayout, nwFiles
@pytest.fixture(scope="function")
def dummyItems(dummyGUI):
"""Create a list of dummy items.
"""
theProject = NWProject(dummyGUI)
itemA = NWItem(theProject)
itemA.itemName = "Novel"
itemA.itemType = nwItemType.ROOT
itemA.itemClass = nwItemClass.NOVEL
itemA.isExpanded = True
itemB = NWItem(theProject)
itemB.itemName = "Act One"
itemB.itemType = nwItemType.FOLDER
itemB.itemClass = nwItemClass.NOVEL
itemB.isExpanded = True
itemC = NWItem(theProject)
itemC.itemName = "Chapter One"
itemC.itemType = nwItemType.FILE
itemC.itemClass = nwItemClass.NOVEL
itemC.itemLayout = nwItemLayout.CHAPTER
itemC.charCount = 300
itemC.wordCount = 50
itemC.paraCount = 2
itemD = NWItem(theProject)
itemD.itemName = "Scene One"
itemD.itemType = nwItemType.FILE
itemD.itemClass = nwItemClass.NOVEL
itemD.itemLayout = nwItemLayout.SCENE
itemD.charCount = 3000
itemD.wordCount = 500
itemD.paraCount = 20
itemE = NWItem(theProject)
itemE.itemName = "Outtakes"
itemE.itemType = nwItemType.ROOT
itemE.itemClass = nwItemClass.ARCHIVE
itemE.isExpanded = False
itemF = NWItem(theProject)
itemF.itemName = "Trash"
itemF.itemType = nwItemType.TRASH
itemF.itemClass = nwItemClass.TRASH
itemF.isExpanded = False
itemG = NWItem(theProject)
itemG.itemName = "Characters"
itemG.itemType = nwItemType.ROOT
itemG.itemClass = nwItemClass.CHARACTER
itemG.isExpanded = True
itemH = NWItem(theProject)
itemH.itemName = "Jane Doe"
itemH.itemType = nwItemType.FILE
itemH.itemClass = nwItemClass.CHARACTER
itemH.itemLayout = nwItemLayout.NOTE
itemH.charCount = 2000
itemH.wordCount = 400
itemH.paraCount = 16
theItems = [
("a000000000001", None, itemA),
("b000000000001", "a000000000001", itemB),
("c000000000001", "b000000000001", itemC),
("c000000000002", "b000000000001", itemD),
("a000000000002", None, itemE),
("a000000000003", None, itemF),
("a000000000004", None, itemG),
("b000000000002", "a000000000002", itemH),
]
return theItems
@pytest.mark.core
def testCoreTree_BuildTree(dummyGUI, dummyItems):
"""Test building a project tree from a list of items.
"""
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
theTree.setSeed(42)
assert theTree._handleSeed == 42
# Check that tree is empty (calls NWTree.__bool__)
assert not theTree
# Check for archive and trash folders
assert theTree.trashRoot() is None
assert theTree.archiveRoot() is None
assert not theTree.isTrashRoot("a000000000003")
aHandles = []
for tHandle, pHande, nwItem in dummyItems:
aHandles.append(tHandle)
assert theTree.append(tHandle, pHande, nwItem)
assert theTree._treeChanged
# Check that tree is not empty (calls __bool__)
assert theTree
# Check the number of elements (calls __len__)
assert len(theTree) == len(dummyItems)
# Check that we have the correct handles
assert theTree.handles() == aHandles
# Check by iterator (calls __iter__, __next__ and __getitem__)
for theItem, theHandle in zip(theTree, aHandles):
assert theItem.itemHandle == theHandle
# Check that we have the correct archive and trash folders
assert theTree.trashRoot() == "a000000000003"
assert theTree.archiveRoot() == "a000000000002"
assert theTree.isTrashRoot("a000000000003")
# Try to add another trash folder
itemT = NWItem(theProject)
itemT.itemName = "Trash"
itemT.itemType = nwItemType.TRASH
itemT.itemClass = nwItemClass.TRASH
itemT.isExpanded = False
assert not theTree.append("1234567890abc", None, itemT)
assert len(theTree) == len(dummyItems)
# Generate handle automatically
itemT = NWItem(theProject)
itemT.itemName = "New File"
itemT.itemType = nwItemType.FILE
itemT.itemClass = nwItemClass.NOVEL
itemT.itemLayout = nwItemLayout.SCENE
assert theTree.append(None, None, itemT)
assert len(theTree) == len(dummyItems) + 1
theList = theTree.handles()
assert theList[-1] == "73475cb40a568"
# Try to add existing handle
assert not theTree.append("73475cb40a568", None, itemT)
assert len(theTree) == len(dummyItems) + 1
# Delete a non-existing item
del theTree["dummy"]
assert len(theTree) == len(dummyItems) + 1
# Delete the last item
del theTree["73475cb40a568"]
assert len(theTree) == len(dummyItems)
assert "73475cb40a568" not in theTree
# Delete the Novel, Archive and Trash folders
del theTree["a000000000001"]
assert len(theTree) == len(dummyItems) - 1
assert "a000000000001" not in theTree
del theTree["a000000000002"]
assert len(theTree) == len(dummyItems) - 2
assert "a000000000002" not in theTree
assert theTree.archiveRoot() is None
del theTree["a000000000003"]
assert len(theTree) == len(dummyItems) - 3
assert "a000000000003" not in theTree
assert theTree.trashRoot() is None
# END Test testCoreTree_BuildTree
@pytest.mark.core
def testCoreTree_Methods(dummyGUI, dummyItems):
"""Test building a project tree from a list of items.
"""
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
for tHandle, pHande, nwItem in dummyItems:
theTree.append(tHandle, pHande, nwItem)
assert len(theTree) == len(dummyItems)
# Root item lookup
theTree._treeRoots.append("dummy")
assert theTree.findRoot(nwItemClass.WORLD) is None
assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001"
assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004"
# Check for root uniqueness
assert theTree.checkRootUnique(nwItemClass.CUSTOM)
assert theTree.checkRootUnique(nwItemClass.WORLD)
assert not theTree.checkRootUnique(nwItemClass.NOVEL)
assert not theTree.checkRootUnique(nwItemClass.CHARACTER)
# Find root item of child item
assert theTree.getRootItem("b000000000001").itemHandle == "a000000000001"
assert theTree.getRootItem("c000000000001").itemHandle == "a000000000001"
assert theTree.getRootItem("c000000000002").itemHandle == "a000000000001"
assert theTree.getRootItem("dummy") is None
# Get item path
assert theTree.getItemPath("dummy") == []
assert theTree.getItemPath("c000000000001") == [
"c000000000001", "b000000000001", "a000000000001"
]
# Break the folder parent handle
theTree["b000000000001"].itemParent = "dummy"
assert theTree.getItemPath("c000000000001") == [
"c000000000001", "b000000000001"
]
theTree["b000000000001"].itemParent = "a000000000001"
assert theTree.getItemPath("c000000000001") == [
"c000000000001", "b000000000001", "a000000000001"
]
# Change file layout
assert not theTree.setFileItemLayout("dummy", nwItemLayout.UNNUMBERED)
assert not theTree.setFileItemLayout("b000000000001", nwItemLayout.UNNUMBERED)
assert not theTree.setFileItemLayout("c000000000001", "stuff")
assert theTree.setFileItemLayout("c000000000001", nwItemLayout.UNNUMBERED)
assert theTree["c000000000001"].itemLayout == nwItemLayout.UNNUMBERED
# END Test testCoreTree_Methods
@pytest.mark.core
def testCoreTree_MakeHandles(monkeypatch, dummyGUI):
"""Test generating item handles.
"""
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
theTree.setSeed(42)
tHandle = theTree._makeHandle()
assert tHandle == "73475cb40a568"
# Add the next in line to the project to foprce duplicate
theTree._projTree["44cb730c42048"] = None
tHandle = theTree._makeHandle()
assert tHandle == "71ee45a3c0db9"
# Fix the time() function and force a handle collission
theTree.setSeed(None)
monkeypatch.setattr("nw.core.tree.time", lambda: 123.4)
tHandle = theTree._makeHandle()
theTree._projTree[tHandle] = None
assert tHandle == "5f466d7afa48b"
tHandle = theTree._makeHandle()
theTree._projTree[tHandle] = None
assert tHandle == "a79acf4c634a7"
monkeypatch.undo()
# END Test testCoreTree_MakeHandles
@pytest.mark.core
def testCoreTree_Stats(dummyGUI, dummyItems):
"""Test project stats methods.
"""
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
for tHandle, pHande, nwItem in dummyItems:
theTree.append(tHandle, pHande, nwItem)
assert len(theTree) == len(dummyItems)
theTree._treeOrder.append("dummy")
# Count Words
novelWords, noteWords = theTree.sumWords()
assert novelWords == 550
assert noteWords == 400
# Count types
nRoot, nFolder, nFile = theTree.countTypes()
assert nRoot == 3
assert nFolder == 1
assert nFile == 3
# END Test testCoreTree_Stats
@pytest.mark.core
def testCoreTree_Reorder(dummyGUI, dummyItems):
"""Test changing tree order.
"""
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
aHandle = []
for tHandle, pHande, nwItem in dummyItems:
aHandle.append(tHandle)
theTree.append(tHandle, pHande, nwItem)
assert len(theTree) == len(dummyItems)
bHandle = aHandle.copy()
bHandle[2], bHandle[3] = bHandle[3], bHandle[2]
assert aHandle != bHandle
assert theTree.handles() == aHandle
theTree.setOrder(bHandle)
assert theTree.handles() == bHandle
theTree.setOrder(bHandle + ["dummy"])
assert theTree.handles() == bHandle
theTree._treeOrder.append("dummy")
theTree.setOrder(bHandle)
assert theTree.handles() == bHandle
# END Test testCoreTree_Reorder
@pytest.mark.core
def testCoreTree_XMLPackUnpack(dummyGUI, dummyItems):
"""Test changing tree order.
"""
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
for tHandle, pHande, nwItem in dummyItems:
theTree.append(tHandle, pHande, nwItem)
assert len(theTree) == len(dummyItems)
nwXML = etree.Element("novelWriterXML")
theTree.packXML(nwXML)
assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == (
b"<novelWriterXML>"
b"<content count=\"8\">"
b"<item handle=\"a000000000001\" order=\"0\" parent=\"None\">"
b"<name>Novel</name><type>ROOT</type><class>NOVEL</class><status>None</status>"
b"<expanded>True</expanded></item>"
b"<item handle=\"b000000000001\" order=\"0\" parent=\"a000000000001\">"
b"<name>Act One</name><type>FOLDER</type><class>NOVEL</class><status>None</status>"
b"<expanded>True</expanded></item>"
b"<item handle=\"c000000000001\" order=\"0\" parent=\"b000000000001\">"
b"<name>Chapter One</name><type>FILE</type><class>NOVEL</class><status>None</status>"
b"<exported>True</exported><layout>CHAPTER</layout><charCount>300</charCount>"
b"<wordCount>50</wordCount><paraCount>2</paraCount><cursorPos>0</cursorPos></item>"
b"<item handle=\"c000000000002\" order=\"0\" parent=\"b000000000001\">"
b"<name>Scene One</name><type>FILE</type><class>NOVEL</class><status>None</status>"
b"<exported>True</exported><layout>SCENE</layout><charCount>3000</charCount>"
b"<wordCount>500</wordCount><paraCount>20</paraCount><cursorPos>0</cursorPos></item>"
b"<item handle=\"a000000000002\" order=\"0\" parent=\"None\">"
b"<name>Outtakes</name><type>ROOT</type><class>ARCHIVE</class><status>None</status>"
b"<expanded>False</expanded></item>"
b"<item handle=\"a000000000003\" order=\"0\" parent=\"None\">"
b"<name>Trash</name><type>TRASH</type><class>TRASH</class><status>None</status>"
b"<expanded>False</expanded></item>"
b"<item handle=\"a000000000004\" order=\"0\" parent=\"None\">"
b"<name>Characters</name><type>ROOT</type><class>CHARACTER</class><status>None</status>"
b"<expanded>True</expanded></item>"
b"<item handle=\"b000000000002\" order=\"0\" parent=\"a000000000002\">"
b"<name>Jane Doe</name><type>FILE</type><class>CHARACTER</class><status>None</status>"
b"<exported>True</exported><layout>NOTE</layout><charCount>2000</charCount>"
b"<wordCount>400</wordCount><paraCount>16</paraCount><cursorPos>0</cursorPos></item>"
b"</content></novelWriterXML>"
)
theTree.clear()
assert len(theTree) == 0
assert not theTree.unpackXML(nwXML)
assert theTree.unpackXML(nwXML[0])
assert len(theTree) == len(dummyItems)
# END Test testCoreTree_XMLPackUnpack
@pytest.mark.core
def testCoreTree_ToCFile(monkeypatch, dummyGUI, dummyItems, tmpDir):
"""Test writing the ToC.txt file.
"""
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
for tHandle, pHande, nwItem in dummyItems:
theTree.append(tHandle, pHande, nwItem)
assert len(theTree) == len(dummyItems)
theTree._treeOrder.append("dummy")
def dummyIsFile(fileName):
"""Return True for items that are files in novelWriter and
should thus also be files in the project folder structure.
"""
dItem = theTree[fileName[8:21]]
assert dItem is not None
return dItem.itemType == nwItemType.FILE
monkeypatch.setattr("os.path.isfile", dummyIsFile)
theProject.projContent = "content"
theProject.projPath = None
assert not theTree.writeToCFile()
theProject.projPath = tmpDir
assert theTree.writeToCFile()
pathA = os.path.join("content", "c000000000001.nwd")
pathB = os.path.join("content", "c000000000002.nwd")
pathC = os.path.join("content", "b000000000002.nwd")
with open(os.path.join(tmpDir, nwFiles.TOC_TXT), mode="r", encoding="utf8") as inFile:
assert inFile.read() == (
"\n"
"Table of Contents\n"
"=================\n"
"\n"
"File Name Class Layout Document Label\n"
"-------------------------------------------------------------\n"
f"{pathA} NOVEL CHAPTER Chapter One\n"
f"{pathB} NOVEL SCENE Scene One\n"
f"{pathC} CHARACTER NOTE Jane Doe\n"
)
# END Test testCoreTree_ToCFile
-44
View File
@@ -1,44 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Error Tester
"""
import nw
import pytest
from PyQt5.QtWidgets import qApp
from nw.error import NWErrorMessage, exceptionHandler
@pytest.mark.error
def testErrorDialog(qtbot, nwFuncTemp, nwTemp):
qApp.closeAllWindows()
nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
nwErr = NWErrorMessage(nwGUI)
qtbot.addWidget(nwErr)
nwErr.show()
# Invalid Error
nwErr.setMessage(Exception, "Faulty Error", 123)
assert nwErr.msgBody.toPlainText() == "Failed to generate error report ..."
# Valid Error
nwErr.setMessage(Exception, "First Error", None)
theMessage = nwErr.msgBody.toPlainText()
assert theMessage
assert "First Error" in theMessage
assert "Exception" in theMessage
nwErr._doClose()
nwErr.close()
theMessage = exceptionHandler(Exception, "Second Error", None, testMode=True)
assert theMessage
assert "Second Error" in theMessage
assert "Exception" in theMessage
nwGUI.closeMain()
# qtbot.stopForInteraction()
@@ -9,7 +9,7 @@ import os
import sys
from shutil import copyfile
from nwtools import cmpFiles, getGuiItem
from tools import cmpFiles, getGuiItem
from PyQt5.QtCore import Qt, QItemSelectionModel
from PyQt5.QtWidgets import (
@@ -30,8 +30,8 @@ typeDelay = 1
stepDelay = 20
@pytest.mark.gui
def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
def testProjectSettings(qtbot, monkeypatch, yesToAll, fncDir, nwTempGUI, refDir, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -43,8 +43,8 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR
# Create new project
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": nwFuncTemp})
nwGUI.mainConf.backupPath = nwFuncTemp
assert nwGUI.newProject({"projPath": fncDir})
nwGUI.mainConf.backupPath = fncDir
# Get the dialog object
monkeypatch.setattr(GuiProjectSettings, "exec_", lambda *args: None)
@@ -135,9 +135,9 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR
qtbot.wait(stepDelay)
# Check the files
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
projFile = os.path.join(fncDir, "nwProject.nwx")
testFile = os.path.join(nwTempGUI, "2_nwProject.nwx")
refFile = os.path.join(nwRef, "gui", "2_nwProject.nwx")
refFile = os.path.join(refDir, "gui", "2_nwProject.nwx")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [2, 8, 9, 10])
@@ -145,8 +145,8 @@ def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwR
nwGUI.closeMain()
@pytest.mark.gui
def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
def testItemEditor(qtbot, yesToAll, monkeypatch, fncDir, nwTempGUI, refDir, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -154,7 +154,7 @@ def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, n
# Create new, save, open project
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": nwFuncTemp})
assert nwGUI.newProject({"projPath": fncDir})
assert nwGUI.openDocument("0e17daca5f3e1")
assert nwGUI.treeView.setSelectedHandle("0e17daca5f3e1", doScroll=True)
@@ -208,9 +208,9 @@ def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, n
qtbot.wait(stepDelay)
# Check the files
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
projFile = os.path.join(fncDir, "nwProject.nwx")
testFile = os.path.join(nwTempGUI, "3_nwProject.nwx")
refFile = os.path.join(nwRef, "gui", "3_nwProject.nwx")
refFile = os.path.join(refDir, "gui", "3_nwProject.nwx")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
@@ -218,8 +218,8 @@ def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, n
nwGUI.closeMain()
@pytest.mark.gui
def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
def testWritingStatsExport(qtbot, monkeypatch, yesToAll, fncDir, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -227,13 +227,13 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
# Create new, save, close project
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": nwFuncTemp})
assert nwGUI.newProject({"projPath": fncDir})
qtbot.wait(200)
assert nwGUI.saveProject()
assert nwGUI.closeProject()
qtbot.wait(stepDelay)
sessFile = os.path.join(nwFuncTemp, "meta", nwFiles.SESS_STATS)
sessFile = os.path.join(fncDir, "meta", nwFiles.SESS_STATS)
with open(sessFile, mode="w+", encoding="utf-8") as outFile:
outFile.write(
"# Start Time End Time Novel Notes\n"
@@ -244,10 +244,10 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
)
# Open again, and check the stats
assert nwGUI.openProject(nwFuncTemp)
assert nwGUI.openProject(fncDir)
qtbot.wait(stepDelay)
nwGUI.mainConf.lastPath = nwFuncTemp
nwGUI.mainConf.lastPath = fncDir
nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiWritingStats") is not None, timeout=1000)
@@ -264,7 +264,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(100)
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
jsonStats = os.path.join(fncDir, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.load(inFile)
@@ -282,7 +282,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
jsonStats = os.path.join(fncDir, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.loads(inFile.read())
@@ -299,7 +299,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
jsonStats = os.path.join(fncDir, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.load(inFile)
@@ -316,7 +316,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
jsonStats = os.path.join(fncDir, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.load(inFile)
@@ -329,7 +329,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
jsonStats = os.path.join(fncDir, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.load(inFile)
@@ -341,7 +341,7 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
jsonStats = os.path.join(fncDir, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.load(inFile)
@@ -357,8 +357,8 @@ def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
nwGUI.closeMain()
@pytest.mark.gui
def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp])
def testAboutBox(qtbot, monkeypatch, fncDir, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -374,12 +374,20 @@ def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp):
msgAbout.show()
assert msgAbout.pageAbout.document().characterCount() > 100
assert msgAbout.pageNotes.document().characterCount() > 100
assert msgAbout.pageLicense.document().characterCount() > 100
msgAbout.mainConf.guiLang = "whatever"
msgAbout.mainConf.assetPath = "whatever"
msgAbout._fillNotesPage()
assert msgAbout.pageNotes.toPlainText() == "Error loading release notes text ..."
msgAbout._fillLicensePage()
assert msgAbout.pageLicense.toPlainText() == "Error loading license text ..."
msgAbout.showReleaseNotes()
assert msgAbout.tabBox.currentWidget() == msgAbout.pageNotes
# Qt About
monkeypatch.setattr(QMessageBox, "aboutQt", lambda *args, **kwargs: None)
nwGUI.mainMenu.aAboutQt.activate(QAction.Trigger)
@@ -389,9 +397,9 @@ def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp):
nwGUI.closeMain()
@pytest.mark.gui
def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, refDir, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -420,13 +428,13 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
testFile = os.path.join(nwTempBuild, "1_LoremIpsum.nwd")
refFile = os.path.join(nwRef, "build", "1_LoremIpsum.nwd")
refFile = os.path.join(refDir, "build", "1_LoremIpsum.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
testFile = os.path.join(nwTempBuild, "1_LoremIpsum.htm")
refFile = os.path.join(nwRef, "build", "1_LoremIpsum.htm")
refFile = os.path.join(refDir, "build", "1_LoremIpsum.htm")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
@@ -459,13 +467,13 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
testFile = os.path.join(nwTempBuild, "2_LoremIpsum.nwd")
refFile = os.path.join(nwRef, "build", "2_LoremIpsum.nwd")
refFile = os.path.join(refDir, "build", "2_LoremIpsum.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
testFile = os.path.join(nwTempBuild, "2_LoremIpsum.htm")
refFile = os.path.join(nwRef, "build", "2_LoremIpsum.htm")
refFile = os.path.join(refDir, "build", "2_LoremIpsum.htm")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
@@ -479,14 +487,14 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
assert nwBuild._saveDocument(nwBuild.FMT_NWD)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
testFile = os.path.join(nwTempBuild, "3_LoremIpsum.nwd")
refFile = os.path.join(nwRef, "build", "3_LoremIpsum.nwd")
refFile = os.path.join(refDir, "build", "3_LoremIpsum.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
assert nwBuild._saveDocument(nwBuild.FMT_HTM)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
testFile = os.path.join(nwTempBuild, "3_LoremIpsum.htm")
refFile = os.path.join(nwRef, "build", "3_LoremIpsum.htm")
refFile = os.path.join(refDir, "build", "3_LoremIpsum.htm")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
@@ -513,14 +521,14 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
assert nwBuild._saveDocument(nwBuild.FMT_NWD)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
testFile = os.path.join(nwTempBuild, "4_LoremIpsum.nwd")
refFile = os.path.join(nwRef, "build", "4_LoremIpsum.nwd")
refFile = os.path.join(refDir, "build", "4_LoremIpsum.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
assert nwBuild._saveDocument(nwBuild.FMT_HTM)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
testFile = os.path.join(nwTempBuild, "4_LoremIpsum.htm")
refFile = os.path.join(nwRef, "build", "4_LoremIpsum.htm")
refFile = os.path.join(refDir, "build", "4_LoremIpsum.htm")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
@@ -528,14 +536,14 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
assert nwBuild._saveDocument(nwBuild.FMT_JSON_H)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.json")
testFile = os.path.join(nwTempBuild, "4H_LoremIpsum.json")
refFile = os.path.join(nwRef, "build", "4H_LoremIpsum.json")
refFile = os.path.join(refDir, "build", "4H_LoremIpsum.json")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [8])
assert nwBuild._saveDocument(nwBuild.FMT_JSON_M)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.json")
testFile = os.path.join(nwTempBuild, "4M_LoremIpsum.json")
refFile = os.path.join(nwRef, "build", "4M_LoremIpsum.json")
refFile = os.path.join(refDir, "build", "4M_LoremIpsum.json")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [8])
@@ -581,9 +589,9 @@ def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
nwGUI.closeMain()
@pytest.mark.gui
def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef, nwTemp):
def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, refDir, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -612,7 +620,7 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
projFile = os.path.join(nwLipsum, "content", "73475cb40a568.nwd")
testFile = os.path.join(nwTempGUI, "4_73475cb40a568.nwd")
refFile = os.path.join(nwRef, "gui", "4_73475cb40a568.nwd")
refFile = os.path.join(refDir, "gui", "4_73475cb40a568.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
@@ -638,7 +646,7 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
# This should give us back the file as it was before
projFile = os.path.join(nwLipsum, "content", "71ee45a3c0db9.nwd")
testFile = os.path.join(nwTempGUI, "4_71ee45a3c0db9.nwd")
refFile = os.path.join(nwRef, "gui", "4_73475cb40a568.nwd")
refFile = os.path.join(refDir, "gui", "4_73475cb40a568.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [1, 2, 3])
@@ -662,19 +670,19 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
projFile = os.path.join(nwLipsum, "content", "25fc0e7096fc6.nwd")
testFile = os.path.join(nwTempGUI, "5_25fc0e7096fc6.nwd")
refFile = os.path.join(nwRef, "gui", "5_25fc0e7096fc6.nwd")
refFile = os.path.join(refDir, "gui", "5_25fc0e7096fc6.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = os.path.join(nwLipsum, "content", "31489056e0916.nwd")
testFile = os.path.join(nwTempGUI, "5_31489056e0916.nwd")
refFile = os.path.join(nwRef, "gui", "5_31489056e0916.nwd")
refFile = os.path.join(refDir, "gui", "5_31489056e0916.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = os.path.join(nwLipsum, "content", "98010bd9270f9.nwd")
testFile = os.path.join(nwTempGUI, "5_98010bd9270f9.nwd")
refFile = os.path.join(nwRef, "gui", "5_98010bd9270f9.nwd")
refFile = os.path.join(refDir, "gui", "5_98010bd9270f9.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
@@ -700,31 +708,31 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
projFile = os.path.join(nwLipsum, "content", "1a6562590ef19.nwd")
testFile = os.path.join(nwTempGUI, "5_25fc0e7096fc6.nwd")
refFile = os.path.join(nwRef, "gui", "5_25fc0e7096fc6.nwd")
refFile = os.path.join(refDir, "gui", "5_25fc0e7096fc6.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [1, 2, 3])
projFile = os.path.join(nwLipsum, "content", "031b4af5197ec.nwd")
testFile = os.path.join(nwTempGUI, "5_031b4af5197ec.nwd")
refFile = os.path.join(nwRef, "gui", "5_031b4af5197ec.nwd")
refFile = os.path.join(refDir, "gui", "5_031b4af5197ec.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = os.path.join(nwLipsum, "content", "41cfc0d1f2d12.nwd")
testFile = os.path.join(nwTempGUI, "5_41cfc0d1f2d12.nwd")
refFile = os.path.join(nwRef, "gui", "5_41cfc0d1f2d12.nwd")
refFile = os.path.join(refDir, "gui", "5_41cfc0d1f2d12.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = os.path.join(nwLipsum, "content", "2858dcd1057d3.nwd")
testFile = os.path.join(nwTempGUI, "5_2858dcd1057d3.nwd")
refFile = os.path.join(nwRef, "gui", "5_2858dcd1057d3.nwd")
refFile = os.path.join(refDir, "gui", "5_2858dcd1057d3.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = os.path.join(nwLipsum, "content", "2fca346db6561.nwd")
testFile = os.path.join(nwTempGUI, "5_2fca346db6561.nwd")
refFile = os.path.join(nwRef, "gui", "5_2fca346db6561.nwd")
refFile = os.path.join(refDir, "gui", "5_2fca346db6561.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
@@ -732,7 +740,7 @@ def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef
nwGUI.closeMain()
@pytest.mark.gui
def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir):
if sys.platform.startswith("darwin"):
# Disable for macOS because the test segfaults on QWizard.show()
@@ -744,7 +752,7 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
ProjWizardCustomPage, ProjWizardFinalPage
)
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -929,8 +937,8 @@ def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
nwGUI.close()
@pytest.mark.gui
def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp])
def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -1009,8 +1017,8 @@ def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
nwGUI.close()
@pytest.mark.gui
def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpConf):
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp])
def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir, refDir, tmpConf):
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -1195,20 +1203,21 @@ def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpC
# qtbot.stopForInteraction()
nwGUI.closeMain()
refConf = os.path.join(nwRef, "novelwriter_prefs.conf")
refConf = os.path.join(refDir, "novelwriter_prefs.conf")
projConf = os.path.join(nwGUI.mainConf.confPath, "novelwriter.conf")
testConf = os.path.join(nwTemp, "novelwriter_prefs.conf")
testConf = os.path.join(tmpDir, "novelwriter_prefs.conf")
copyfile(projConf, testConf)
ignoreLines = [
2, # Timestamp
11, 12, 13, 14, 15, 16, 17, # Window sizes
7, 27, # Fonts (depends on system default)
9, # Release Notes
12, 13, 14, 15, 16, 17, 18, # Window sizes
7, 28, # Fonts (depends on system default)
]
assert cmpFiles(testConf, refConf, ignoreLines)
@pytest.mark.gui
def testQuotesDialog(qtbot, yesToAll, nwMinimal, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
def testQuotesDialog(qtbot, yesToAll, nwMinimal, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -1237,15 +1246,15 @@ def testQuotesDialog(qtbot, yesToAll, nwMinimal, nwTemp):
nwGUI.close()
@pytest.mark.gui
def testDialogsOpenClose(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
def testDialogsOpenClose(qtbot, monkeypatch, yesToAll, nwMinimal, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
qtbot.wait(stepDelay)
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: nwTemp)
assert nwGUI.selectProjectPath() == nwTemp
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: tmpDir)
assert nwGUI.selectProjectPath() == tmpDir
# qtbot.stopForInteraction()
nwGUI.closeMain()
+39 -117
View File
@@ -4,12 +4,10 @@
import nw
import pytest
import logging
import os
import sys
from shutil import copyfile
from nwtools import cmpFiles
from tools import cmpFiles
from PyQt5.QtCore import Qt, QUrl, QPoint, QItemSelectionModel
from PyQt5.QtGui import QTextCursor, QColor, QPixmap, QIcon, QTextBlock
@@ -27,85 +25,9 @@ typeDelay = 1
stepDelay = 20
@pytest.mark.gui
def testLaunch(qtbot, monkeypatch, nwFuncTemp, nwTemp):
def testDocEditor(qtbot, yesToAll, fncDir, nwTempGUI, refDir, tmpDir):
# Defaults
nwGUI = nw.main(
["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp, "--style=Fusion"]
)
assert nw.logger.getEffectiveLevel() == logging.WARNING
nwGUI.closeMain()
nwGUI.close()
# Log Levels
nwGUI = nw.main(
["--testmode", "--info", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
)
assert nw.logger.getEffectiveLevel() == logging.INFO
nwGUI.closeMain()
nwGUI.close()
nwGUI = nw.main(
["--testmode", "--debug", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
)
assert nw.logger.getEffectiveLevel() == logging.DEBUG
nwGUI.closeMain()
nwGUI.close()
nwGUI = nw.main(
["--testmode", "--verbose", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
)
assert nw.logger.getEffectiveLevel() == 5
nwGUI.closeMain()
nwGUI.close()
# Help and Version
with pytest.raises(SystemExit) as ex:
nwGUI = nw.main(
["--testmode", "--help", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
)
nwGUI.closeMain()
nwGUI.close()
assert ex.value.code == 0
with pytest.raises(SystemExit) as ex:
nwGUI = nw.main(
["--testmode", "--version", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
)
nwGUI.closeMain()
nwGUI.close()
assert ex.value.code == 0
# Invalid options
with pytest.raises(SystemExit) as ex:
nwGUI = nw.main(
["--testmode", "--invalid", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
)
nwGUI.closeMain()
nwGUI.close()
assert ex.value.code == 2
# Simulate import error
monkeypatch.setitem(sys.modules, "lxml", None)
monkeypatch.setattr("sys.hexversion", 0x0)
monkeypatch.setattr("nw.CONFIG.verQtValue", 50000)
monkeypatch.setattr("nw.CONFIG.verPyQtValue", 50000)
with pytest.raises(SystemExit) as ex:
nwGUI = nw.main(
["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp]
)
nwGUI.closeMain()
nwGUI.close()
assert ex.value.code & 4 == 4 # Python version not satisfied
assert ex.value.code & 8 == 8 # Qt version not satisfied
assert ex.value.code & 16 == 16 # PyQt version not satisfied
assert ex.value.code & 32 == 32 # lxml package missing
monkeypatch.undo()
@pytest.mark.gui
def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -113,7 +35,7 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
# Create new, save, close project
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": nwFuncTemp})
assert nwGUI.newProject({"projPath": fncDir})
assert nwGUI.saveProject()
assert nwGUI.closeProject()
@@ -130,9 +52,9 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
assert not nwGUI.theProject.spellCheck
# Check the files
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
projFile = os.path.join(fncDir, "nwProject.nwx")
testFile = os.path.join(nwTempGUI, "0_nwProject.nwx")
refFile = os.path.join(nwRef, "gui", "0_nwProject.nwx")
refFile = os.path.join(refDir, "gui", "0_nwProject.nwx")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
qtbot.wait(stepDelay)
@@ -140,7 +62,7 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
# qtbot.stopForInteraction()
# Re-open project
assert nwGUI.openProject(nwFuncTemp)
assert nwGUI.openProject(fncDir)
qtbot.wait(stepDelay)
# Check that we loaded the data
@@ -148,8 +70,8 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
assert len(nwGUI.theProject.projTree._treeOrder) == 8
assert len(nwGUI.theProject.projTree._treeRoots) == 4
assert nwGUI.theProject.projTree.trashRoot() is None
assert nwGUI.theProject.projPath == nwFuncTemp
assert nwGUI.theProject.projMeta == os.path.join(nwFuncTemp, "meta")
assert nwGUI.theProject.projPath == fncDir
assert nwGUI.theProject.projMeta == os.path.join(fncDir, "meta")
assert nwGUI.theProject.projFile == "nwProject.nwx"
assert nwGUI.theProject.projName == "New Project"
assert nwGUI.theProject.bookTitle == ""
@@ -379,33 +301,33 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
assert nwGUI.saveProject()
# Check the files
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
projFile = os.path.join(fncDir, "nwProject.nwx")
testFile = os.path.join(nwTempGUI, "1_nwProject.nwx")
refFile = os.path.join(nwRef, "gui", "1_nwProject.nwx")
refFile = os.path.join(refDir, "gui", "1_nwProject.nwx")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
projFile = os.path.join(nwFuncTemp, "content", "031b4af5197ec.nwd")
projFile = os.path.join(fncDir, "content", "031b4af5197ec.nwd")
testFile = os.path.join(nwTempGUI, "1_031b4af5197ec.nwd")
refFile = os.path.join(nwRef, "gui", "1_031b4af5197ec.nwd")
refFile = os.path.join(refDir, "gui", "1_031b4af5197ec.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = os.path.join(nwFuncTemp, "content", "1a6562590ef19.nwd")
projFile = os.path.join(fncDir, "content", "1a6562590ef19.nwd")
testFile = os.path.join(nwTempGUI, "1_1a6562590ef19.nwd")
refFile = os.path.join(nwRef, "gui", "1_1a6562590ef19.nwd")
refFile = os.path.join(refDir, "gui", "1_1a6562590ef19.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = os.path.join(nwFuncTemp, "content", "0e17daca5f3e1.nwd")
projFile = os.path.join(fncDir, "content", "0e17daca5f3e1.nwd")
testFile = os.path.join(nwTempGUI, "1_0e17daca5f3e1.nwd")
refFile = os.path.join(nwRef, "gui", "1_0e17daca5f3e1.nwd")
refFile = os.path.join(refDir, "gui", "1_0e17daca5f3e1.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = os.path.join(nwFuncTemp, "content", "41cfc0d1f2d12.nwd")
projFile = os.path.join(fncDir, "content", "41cfc0d1f2d12.nwd")
testFile = os.path.join(nwTempGUI, "1_41cfc0d1f2d12.nwd")
refFile = os.path.join(nwRef, "gui", "1_41cfc0d1f2d12.nwd")
refFile = os.path.join(refDir, "gui", "1_41cfc0d1f2d12.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
@@ -414,9 +336,9 @@ def testDocEditor(qtbot, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
nwGUI.close()
@pytest.mark.gui
def testDocViewer(qtbot, yesToAll, nwLipsum, nwTemp):
def testDocViewer(qtbot, yesToAll, nwLipsum, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -566,9 +488,9 @@ def testDocViewer(qtbot, yesToAll, nwLipsum, nwTemp):
nwGUI.close()
@pytest.mark.gui
def testProjectTree(qtbot, yesToAll, nwMinimal, nwTemp):
def testProjectTree(qtbot, yesToAll, nwMinimal, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -690,9 +612,9 @@ def testProjectTree(qtbot, yesToAll, nwMinimal, nwTemp):
nwGUI.close()
@pytest.mark.gui
def testEditFormatMenu(qtbot, yesToAll, nwLipsum, nwTemp):
def testEditFormatMenu(qtbot, yesToAll, nwLipsum, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -927,9 +849,9 @@ def testEditFormatMenu(qtbot, yesToAll, nwLipsum, nwTemp):
nwGUI.close()
@pytest.mark.gui
def testContextMenu(qtbot, yesToAll, nwLipsum, nwTemp):
def testContextMenu(qtbot, yesToAll, nwLipsum, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -1013,15 +935,15 @@ def testContextMenu(qtbot, yesToAll, nwLipsum, nwTemp):
nwGUI.close()
@pytest.mark.gui
def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
def testInsertMenu(qtbot, monkeypatch, fncDir, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
qtbot.wait(stepDelay)
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": nwFuncTemp})
assert nwGUI.newProject({"projPath": fncDir})
assert nwGUI.treeView._getTreeItem("0e17daca5f3e1") is not None
@@ -1162,7 +1084,7 @@ def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp):
assert not nwGUI.importDocument()
# Then a valid path, but bot a file that exists
theFile = os.path.join(nwTemp, "import.txt")
theFile = os.path.join(tmpDir, "import.txt")
monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwards: (theFile, ""))
assert not nwGUI.importDocument()
@@ -1205,16 +1127,16 @@ def testInsertMenu(qtbot, monkeypatch, nwFuncTemp, nwTemp):
assert len(theBits) == 3
assert theBits[0] == "File details for the currently open file"
assert theBits[1] == "Handle: 0e17daca5f3e1"
assert theBits[2] == "Location: %s" % os.path.join(nwFuncTemp, "content", "0e17daca5f3e1.nwd")
assert theBits[2] == "Location: %s" % os.path.join(fncDir, "content", "0e17daca5f3e1.nwd")
# qtbot.stopForInteraction()
nwGUI.closeMain()
nwGUI.close()
@pytest.mark.gui
def testTextSearch(qtbot, monkeypatch, yesToAll, nwLipsum, nwTemp):
def testTextSearch(qtbot, monkeypatch, yesToAll, nwLipsum, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -1392,9 +1314,9 @@ def testTextSearch(qtbot, monkeypatch, yesToAll, nwLipsum, nwTemp):
nwGUI.close()
@pytest.mark.gui
def testOutline(qtbot, yesToAll, nwLipsum, nwTemp):
def testOutline(qtbot, yesToAll, nwLipsum, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % tmpDir])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -1453,9 +1375,9 @@ def testOutline(qtbot, yesToAll, nwLipsum, nwTemp):
nwGUI.close()
@pytest.mark.gui
def testThemes(qtbot, yesToAll, nwMinimal, nwTemp):
def testThemes(qtbot, yesToAll, nwMinimal, tmpDir):
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -1478,7 +1400,7 @@ def testThemes(qtbot, yesToAll, nwMinimal, nwTemp):
# Re-open
assert nw.CONFIG.confPath == nwMinimal
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % tmpDir, nwMinimal])
assert nwGUI.mainConf.confPath == nwMinimal
qtbot.addWidget(nwGUI)
nwGUI.show()
-679
View File
@@ -1,679 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Project Class Tester
"""
import pytest
import os
from shutil import copyfile
from zipfile import ZipFile
from nwtools import cmpFiles
from nw.core.project import NWProject
from nw.core.document import NWDoc
from nw.core.spellcheck import NWSpellEnchant, NWSpellSimple
from nw.constants import nwConst, nwItemClass, nwItemType, nwItemLayout, nwFiles
@pytest.mark.project
def testProjectNewOpenSave(nwFuncTemp, nwTempProj, nwRef, nwTemp, nwDummy):
"""Test that a basic project can be created, and opened and saved.
"""
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
testFile = os.path.join(nwTempProj, "1_nwProject.nwx")
refFile = os.path.join(nwRef, "proj", "1_nwProject.nwx")
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
# Setting no data should fail
assert not theProject.newProject({})
# Try again with a proper path
assert theProject.newProject({"projPath": nwFuncTemp})
assert theProject.setProjectPath(nwFuncTemp)
assert theProject.saveProject()
assert theProject.closeProject()
# Creating the project once more should fail
assert not theProject.newProject({"projPath": nwFuncTemp})
# Check the new project
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [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, refFile, [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, refFile, [2, 6, 7, 8])
@pytest.mark.project
def testProjectNewRoot(nwFuncTemp, nwTempProj, nwRef, nwDummy):
"""Check that new root folders can be added to the project.
"""
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
testFile = os.path.join(nwTempProj, "2_nwProject.nwx")
refFile = os.path.join(nwRef, "proj", "2_nwProject.nwx")
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.newProject({"projPath": nwFuncTemp})
assert theProject.setProjectPath(nwFuncTemp)
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, refFile, [2, 6, 7, 8])
assert not theProject.projChanged
@pytest.mark.project
def testProjectNewFile(nwFuncTemp, nwTempProj, nwRef, nwDummy):
"""Check that new files can be added to the project.
"""
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
testFile = os.path.join(nwTempProj, "3_nwProject.nwx")
refFile = os.path.join(nwRef, "proj", "3_nwProject.nwx")
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.newProject({"projPath": nwFuncTemp})
assert theProject.setProjectPath(nwFuncTemp)
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, refFile, [2, 6, 7, 8])
assert not theProject.projChanged
@pytest.mark.project
def testProjectNewCustomA(nwFuncTemp, nwTempProj, nwRef, nwDummy):
"""Create a new project from a project wizard dictionary.
Custom type with chapters and scenes.
"""
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
testFile = os.path.join(nwTempProj, "4_nwProject.nwx")
refFile = os.path.join(nwRef, "proj", "4_nwProject.nwx")
projData = {
"projName": "Test Custom",
"projTitle": "Test Novel",
"projAuthors": "Jane Doe\nJohn Doh\n",
"projPath": nwFuncTemp,
"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(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.newProject(projData)
assert theProject.saveProject()
assert theProject.closeProject()
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
@pytest.mark.project
def testProjectNewCustomB(nwFuncTemp, nwTempProj, nwRef, nwDummy):
"""Create a new project from a project wizard dictionary.
Custom type without chapters, but with scenes.
"""
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
testFile = os.path.join(nwTempProj, "5_nwProject.nwx")
refFile = os.path.join(nwRef, "proj", "5_nwProject.nwx")
projData = {
"projName": "Test Custom",
"projTitle": "Test Novel",
"projAuthors": "Jane Doe\nJohn Doh\n",
"projPath": nwFuncTemp,
"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(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.newProject(projData)
assert theProject.saveProject()
assert theProject.closeProject()
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
@pytest.mark.project
def testProjectNewSampleA(nwFuncTemp, nwConf, nwDummy, nwTemp):
"""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": nwFuncTemp,
"popSample": True,
"popMinimal": False,
"popCustom": False,
}
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
theProject.mainConf = nwConf
# 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(nwConf.appRoot, "sample"))
dstSample = os.path.join(nwTemp, "sample.zip")
nwConf.assetPath = nwTemp
# 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(nwFuncTemp)
assert theProject.projName == "Sample Project"
assert theProject.saveProject()
assert theProject.closeProject()
os.unlink(dstSample)
@pytest.mark.project
def testProjectNewSampleB(monkeypatch, nwFuncTemp, nwConf, nwDummy, nwTemp):
"""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": nwFuncTemp,
"popSample": True,
"popMinimal": False,
"popCustom": False,
}
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
theProject.mainConf = nwConf
# Make sure we do not pick up the nw/assets/sample.zip file
nwConf.assetPath = nwTemp
# 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(nwFuncTemp)
assert theProject.projName == "Sample Project"
assert theProject.saveProject()
assert theProject.closeProject()
# Misdirect the appRoot path so neither is possible
nwConf.appRoot = nwTemp
assert not theProject.newProject(projData)
@pytest.mark.project
def testProjectMethods(monkeypatch, nwMinimal, nwDummy):
"""Test other project class methods and functions.
"""
theProject = NWProject(nwDummy)
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
def altMkdir(*args):
raise Exception("Oops!")
monkeypatch.setattr("os.mkdir", altMkdir)
projPath = os.path.join(nwMinimal, "dummy2")
assert not theProject.setProjectPath(projPath, newProject=True)
# 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"]
@pytest.mark.project
def testDocMeta(nwDummy, nwLipsum):
"""Check that the document meta data string is parsed correctly.
"""
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwLipsum)
aDoc = NWDoc(theProject, nwDummy)
assert aDoc.openDocument("47666c91c7ccf")
theName, theParent, theClass, theLayout = aDoc.getMeta()
assert theName == "Scene Five"
assert theParent == "6bd935d2490cd"
assert theClass == nwItemClass.NOVEL
assert theLayout == nwItemLayout.SCENE
aDoc._docMeta = {"stuff": None}
theName, theParent, theClass, theLayout = aDoc.getMeta()
assert theName == ""
assert theParent is None
assert theClass is None
assert theLayout is None
@pytest.mark.project
def testSpellEnchant(nwTemp, nwConf):
wList = os.path.join(nwTemp, "wordlist.txt")
with open(wList, mode="w") as wFile:
wFile.write("a_word\nb_word\nc_word\n")
spChk = NWSpellEnchant()
spChk.mainConf = nwConf
spChk.setLanguage("en", wList)
assert spChk.checkWord("a_word")
assert spChk.checkWord("b_word")
assert spChk.checkWord("c_word")
assert not spChk.checkWord("d_word")
spChk.addWord("d_word")
assert spChk.checkWord("d_word")
wSuggest = spChk.suggestWords("wrod")
assert len(wSuggest) > 0
assert "word" in wSuggest
dList = spChk.listDictionaries()
assert len(dList) > 0
aTag, aName = spChk.describeDict()
assert aTag == "en"
assert aName != ""
@pytest.mark.project
def testSpellSimple(nwTemp, nwConf):
wList = os.path.join(nwTemp, "wordlist.txt")
with open(wList, mode="w") as wFile:
wFile.write("a_word\nb_word\nc_word\n")
spChk = NWSpellSimple()
spChk.mainConf = nwConf
spChk.setLanguage("en", wList)
assert spChk.checkWord("a_word")
assert spChk.checkWord("b_word")
assert spChk.checkWord("c_word")
assert not spChk.checkWord("d_word")
spChk.addWord("d_word")
assert spChk.checkWord("d_word")
wSuggest = spChk.suggestWords("wrod")
assert len(wSuggest) > 0
assert "word" in wSuggest
dList = spChk.listDictionaries()
assert len(dList) > 0
aTag, aName = spChk.describeDict()
assert aTag == "en"
assert aName == nwConst.SP_INTERNAL
@pytest.mark.project
def testProjectOptions(nwDummy, nwLipsum):
"""Test the class that holds all the GUI state user options that are
tied to the current open project. Non-project related GUI options
are handled by the Config class.
"""
theProject = NWProject(nwDummy)
assert theProject.projMeta is None
theOpts = theProject.optState
assert not theOpts.loadSettings()
assert not theOpts.saveSettings()
# No Settings
assert theProject.openProject(nwLipsum)
assert theOpts.loadSettings()
assert theOpts.saveSettings()
assert str(theOpts.theState) == r"{}"
# Read Invalid Settings and Filter
stateFile = os.path.join(theProject.projMeta, nwFiles.OPTS_FILE)
with open(stateFile, mode="w", encoding="utf8") as outFile:
outFile.write(
r'{"GuiProjectSettings": {"winWidth": 100, "winHeight": 50}, "NoGroup": {"NoName": 0}}'
)
assert theOpts.loadSettings()
assert str(theOpts.theState) == r"{'GuiProjectSettings': {'winWidth': 100, 'winHeight': 50}}"
# Set New Settings
assert not theOpts.setValue("NoGroup", "NoName", None)
assert not theOpts.setValue("GuiProjectSettings", "NoName", None)
assert theOpts.setValue("GuiProjectSettings", "winWidth", 200)
assert theOpts.setValue("GuiProjectSettings", "winHeight", 80)
assert str(theOpts.theState) == r"{'GuiProjectSettings': {'winWidth': 200, 'winHeight': 80}}"
# Check Read/Write Types
## String
assert theOpts.setValue("GuiWritingStats", "winWidth", "123")
assert isinstance(theOpts.getString("GuiWritingStats", "winWidth", "456"), str)
assert theOpts.getString("GuiWritingStats", "NoName", "456") == "456"
## Int
assert theOpts.setValue("GuiWritingStats", "winWidth", "123")
assert isinstance(theOpts.getInt("GuiWritingStats", "winWidth", 456), int)
assert theOpts.getInt("GuiWritingStats", "NoName", 456) == 456
assert theOpts.setValue("GuiWritingStats", "winWidth", "True")
assert theOpts.getInt("GuiWritingStats", "NoName", 456) == 456
## Float
assert theOpts.setValue("GuiWritingStats", "winWidth", "123")
assert isinstance(theOpts.getFloat("GuiWritingStats", "winWidth", 456.0), float)
assert theOpts.getFloat("GuiWritingStats", "NoName", 456.0) == 456.0
assert theOpts.setValue("GuiWritingStats", "winWidth", "True")
assert theOpts.getFloat("GuiWritingStats", "winWidth", 456.0) == 456.0
## Bool
assert theOpts.setValue("GuiWritingStats", "winWidth", True)
assert isinstance(theOpts.getBool("GuiWritingStats", "winWidth", False), bool)
assert theOpts.getFloat("GuiWritingStats", "NoName", False) is False
assert theOpts.setValue("GuiWritingStats", "winWidth", "True")
assert theOpts.getFloat("GuiWritingStats", "winWidth", False) is False
@pytest.mark.project
def testProjectOrphanedFiles(nwDummy, 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(nwDummy)
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()
@pytest.mark.project
def testProjectOldFormat(nwDummy, 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(nwDummy)
theProject.mainConf.showGUI = False
# 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:
with open(aFile, mode="w+", encoding="utf8") as outFile:
outFile.write("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"))
@pytest.mark.project
def testProjectBackup(nwDummy, nwMinimal, nwTemp):
"""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(nwDummy)
assert theProject.openProject(nwMinimal)
# Test faulty settings
# Invalid path
theProject.mainConf.backupPath = None
assert not theProject.zipIt(doNotify=False)
# Missing project name
theProject.mainConf.backupPath = nwTemp
theProject.projName = ""
assert not theProject.zipIt(doNotify=False)
# Non-existent folder
theProject.mainConf.backupPath = os.path.join(nwTemp, "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)
# Test correct settings
theProject.mainConf.backupPath = nwTemp
assert theProject.zipIt(doNotify=False)
theFiles = os.listdir(os.path.join(nwTemp, "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(nwTemp, "Test Minimal", theZip), "r") as inZip:
inZip.extractall(os.path.join(nwTemp, "extract"))
# Check that the main project file was restored
assert cmpFiles(
os.path.join(nwMinimal, "nwProject.nwx"), os.path.join(nwTemp, "extract", "nwProject.nwx")
)
+12
View File
@@ -70,3 +70,15 @@ def getGuiItem(theName):
if qWidget.objectName() == theName:
return qWidget
return None
def readFile(fileName):
"""Returns the content of a file as a string.
"""
with open(fileName, mode="r", encoding="utf8") as inFile:
return inFile.read()
def writeFile(fileName, fileData):
"""Write the contents of a string to a file.
"""
with open(fileName, mode="w", encoding="utf8") as outFile:
outFile.write(fileData)