Improve test coverage

This commit is contained in:
Veronica Berglyd Olsen
2024-03-26 00:26:24 +01:00
parent bdc1f0a5d4
commit eb0fac8a43
3 changed files with 232 additions and 15 deletions
+12 -13
View File
@@ -28,7 +28,7 @@ import logging
from time import time
from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QKeyEvent, QPalette
from PyQt5.QtGui import QCursor, QKeyEvent, QPalette
from PyQt5.QtWidgets import (
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QToolBar, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget, qApp
@@ -110,7 +110,6 @@ class GuiProjectSearch(QWidget):
self.searchResult.setColumnCount(2)
self.searchResult.setIconSize(QSize(iPx, iPx))
self.searchResult.setIndentation(iPx)
self.searchResult.itemPressed.connect(self._searchResultPressed)
self.searchResult.itemDoubleClicked.connect(self._searchResultDoubleClicked)
self.searchResult.itemSelectionChanged.connect(self._searchResultSelected)
@@ -160,8 +159,15 @@ class GuiProjectSearch(QWidget):
"""Process a return keypress forwarded from the main GUI."""
if self.searchText.hasFocus():
self._processSearch()
elif items := self.searchResult.selectedItems():
self._searchResultPressed(items[0], 0)
elif (
self.searchResult.hasFocus()
and (items := self.searchResult.selectedItems())
and (data := items[0].data(0, self.D_RESULT))
and len(data) == 3
):
self.openDocumentSelectRequest.emit(
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), False
)
return
def beginSearch(self) -> None:
@@ -211,6 +217,7 @@ class GuiProjectSearch(QWidget):
def _processSearch(self) -> None:
"""Perform a search."""
if not self._blocked:
qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
start = time()
self._blocked = True
self.searchResult.clear()
@@ -222,6 +229,7 @@ class GuiProjectSearch(QWidget):
self._appendResultSet(item, results, capped)
logger.debug("Search took %.3f ms", 1000*(time() - start))
self._time = time()
qApp.restoreOverrideCursor()
self._blocked = False
return
@@ -235,15 +243,6 @@ class GuiProjectSearch(QWidget):
self.selectedItemChanged.emit(str(data))
return
@pyqtSlot("QTreeWidgetItem*", int)
def _searchResultPressed(self, item: QTreeWidgetItem, column: int) -> None:
"""Process search result pressed."""
if (data := item.data(0, self.D_RESULT)) and len(data) == 3:
self.openDocumentSelectRequest.emit(
str(data[0]), checkInt(data[1], -1), checkInt(data[2], -1), False
)
return
@pyqtSlot("QTreeWidgetItem*", int)
def _searchResultDoubleClicked(self, item: QTreeWidgetItem, column: int) -> None:
"""Process search result double click."""
+95 -2
View File
@@ -32,9 +32,11 @@ from tools import C, NWD_IGNORE, buildTestProject, cmpFiles, XML_IGNORE
from mocked import causeOSError
from novelwriter import CONFIG
from novelwriter.constants import nwFiles, nwItemClass
from novelwriter.constants import nwConst, nwFiles, nwItemClass
from novelwriter.core.coretools import (
DocDuplicator, DocMerger, DocSearch, DocSplitter, ProjectBuilder
)
from novelwriter.core.project import NWProject
from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter, ProjectBuilder
@pytest.mark.core
@@ -401,6 +403,97 @@ def testCoreTools_DocDuplicator(mockGUI, fncPath, tstPaths, mockRnd):
# END Test testCoreTools_DocDuplicator
@pytest.mark.core
def testCoreTools_DocSearch(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText):
"""Test the DocDuplicator utility."""
project = NWProject()
mockRnd.reset()
buildTestProject(project, fncPath)
project.storage.getDocument(C.hSceneDoc).writeDocument(
"### New Scene\n\n" + "\n\n".join(ipsumText)
)
search = DocSearch()
# Defaults
# ========
result = [(i.itemHandle, r, c) for i, r, c in search.iterSearch(project, "Scene")]
assert result[0] == (C.hTitlePage, [], False)
assert result[1] == (C.hChapterDoc, [], False)
assert result[2] == (C.hSceneDoc, [(8, 5, "Scene")], False)
# Cache
assert list(search._cache.keys()) == [C.hTitlePage, C.hChapterDoc, C.hSceneDoc]
# Patterns
# ========
# Escape Using QRegularExpression
with monkeypatch.context() as mp:
mp.setattr(CONFIG, "verQtValue", 0x050f00)
assert search._buildPattern("[A-Za-z0-9_]+") == r"\[A\-Za\-z0\-9_\]\+"
# Escape Using Custom Implementation
with monkeypatch.context() as mp:
mp.setattr(CONFIG, "verQtValue", 0x050d00)
assert search._buildPattern("[A-Za-z0-9_]+") == r"\[A\-Za\-z0\-9_\]\+"
# Whole Words
search.setWholeWords(True)
search.setUserRegEx(True)
assert search._buildPattern("Hi") == r"\bHi\b"
assert search._buildPattern(r"\bHi") == r"\bHi\b"
assert search._buildPattern(r"Hi\b") == r"\bHi\b"
assert search._buildPattern(r"\bHi\b") == r"\bHi\b"
search.setWholeWords(False)
search.setUserRegEx(False)
# Test Settings
# =============
def pruneResult(result, index):
temp = [(i.itemHandle, r, c) for i, r, c in result][index][1]
return [(s, n, c.split()[0]) for s, n, c in temp]
# Defaults
assert pruneResult(search.iterSearch(project, "Lorem"), 2) == [
(15, 5, "Lorem"), (754, 5, "lorem"), (2056, 5, "lorem,"), (2209, 5, "lorem"),
(2425, 5, "lorem"), (2840, 5, "lorem."), (3399, 5, "lorem"),
]
# Whole Words
search.setWholeWords(True)
assert pruneResult(search.iterSearch(project, "Lor"), 2) == []
search.setWholeWords(False)
assert pruneResult(search.iterSearch(project, "Lor"), 2) == [
(15, 3, "Lorem"), (29, 3, "lor"), (754, 3, "lorem"), (2056, 3, "lorem,"),
(2209, 3, "lorem"), (2425, 3, "lorem"), (2840, 3, "lorem."), (3328, 3, "lor."),
(3399, 3, "lorem"),
]
# As RegEx
search.setWholeWords(False)
search.setUserRegEx(True)
assert pruneResult(search.iterSearch(project, r"Lor\b"), 2) == [
(29, 3, "lor"), (3328, 3, "lor."),
]
# Max Results
with monkeypatch.context() as mp:
mp.setattr(nwConst, "MAX_SEARCH_RESULT", 3)
assert pruneResult(search.iterSearch(project, "Lorem"), 2) == [
(15, 5, "Lorem"), (754, 5, "lorem"), (2056, 5, "lorem,"),
]
# Case Sensitive
search.setCaseSensitive(True)
assert pruneResult(search.iterSearch(project, "Lorem"), 2) == [(15, 5, "Lorem")]
search.setCaseSensitive(False)
# END Test testCoreTools_DocSearch
@pytest.mark.core
def testCoreTools_ProjectBuilderWrapper(monkeypatch, caplog, fncPath, mockGUI):
"""Test the wrapper function of the project builder."""
+125
View File
@@ -0,0 +1,125 @@
"""
novelWriter Main GUI Project Search Tester
============================================
This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or 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.
This program 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. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import pytest
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QAction
from novelwriter.enum import nwView
from novelwriter.gui.search import GuiProjectSearch
@pytest.mark.gui
def testGuiDocSearch_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
"""Test navigating the novel tree."""
nwGUI.openProject(prjLipsum)
nwGUI._changeView(nwView.SEARCH)
search = nwGUI.projSearch
def totalCount():
nonlocal search
res = search.searchResult
return sum(
int(res.topLevelItem(i).text(GuiProjectSearch.C_COUNT).strip("()"))
for i in range(res.topLevelItemCount())
)
# Plain search
search.searchText.setText("Lorem")
search.searchAction.activate(QAction.ActionEvent.Trigger)
assert search.searchResult.topLevelItemCount() == 14
assert totalCount() == 42
firstDoc = search.searchResult.topLevelItem(0)
firstResult = firstDoc.child(0)
assert firstDoc is not None
handle = firstDoc.data(GuiProjectSearch.C_RESULT, GuiProjectSearch.D_HANDLE)
result = firstResult.data(GuiProjectSearch.C_RESULT, GuiProjectSearch.D_RESULT)
assert result == (handle, 3, 5)
# Move down
qtbot.keyClick(search, Qt.Key.Key_Down)
assert firstDoc.isSelected() is True
# Move up
qtbot.keyClick(search, Qt.Key.Key_Up)
assert firstDoc.isSelected() is False
# Move right does nothing
qtbot.keyClick(search, Qt.Key.Key_Right)
assert firstDoc.isSelected() is False
# Selecting updates details
firstDoc.setSelected(True)
assert nwGUI.itemDetails._handle == handle
# Press return
search.searchResult.setFocus()
search.searchResult.clearSelection()
firstResult.setSelected(True)
with monkeypatch.context() as mp:
mp.setattr(search.searchResult, "hasFocus", lambda *a: True)
with qtbot.waitSignal(search.openDocumentSelectRequest, timeout=1000) as signal:
qtbot.keyClick(search, Qt.Key.Key_Return)
assert signal.args == [handle, 3, 5, False]
assert nwGUI.docEditor.docHandle == handle
assert nwGUI.docEditor.textCursor().selectedText() == "Lorem"
# Double-click
with qtbot.waitSignal(search.openDocumentSelectRequest, timeout=1000) as signal:
search._searchResultDoubleClicked(firstResult, 0)
assert signal.args == [handle, 3, 5, True]
# Case Sensitive
search.toggleCase.setChecked(True)
search.searchAction.activate(QAction.ActionEvent.Trigger)
assert search.searchResult.topLevelItemCount() == 7
assert totalCount() == 17
search.toggleCase.setChecked(False)
# Whole Words
search.searchText.setText("dolor")
with monkeypatch.context() as mp:
mp.setattr(search.searchText, "hasFocus", lambda *a: True)
qtbot.keyClick(search, Qt.Key.Key_Return)
assert search.searchResult.topLevelItemCount() == 10
assert totalCount() == 34
search.toggleWord.setChecked(True)
search.searchAction.activate(QAction.ActionEvent.Trigger)
assert search.searchResult.topLevelItemCount() == 10
assert totalCount() == 33
# RegEx
search.toggleRegEx.setChecked(True)
search.searchText.setText("(dolor|dolorem)")
search.searchAction.activate(QAction.ActionEvent.Trigger)
assert search.searchResult.topLevelItemCount() == 10
assert totalCount() == 34
# qtbot.stop()
nwGUI.closeProject()
# END Test testGuiDocSearch_Main