Add test coverage of shared class

This commit is contained in:
Veronica Berglyd Olsen
2023-08-23 20:19:48 +02:00
parent 8090832f3b
commit af50b219a6
5 changed files with 207 additions and 23 deletions
+1 -1
View File
@@ -1392,7 +1392,7 @@ class GuiMain(QMainWindow):
userIdle = qApp.applicationState() != Qt.ApplicationActive userIdle = qApp.applicationState() != Qt.ApplicationActive
self.mainStatus.setUserIdle(editIdle or userIdle) self.mainStatus.setUserIdle(editIdle or userIdle)
SHARED.updateIdleTime(currTime, editIdle or userIdle) SHARED.updateIdleTime(currTime, editIdle or userIdle)
self.mainStatus.updateTime(idleTime=SHARED.idleTime) self.mainStatus.updateTime(idleTime=SHARED.projectIdleTime)
return return
@pyqtSlot() @pyqtSlot()
+13 -16
View File
@@ -43,7 +43,7 @@ logger = logging.getLogger(__name__)
class SharedData(QObject): class SharedData(QObject):
__slots__ = ( __slots__ = (
"_gui", "_theme", "_project", "_leckedBy", "_alert", "_gui", "_theme", "_project", "_lockedBy", "_alert",
"_idleTime", "_idleRefTime", "_idleTime", "_idleRefTime",
) )
@@ -58,28 +58,28 @@ class SharedData(QObject):
self._lockedBy = None self._lockedBy = None
self._alert = None self._alert = None
self._idleTime = 0.0 self._idleTime = 0.0
self._idleRedTime = time() self._idleRefTime = time()
return return
@property @property
def mainGui(self) -> GuiMain: def mainGui(self) -> GuiMain:
"""Return the Main GUI instance.""" """Return the Main GUI instance."""
if self._gui is None: if self._gui is None:
raise Exception("UserData class not properly initialised") raise Exception("SharedData class not fully initialised")
return self._gui return self._gui
@property @property
def theme(self) -> GuiTheme: def theme(self) -> GuiTheme:
"""Return the GUI Theme instance.""" """Return the GUI Theme instance."""
if self._theme is None: if self._theme is None:
raise Exception("UserData class not properly initialised") raise Exception("SharedData class not fully initialised")
return self._theme return self._theme
@property @property
def project(self) -> NWProject: def project(self) -> NWProject:
"""Return the active NWProject instance.""" """Return the active NWProject instance."""
if self._project is None: if self._project is None:
raise Exception("UserData class not properly initialised") raise Exception("SharedData class not fully initialised")
return self._project return self._project
@property @property
@@ -92,16 +92,16 @@ class SharedData(QObject):
"""Return cached lock information for the last project.""" """Return cached lock information for the last project."""
return self._lockedBy return self._lockedBy
@property
def projectIdleTime(self) -> float:
"""Return the session idle time."""
return self._idleTime
@property @property
def alert(self) -> _GuiAlert | None: def alert(self) -> _GuiAlert | None:
"""Return a pointer to the last alert box.""" """Return a pointer to the last alert box."""
return self._alert return self._alert
@property
def idleTime(self) -> float:
"""Return the session idle time."""
return self._idleTime
## ##
# Methods # Methods
## ##
@@ -154,12 +154,9 @@ class SharedData(QObject):
the last time this function was called. Otherwise, only the the last time this function was called. Otherwise, only the
reference time is updated. reference time is updated.
""" """
if hasattr(self, "_idleRefTime"): if userIdle:
# This method is called by a timer from C++, and the self._idleTime += currTime - self._idleRefTime
# instance may not have be initialised self._idleRefTime = currTime
if userIdle:
self._idleTime += currTime - self._idleRefTime
self._idleRefTime = currTime
return return
## ##
+3
View File
@@ -22,6 +22,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import sys import sys
import pytest import pytest
import shutil import shutil
import logging
from pathlib import Path from pathlib import Path
@@ -62,6 +63,7 @@ def resetConfigVars():
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=True)
def sessionFixture(): def sessionFixture():
"""A session wide fixture to set up the test environment.""" """A session wide fixture to set up the test environment."""
logging.root.setLevel(logging.INFO)
if _TMP_ROOT.exists(): if _TMP_ROOT.exists():
shutil.rmtree(_TMP_ROOT) shutil.rmtree(_TMP_ROOT)
_TMP_ROOT.mkdir() _TMP_ROOT.mkdir()
@@ -81,6 +83,7 @@ def functionFixture(qtbot):
CONFIG.__init__() CONFIG.__init__()
CONFIG.initConfig(confPath=_TMP_CONF, dataPath=_TMP_CONF) CONFIG.initConfig(confPath=_TMP_CONF, dataPath=_TMP_CONF)
resetConfigVars() resetConfigVars()
logging.getLogger("novelwriter").setLevel(logging.INFO)
return return
+3 -6
View File
@@ -30,8 +30,7 @@ from novelwriter import CONFIG, main, logger
@pytest.mark.base @pytest.mark.base
def testBaseInit_Launch(caplog, monkeypatch, fncPath): def testBaseInit_Launch(caplog, monkeypatch, fncPath):
"""Check launching the main GUI. """Check launching the main GUI."""
"""
monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain)
# TestMode Launch # TestMode Launch
@@ -80,8 +79,7 @@ def testBaseInit_Launch(caplog, monkeypatch, fncPath):
@pytest.mark.base @pytest.mark.base
def testBaseInit_Options(monkeypatch, fncPath): def testBaseInit_Options(monkeypatch, fncPath):
"""Test command line options for logging level. """Test command line options for logging level."""
"""
monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain)
monkeypatch.setattr(sys, "argv", [ monkeypatch.setattr(sys, "argv", [
"novelWriter.py", "--testmode", f"--config={fncPath}", f"--data={fncPath}" "novelWriter.py", "--testmode", f"--config={fncPath}", f"--data={fncPath}"
@@ -146,8 +144,7 @@ def testBaseInit_Options(monkeypatch, fncPath):
@pytest.mark.base @pytest.mark.base
def testBaseInit_Imports(caplog, monkeypatch, fncPath): def testBaseInit_Imports(caplog, monkeypatch, fncPath):
"""Check import error handling. """Check import error handling."""
"""
monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *a: None) monkeypatch.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *a: None)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec_", lambda *a: 0) monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec_", lambda *a: 0)
+187
View File
@@ -0,0 +1,187 @@
"""
novelWriter SharedData Class Tester
=====================================
This file is a part of novelWriter
Copyright 20182023, 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/>.
"""
import pytest
from mocked import MockGuiMain, MockTheme
from PyQt5.QtWidgets import QMessageBox
from novelwriter.core.project import NWProject
from novelwriter.shared import SharedData, _GuiAlert
from tests.tools import buildTestProject
@pytest.mark.base
def testBaseSharedData_Init():
"""Test SharedData class initialisation."""
shared = SharedData()
# When not initialised, it should raise exceptions
with pytest.raises(Exception):
shared.mainGui
with pytest.raises(Exception):
shared.theme
with pytest.raises(Exception):
shared.project
# Create some mock objects
mockGui = MockGuiMain()
mockTheme = MockTheme()
assert mockGui is not mockTheme
# Properly initialise the class
shared.initSharedData(mockGui, mockTheme) # type: ignore
assert shared.mainGui is mockGui
assert shared.theme is mockTheme
assert isinstance(shared.project, NWProject)
assert shared.hasProject is False
assert shared.projectIdleTime == 0.0
assert shared.projectLock is None
assert shared.alert is None
# END Test testBaseSharedData_Init
@pytest.mark.base
def testBaseSharedData_Projects(fncPath, caplog: pytest.LogCaptureFixture):
"""Test SharedData handling of projects."""
project = NWProject()
buildTestProject(project, fncPath)
project.closeProject(0.0) # Clears the lockfile
shared = SharedData()
assert shared._project is None
# Initialise the instance, should create an empty project
mockGui = MockGuiMain()
mockTheme = MockTheme()
shared.initSharedData(mockGui, mockTheme) # type: ignore
assert isinstance(shared.project, NWProject)
assert shared.hasProject is False
# Load the test project
assert shared.openProject(fncPath) is True
assert shared.hasProject is True
# We cannot open two projects
caplog.clear()
assert shared.openProject(fncPath) is False
assert caplog.messages[-1] == "A project is already open"
assert shared._idleTime == 0.0
# Update idle time
refTime = shared._idleRefTime
shared.updateIdleTime(refTime + 1.0, False)
shared.updateIdleTime(refTime + 2.0, True)
shared.updateIdleTime(refTime + 3.0, False)
shared.updateIdleTime(refTime + 4.0, True)
assert round(shared.projectIdleTime) == 2
# Save project
assert shared.saveProject() is True
# Close project
shared.closeProject()
assert shared.hasProject is False
# Cannot save a project after it's been closed
assert shared.saveProject() is False
# Check locked project info
project.openProject(fncPath) # First open with our independent project instance
assert shared.hasProject is False
assert shared.projectLock is None
assert shared.openProject(fncPath) is False # Then with out shared instance
assert shared.hasProject is False
assert isinstance(shared.projectLock, list)
# END Test testBaseSharedData_Projects
@pytest.mark.base
def testBaseSharedData_Alerts(monkeypatch, caplog: pytest.LogCaptureFixture):
"""Test SharedData class alert helper functions."""
monkeypatch.setattr(QMessageBox, "exec_", lambda *a: None)
monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.Yes)
shared = SharedData()
mockGui = MockGuiMain()
mockTheme = MockTheme()
shared.initSharedData(mockGui, mockTheme) # type: ignore
assert shared.alert is None
# Info box
caplog.clear()
shared.info("Hello World", info="foo", details="bar")
assert isinstance(shared.alert, _GuiAlert)
assert shared.alert.text() == "Hello World"
assert shared.alert.informativeText() == "foo"
assert shared.alert.detailedText() == "bar"
assert caplog.text.strip().startswith("INFO")
assert caplog.text.strip().endswith("Hello World foo bar")
shared._alert = None
# Warning box
caplog.clear()
shared.warn("Oops!", info="foo", details="bar")
assert isinstance(shared.alert, _GuiAlert)
assert shared.alert.text() == "Oops!"
assert shared.alert.informativeText() == "foo"
assert shared.alert.detailedText() == "bar"
assert caplog.text.strip().startswith("WARNING")
assert caplog.text.strip().endswith("Oops! foo bar")
shared._alert = None
# Error box
caplog.clear()
shared.error("Oh noes!", info="foo", details="bar")
assert isinstance(shared.alert, _GuiAlert)
assert shared.alert.text() == "Oh noes!"
assert shared.alert.informativeText() == "foo"
assert shared.alert.detailedText() == "bar"
assert caplog.text.strip().startswith("ERROR")
assert caplog.text.strip().endswith("Oh noes! foo bar")
shared._alert = None
# Error box with exception
caplog.clear()
shared.error("Oh noes!", info="foo", details="bar", exc=Exception("Boom!"))
assert isinstance(shared.alert, _GuiAlert)
assert shared.alert.text() == "Oh noes!"
assert shared.alert.informativeText() == "foo<br><b>Exception</b>: Boom!"
assert shared.alert.detailedText() == "bar"
assert caplog.text.strip().startswith("ERROR")
assert caplog.text.strip().endswith("Oh noes! foo bar")
shared._alert = None
# Question box
assert shared.question("Why?") is True
assert isinstance(shared.alert, _GuiAlert)
assert shared.alert.text() == "Why?"
shared._alert = None
# END Test testBaseSharedData_Alerts