Complete the build process in the build tool

This commit is contained in:
Veronica Berglyd Olsen
2023-06-05 23:39:28 +02:00
parent b0201c58e9
commit 9bc66128a7
6 changed files with 156 additions and 112 deletions
+8 -8
View File
@@ -206,14 +206,14 @@ class nwLabels:
nwOutline.SYNOP: QT_TRANSLATE_NOOP("Constant", "Synopsis"), nwOutline.SYNOP: QT_TRANSLATE_NOOP("Constant", "Synopsis"),
} }
BUILD_FORMATS = { BUILD_FORMATS = {
"odt": ("odt", QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)")), "odt": (".odt", QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)")),
"fodt": ("fodt", QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)")), "fodt": (".fodt", QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)")),
"html": ("html", QT_TRANSLATE_NOOP("Constant", "novelWriter HTML (.html)")), "html": (".html", QT_TRANSLATE_NOOP("Constant", "novelWriter HTML (.html)")),
"nwd": ("nwd", QT_TRANSLATE_NOOP("Constant", "novelWriter Markdown (.nwd)")), "nwd": (".nwd", QT_TRANSLATE_NOOP("Constant", "novelWriter Markdown (.nwd)")),
"md": ("md", QT_TRANSLATE_NOOP("Constant", "Standard Markdown (.md)")), "md": (".md", QT_TRANSLATE_NOOP("Constant", "Standard Markdown (.md)")),
"md+": ("md", QT_TRANSLATE_NOOP("Constant", "Extended Markdown (.md)")), "md+": (".md", QT_TRANSLATE_NOOP("Constant", "Extended Markdown (.md)")),
"jhtml": ("json", QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter HTML (.json)")), "jhtml": (".json", QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter HTML (.json)")),
"jnwd": ("json", QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter Markdown (.json)")), "jnwd": (".json", QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter Markdown (.json)")),
} }
# END Class nwLabels # END Class nwLabels
+7 -1
View File
@@ -123,6 +123,7 @@ class FilterMode(Enum):
INCLUDED = 2 INCLUDED = 2
EXCLUDED = 3 EXCLUDED = 3
SKIPPED = 4 SKIPPED = 4
ROOT = 5
# END Enum FilterMode # END Enum FilterMode
@@ -311,7 +312,9 @@ class BuildSettings:
"""Check if a root handle is allowed in the build.""" """Check if a root handle is allowed in the build."""
return tHandle not in self._skipRoot return tHandle not in self._skipRoot
def buildItemFilter(self, project: NWProject) -> dict[str, tuple[bool, FilterMode]]: def buildItemFilter(
self, project: NWProject, withRoots: bool = False
) -> dict[str, tuple[bool, FilterMode]]:
"""Return a dictionary of item handles with filter decissions """Return a dictionary of item handles with filter decissions
applied. applied.
""" """
@@ -333,6 +336,9 @@ class BuildSettings:
if item.isInactiveClass() or (item.itemRoot in self._skipRoot): if item.isInactiveClass() or (item.itemRoot in self._skipRoot):
result[tHandle] = (False, FilterMode.SKIPPED) result[tHandle] = (False, FilterMode.SKIPPED)
continue continue
if withRoots and item.isRootType():
result[tHandle] = (True, FilterMode.ROOT)
continue
if not item.isFileType(): if not item.isFileType():
result[tHandle] = (False, FilterMode.SKIPPED) result[tHandle] = (False, FilterMode.SKIPPED)
continue continue
+83 -28
View File
@@ -106,25 +106,41 @@ class NWBuildDocument:
self._queue.append(item.itemHandle) self._queue.append(item.itemHandle)
return return
def iterBuild(self, path: Path, bFormat: str) -> Iterable[tuple[int, bool]]:
"""Wrapper for builder based on format."""
if bFormat in ("odt", "fodt"):
yield from self.iterBuildOpenDocument(path, bFormat == "fodt")
elif bFormat in ("html", "jhtml"):
yield from self.iterBuildHTML(path if bFormat == "html" else None)
elif bFormat in ("md", "md+"):
yield from self.iterBuildMarkdown(path, bFormat == "md+")
elif bFormat in ("nwd", "jnwd"):
yield from self.iterBuildNovelWriter(path if bFormat == "nwd" else None)
return
def iterBuildOpenDocument(self, path: Path, isFlat: bool) -> Iterable[tuple[int, bool]]: def iterBuildOpenDocument(self, path: Path, isFlat: bool) -> Iterable[tuple[int, bool]]:
"""Build an Open Document file.""" """Build an Open Document file."""
makeOdt = ToOdt(self._project, isFlat=isFlat) makeObj = ToOdt(self._project, isFlat=isFlat)
self._setupBuild(makeOdt) filtered = self._setupBuild(makeObj)
makeOdt.initDocument() makeObj.initDocument()
for i, tHandle in enumerate(self._queue): for i, tHandle in enumerate(self._queue):
yield i, self._doBuild(makeOdt, tHandle) self._error = None
if filtered.get(tHandle, (False, 0))[0]:
yield i, self._doBuild(makeObj, tHandle)
else:
yield i, False
makeOdt.closeDocument() makeObj.closeDocument()
self._error = None self._error = None
self._cache = makeOdt self._cache = makeObj
try: try:
if isFlat: if isFlat:
makeOdt.saveFlatXML(path) makeObj.saveFlatXML(path)
else: else:
makeOdt.saveOpenDocText(path) makeObj.saveOpenDocText(path)
except Exception as exc: except Exception as exc:
self._error = formatException(exc) self._error = formatException(exc)
@@ -134,21 +150,25 @@ class NWBuildDocument:
"""Build an HTML file. If path is None, no file is saved. This """Build an HTML file. If path is None, no file is saved. This
is used for generating build previews. is used for generating build previews.
""" """
makeHtml = ToHtml(self._project) makeObj = ToHtml(self._project)
self._setupBuild(makeHtml) filtered = self._setupBuild(makeObj)
if self._build.getBool("format.replaceTabs"): if self._build.getBool("format.replaceTabs"):
makeHtml.replaceTabs() makeObj.replaceTabs()
for i, tHandle in enumerate(self._queue): for i, tHandle in enumerate(self._queue):
yield i, self._doBuild(makeHtml, tHandle) self._error = None
if filtered.get(tHandle, (False, 0))[0]:
yield i, self._doBuild(makeObj, tHandle)
else:
yield i, False
self._error = None self._error = None
self._cache = makeHtml self._cache = makeObj
if isinstance(path, Path): if isinstance(path, Path):
try: try:
makeHtml.saveHTML5(path) makeObj.saveHTML5(path)
except Exception as exc: except Exception as exc:
self._error = formatException(exc) self._error = formatException(exc)
@@ -156,35 +176,66 @@ class NWBuildDocument:
def iterBuildMarkdown(self, path: Path, extendedMd: bool) -> Iterable[tuple[int, bool]]: def iterBuildMarkdown(self, path: Path, extendedMd: bool) -> Iterable[tuple[int, bool]]:
"""Build a Markdown file.""" """Build a Markdown file."""
makeMd = ToMarkdown(self._project) makeObj = ToMarkdown(self._project)
self._setupBuild(makeMd) filtered = self._setupBuild(makeObj)
if extendedMd: if extendedMd:
makeMd.setGitHubMarkdown() makeObj.setGitHubMarkdown()
else: else:
makeMd.setStandardMarkdown() makeObj.setStandardMarkdown()
if self._build.getBool("format.replaceTabs"): if self._build.getBool("format.replaceTabs"):
makeMd.replaceTabs(nSpaces=4, spaceChar=" ") makeObj.replaceTabs(nSpaces=4, spaceChar=" ")
for i, tHandle in enumerate(self._queue): for i, tHandle in enumerate(self._queue):
yield i, self._doBuild(makeMd, tHandle) self._error = None
if filtered.get(tHandle, (False, 0))[0]:
yield i, self._doBuild(makeObj, tHandle)
else:
yield i, False
self._error = None self._error = None
self._cache = makeMd self._cache = makeObj
try: try:
makeMd.saveMarkdown(path) makeObj.saveMarkdown(path)
except Exception as exc: except Exception as exc:
self._error = formatException(exc) self._error = formatException(exc)
return return
def iterBuildNovelWriter(self, path: Path | None) -> Iterable[tuple[int, bool]]:
"""Build a novelWriter Markdown file."""
makeObj = ToMarkdown(self._project)
filtered = self._setupBuild(makeObj)
makeObj.setKeepMarkdown(True)
if self._build.getBool("format.replaceTabs"):
makeObj.replaceTabs(nSpaces=4, spaceChar=" ")
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
yield i, self._doBuild(makeObj, tHandle, convert=False)
else:
yield i, False
self._error = None
self._cache = makeObj
if isinstance(path, Path):
try:
makeObj.saveRawMarkdown(path)
except Exception as exc:
self._error = formatException(exc)
return
## ##
# Internal Functions # Internal Functions
## ##
def _setupBuild(self, bldObj: Tokenizer): def _setupBuild(self, bldObj: Tokenizer) -> dict:
"""Configure the build object.""" """Configure the build object."""
# Get Settings # Get Settings
fmtTitle = self._build.getStr("headings.fmtTitle") fmtTitle = self._build.getStr("headings.fmtTitle")
@@ -199,6 +250,7 @@ class NWBuildDocument:
incComments = self._build.getBool("text.includeComments") incComments = self._build.getBool("text.includeComments")
incKeywords = self._build.getBool("text.includeKeywords") incKeywords = self._build.getBool("text.includeKeywords")
incBodyText = self._build.getBool("text.includeBodyText") incBodyText = self._build.getBool("text.includeBodyText")
noteHeadings = self._build.getBool("text.addNoteHeadings")
buildLang = self._build.getStr("format.buildLang") buildLang = self._build.getStr("format.buildLang")
textFont = self._build.getStr("format.textFont") textFont = self._build.getStr("format.textFont")
@@ -244,11 +296,12 @@ class NWBuildDocument:
bldObj.setColourHeaders(odtAddColours) bldObj.setColourHeaders(odtAddColours)
bldObj.setLanguage(buildLang) bldObj.setLanguage(buildLang)
return filtered = self._build.buildItemFilter(self._project, withRoots=noteHeadings)
def _doBuild(self, bldObj: Tokenizer, tHandle: str) -> bool: return filtered
def _doBuild(self, bldObj: Tokenizer, tHandle: str, convert: bool = True) -> bool:
"""Build a single document and add it to the build object.""" """Build a single document and add it to the build object."""
self._error = None
tItem = self._project.tree[tHandle] tItem = self._project.tree[tHandle]
if tItem is None: if tItem is None:
self._error = f"Build: Unknown item '{tHandle}'" self._error = f"Build: Unknown item '{tHandle}'"
@@ -258,13 +311,15 @@ class NWBuildDocument:
try: try:
if tItem.isRootType() and not tItem.isNovelLike(): if tItem.isRootType() and not tItem.isNovelLike():
bldObj.addRootHeading(tItem.itemHandle) bldObj.addRootHeading(tItem.itemHandle)
bldObj.doConvert() if convert:
bldObj.doConvert()
elif tItem.isFileType(): elif tItem.isFileType():
bldObj.setText(tHandle) bldObj.setText(tHandle)
bldObj.doPreProcessing() bldObj.doPreProcessing()
bldObj.tokenizeText() bldObj.tokenizeText()
bldObj.doHeaders() bldObj.doHeaders()
bldObj.doConvert() if convert:
bldObj.doConvert()
else: else:
logger.info(f"Build: Skipping '{tHandle}'") logger.info(f"Build: Skipping '{tHandle}'")
+1 -2
View File
@@ -786,8 +786,7 @@ class ItemIndex:
"""Iterate over all item headers of an item. """Iterate over all item headers of an item.
""" """
if tHandle in self._items: if tHandle in self._items:
for sTitle, hItem in self._items[tHandle].items(): yield from self._items[tHandle].items()
yield sTitle, hItem
return return
def iterAllHeaders(self): def iterAllHeaders(self):
+48 -61
View File
@@ -24,19 +24,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations from __future__ import annotations
import logging import logging
from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from PyQt5.QtCore import QSize, Qt, pyqtSlot from PyQt5.QtCore import QSize, Qt, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractButton, QAbstractItemView, QDialog, QDialogButtonBox, QGridLayout, QAbstractButton, QAbstractItemView, QDialog, QDialogButtonBox, QFileDialog,
QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QProgressBar, QLabel, QListWidget, QListWidgetItem, QProgressBar, QPushButton, QSplitter,
QPushButton, QSplitter, QVBoxLayout, QWidget QVBoxLayout, QWidget
) )
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.common import makeFileNameSafe from novelwriter.common import makeFileNameSafe
from novelwriter.constants import nwLabels from novelwriter.constants import nwLabels
from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
from novelwriter.core.buildsettings import BuildSettings from novelwriter.core.buildsettings import BuildSettings
@@ -70,11 +72,11 @@ class GuiManuscriptBuild(QDialog):
self.setWindowTitle(self.tr("Build Manuscript")) self.setWindowTitle(self.tr("Build Manuscript"))
self.setMinimumWidth(CONFIG.pxInt(500)) self.setMinimumWidth(CONFIG.pxInt(500))
self.setMinimumHeight(CONFIG.pxInt(300)) self.setMinimumHeight(CONFIG.pxInt(250))
iPx = self.mainTheme.baseIconSize iPx = self.mainTheme.baseIconSize
wWin = CONFIG.pxInt(660) wWin = CONFIG.pxInt(660)
hWin = CONFIG.pxInt(390) hWin = CONFIG.pxInt(360)
pOptions = self.theProject.options pOptions = self.theProject.options
self.resize( self.resize(
@@ -138,35 +140,18 @@ class GuiManuscriptBuild(QDialog):
self.lblMain.setWordWrap(True) self.lblMain.setWordWrap(True)
self.lblMain.setFont(font) self.lblMain.setFont(font)
self.spaceWidget = QWidget()
self.spaceWidget.setFixedHeight(CONFIG.pxInt(8))
# Build Path
self.lblPath = QLabel(self.tr("Build Folder"))
self.buildPath = QLineEdit()
self.buildPath.setText(str(self._build.lastPath))
self.btnBrowse = QPushButton(self.mainTheme.getIcon("browse"), "")
self.pathBox = QHBoxLayout()
self.pathBox.addWidget(self.buildPath)
self.pathBox.addWidget(self.btnBrowse)
# Build Name
self.lblName = QLabel(self.tr("Build Name"))
self.buildName = QLineEdit()
self.btnReset = QPushButton(self.mainTheme.getIcon("revert"), "")
self.btnReset.setToolTip(self.tr("Reset Build Name to default"))
self.btnReset.clicked.connect(self._doResetBuildName)
self.nameBox = QHBoxLayout()
self.nameBox.addWidget(self.buildName)
self.nameBox.addWidget(self.btnReset)
# Build Progress # Build Progress
self.lblProgress = QLabel(self.tr("Build Progress")) self.lblProgress = QLabel(self.tr("Build Progress"))
self.buildProgress = QProgressBar() self.buildProgress = QProgressBar()
self.buildProgress.setMinimum(0)
self.buildProgress.setValue(0) self.buildProgress.setValue(0)
self.progressBox = QVBoxLayout()
self.progressBox.addWidget(self.lblProgress)
self.progressBox.addWidget(self.buildProgress)
self.progressBox.setSpacing(CONFIG.pxInt(4))
# Dialog Buttons # Dialog Buttons
self.btnBuild = QPushButton(self.mainTheme.getIcon("export"), self.tr("&Build")) self.btnBuild = QPushButton(self.mainTheme.getIcon("export"), self.tr("&Build"))
self.dlgButtons = QDialogButtonBox(QDialogButtonBox.Close) self.dlgButtons = QDialogButtonBox(QDialogButtonBox.Close)
@@ -187,26 +172,15 @@ class GuiManuscriptBuild(QDialog):
CONFIG.pxInt(pOptions.getInt("GuiManuscriptBuild", "sumWidth", wWin//2)), CONFIG.pxInt(pOptions.getInt("GuiManuscriptBuild", "sumWidth", wWin//2)),
]) ])
self.outerBox = QGridLayout() self.outerBox = QVBoxLayout()
self.outerBox.addWidget(self.lblMain, 0, 0, 1, 2, Qt.AlignCenter) self.outerBox.addWidget(self.lblMain, 0, Qt.AlignCenter)
self.outerBox.addWidget(self.spaceWidget, 1, 0, 1, 2) self.outerBox.addWidget(self.mainSplit, 1)
self.outerBox.addWidget(self.mainSplit, 2, 0, 1, 2) self.outerBox.addLayout(self.progressBox, 0)
self.outerBox.addWidget(self.lblPath, 3, 0, 1, 1) self.outerBox.addWidget(self.dlgButtons, 0)
self.outerBox.addLayout(self.pathBox, 3, 1, 1, 1) self.outerBox.setSpacing(CONFIG.pxInt(12))
self.outerBox.addWidget(self.lblName, 4, 0, 1, 1)
self.outerBox.addLayout(self.nameBox, 4, 1, 1, 1)
self.outerBox.addWidget(self.lblProgress, 5, 0, 1, 1)
self.outerBox.addWidget(self.buildProgress, 5, 1, 1, 1)
self.outerBox.addWidget(self.dlgButtons, 6, 0, 1, 2)
self.outerBox.setRowStretch(2, 1)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
if self._build.lastBuildName:
self.buildName.setText(makeFileNameSafe(self._build.lastBuildName))
else:
self._doResetBuildName()
self.btnBuild.setFocus() self.btnBuild.setFocus()
self._populateContentList() self._populateContentList()
@@ -252,30 +226,43 @@ class GuiManuscriptBuild(QDialog):
self.close() self.close()
return return
@pyqtSlot()
def _doResetBuildName(self):
"""Generate a default build name."""
bName = makeFileNameSafe(f"{self.theProject.data.name} - {self._build.name}")
self.buildName.setText(bName)
self._build.setLastBuildName(bName)
return
## ##
# Internal Functions # Internal Functions
## ##
def _runBuild(self) -> bool: def _runBuild(self) -> bool:
"""Run the currently selected build.""" """Run the currently selected build."""
bFormat = self._getSelectedFormat() selFormat = self._getSelectedFormat()
if not bFormat: if not selFormat or selFormat not in nwLabels.BUILD_FORMATS:
return False return False
bPath = self.buildPath.text() lastName = self._build.lastBuildName
bName = self.buildName.text() if not lastName:
lastName = f"{self.theProject.data.name} - {self._build.name}.ext"
self._build.setLastFormat(bFormat) lastPath = self._build.lastPath
self._build.setLastPath(bPath) selExt = nwLabels.BUILD_FORMATS[selFormat][0]
self._build.setLastBuildName(bName) selName = Path(makeFileNameSafe(lastName)).with_suffix(selExt)
self.buildProgress.setValue(0)
savePath, _ = QFileDialog.getSaveFileName(
self, self.tr("Save Manuscript As"), str(lastPath / selName)
)
if not savePath:
return False
buildPath = Path(savePath)
docBuild = NWBuildDocument(self.theProject, self._build)
docBuild.queueAll()
self.buildProgress.setMaximum(len(docBuild))
for i, _ in docBuild.iterBuild(buildPath, selFormat):
self.buildProgress.setValue(i+1)
self._build.setLastFormat(selFormat)
self._build.setLastPath(buildPath.parent)
self._build.setLastBuildName(buildPath.name)
return True return True
+9 -12
View File
@@ -36,7 +36,7 @@ BUILD_CONF = {
"settings": { "settings": {
"filter.includeNovel": True, "filter.includeNovel": True,
"filter.includeNotes": True, "filter.includeNotes": True,
"filter.includeInactive": False, "filter.includeInactive": True,
"headings.fmtTitle": "Title: {Title}", "headings.fmtTitle": "Title: {Title}",
"headings.fmtChapter": "Chapter: {Title}", "headings.fmtChapter": "Chapter: {Title}",
"headings.fmtUnnumbered": "{Title}", "headings.fmtUnnumbered": "{Title}",
@@ -96,7 +96,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP
if docBuild.error: if docBuild.error:
error.append(docBuild.error) error.append(docBuild.error)
assert count == 21 assert count == 19
assert error == [] assert error == []
copyfile(docFile, tstFile) copyfile(docFile, tstFile)
@@ -114,7 +114,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP
if docBuild.error: if docBuild.error:
error.append(docBuild.error) error.append(docBuild.error)
assert count == 21 assert count == 19
assert error == [] assert error == []
assert docFile.is_file() assert docFile.is_file()
@@ -137,19 +137,17 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.toodt.ToOdt.doConvert", causeException) mp.setattr("novelwriter.core.toodt.ToOdt.doConvert", causeException)
assert len(docBuild) == 21
docBuild.addDocument("0000000000000")
assert len(docBuild) == 22
count = 0 count = 0
error = [] error = []
docFile = fncPath / "Lorem Ipsum Err.fodt" docFile = fncPath / "Lorem Ipsum Err.fodt"
for _, success in docBuild.iterBuildOpenDocument(docFile, True): for _, success in docBuild.iterBuildOpenDocument(docFile, True):
count += 1 if success else 0 count += 1 if success else 0
if docBuild.error: if not success and docBuild.error:
error.append(docBuild.error) error.append(docBuild.error)
assert count == 3 assert count == 1
assert error == [ assert error == [
"Build: Failed to build '7a992350f3eb6'", "Build: Failed to build '7a992350f3eb6'",
"Build: Failed to build '8c58a65414c23'", "Build: Failed to build '8c58a65414c23'",
@@ -169,7 +167,6 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP
"Build: Failed to build '2426c6f0ca922'", "Build: Failed to build '2426c6f0ca922'",
"Build: Failed to build '60bdf227455cc'", "Build: Failed to build '60bdf227455cc'",
"Build: Failed to build '04468803b92e1'", "Build: Failed to build '04468803b92e1'",
"Build: Unknown item '0000000000000'",
] ]
# END Test testCoreDocBuild_OpenDocument # END Test testCoreDocBuild_OpenDocument
@@ -204,7 +201,7 @@ def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
if docBuild.error: if docBuild.error:
error.append(docBuild.error) error.append(docBuild.error)
assert count == 21 assert count == 19
assert error == [] assert error == []
copyfile(docFile, tstFile) copyfile(docFile, tstFile)
@@ -255,7 +252,7 @@ def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths
if docBuild.error: if docBuild.error:
error.append(docBuild.error) error.append(docBuild.error)
assert count == 21 assert count == 19
assert error == [] assert error == []
copyfile(docFile, tstFile) copyfile(docFile, tstFile)
@@ -275,7 +272,7 @@ def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths
if docBuild.error: if docBuild.error:
error.append(docBuild.error) error.append(docBuild.error)
assert count == 21 assert count == 19
assert error == [] assert error == []
copyfile(docFile, tstFile) copyfile(docFile, tstFile)