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 time import time
from PyQt5.QtCore import QSize, Qt, pyqtSignal, pyqtSlot 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 ( from PyQt5.QtWidgets import (
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QToolBar, QTreeWidget, QHBoxLayout, QHeaderView, QLabel, QLineEdit, QToolBar, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget, qApp QTreeWidgetItem, QVBoxLayout, QWidget, qApp
@@ -110,7 +110,6 @@ class GuiProjectSearch(QWidget):
self.searchResult.setColumnCount(2) self.searchResult.setColumnCount(2)
self.searchResult.setIconSize(QSize(iPx, iPx)) self.searchResult.setIconSize(QSize(iPx, iPx))
self.searchResult.setIndentation(iPx) self.searchResult.setIndentation(iPx)
self.searchResult.itemPressed.connect(self._searchResultPressed)
self.searchResult.itemDoubleClicked.connect(self._searchResultDoubleClicked) self.searchResult.itemDoubleClicked.connect(self._searchResultDoubleClicked)
self.searchResult.itemSelectionChanged.connect(self._searchResultSelected) self.searchResult.itemSelectionChanged.connect(self._searchResultSelected)
@@ -160,8 +159,15 @@ class GuiProjectSearch(QWidget):
"""Process a return keypress forwarded from the main GUI.""" """Process a return keypress forwarded from the main GUI."""
if self.searchText.hasFocus(): if self.searchText.hasFocus():
self._processSearch() self._processSearch()
elif items := self.searchResult.selectedItems(): elif (
self._searchResultPressed(items[0], 0) 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 return
def beginSearch(self) -> None: def beginSearch(self) -> None:
@@ -211,6 +217,7 @@ class GuiProjectSearch(QWidget):
def _processSearch(self) -> None: def _processSearch(self) -> None:
"""Perform a search.""" """Perform a search."""
if not self._blocked: if not self._blocked:
qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
start = time() start = time()
self._blocked = True self._blocked = True
self.searchResult.clear() self.searchResult.clear()
@@ -222,6 +229,7 @@ class GuiProjectSearch(QWidget):
self._appendResultSet(item, results, capped) self._appendResultSet(item, results, capped)
logger.debug("Search took %.3f ms", 1000*(time() - start)) logger.debug("Search took %.3f ms", 1000*(time() - start))
self._time = time() self._time = time()
qApp.restoreOverrideCursor()
self._blocked = False self._blocked = False
return return
@@ -235,15 +243,6 @@ class GuiProjectSearch(QWidget):
self.selectedItemChanged.emit(str(data)) self.selectedItemChanged.emit(str(data))
return 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) @pyqtSlot("QTreeWidgetItem*", int)
def _searchResultDoubleClicked(self, item: QTreeWidgetItem, column: int) -> None: def _searchResultDoubleClicked(self, item: QTreeWidgetItem, column: int) -> None:
"""Process search result double click.""" """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 mocked import causeOSError
from novelwriter import CONFIG 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.project import NWProject
from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter, ProjectBuilder
@pytest.mark.core @pytest.mark.core
@@ -401,6 +403,97 @@ def testCoreTools_DocDuplicator(mockGUI, fncPath, tstPaths, mockRnd):
# END Test testCoreTools_DocDuplicator # 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 @pytest.mark.core
def testCoreTools_ProjectBuilderWrapper(monkeypatch, caplog, fncPath, mockGUI): def testCoreTools_ProjectBuilderWrapper(monkeypatch, caplog, fncPath, mockGUI):
"""Test the wrapper function of the project builder.""" """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