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
self.mainStatus.setUserIdle(editIdle or userIdle)
SHARED.updateIdleTime(currTime, editIdle or userIdle)
self.mainStatus.updateTime(idleTime=SHARED.idleTime)
self.mainStatus.updateTime(idleTime=SHARED.projectIdleTime)
return
@pyqtSlot()
+13 -16
View File
@@ -43,7 +43,7 @@ logger = logging.getLogger(__name__)
class SharedData(QObject):
__slots__ = (
"_gui", "_theme", "_project", "_leckedBy", "_alert",
"_gui", "_theme", "_project", "_lockedBy", "_alert",
"_idleTime", "_idleRefTime",
)
@@ -58,28 +58,28 @@ class SharedData(QObject):
self._lockedBy = None
self._alert = None
self._idleTime = 0.0
self._idleRedTime = time()
self._idleRefTime = time()
return
@property
def mainGui(self) -> GuiMain:
"""Return the Main GUI instance."""
if self._gui is None:
raise Exception("UserData class not properly initialised")
raise Exception("SharedData class not fully initialised")
return self._gui
@property
def theme(self) -> GuiTheme:
"""Return the GUI Theme instance."""
if self._theme is None:
raise Exception("UserData class not properly initialised")
raise Exception("SharedData class not fully initialised")
return self._theme
@property
def project(self) -> NWProject:
"""Return the active NWProject instance."""
if self._project is None:
raise Exception("UserData class not properly initialised")
raise Exception("SharedData class not fully initialised")
return self._project
@property
@@ -92,16 +92,16 @@ class SharedData(QObject):
"""Return cached lock information for the last project."""
return self._lockedBy
@property
def projectIdleTime(self) -> float:
"""Return the session idle time."""
return self._idleTime
@property
def alert(self) -> _GuiAlert | None:
"""Return a pointer to the last alert box."""
return self._alert
@property
def idleTime(self) -> float:
"""Return the session idle time."""
return self._idleTime
##
# Methods
##
@@ -154,12 +154,9 @@ class SharedData(QObject):
the last time this function was called. Otherwise, only the
reference time is updated.
"""
if hasattr(self, "_idleRefTime"):
# This method is called by a timer from C++, and the
# instance may not have be initialised
if userIdle:
self._idleTime += currTime - self._idleRefTime
self._idleRefTime = currTime
if userIdle:
self._idleTime += currTime - self._idleRefTime
self._idleRefTime = currTime
return
##
+3
View File
@@ -22,6 +22,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import sys
import pytest
import shutil
import logging
from pathlib import Path
@@ -62,6 +63,7 @@ def resetConfigVars():
@pytest.fixture(scope="session", autouse=True)
def sessionFixture():
"""A session wide fixture to set up the test environment."""
logging.root.setLevel(logging.INFO)
if _TMP_ROOT.exists():
shutil.rmtree(_TMP_ROOT)
_TMP_ROOT.mkdir()
@@ -81,6 +83,7 @@ def functionFixture(qtbot):
CONFIG.__init__()
CONFIG.initConfig(confPath=_TMP_CONF, dataPath=_TMP_CONF)
resetConfigVars()
logging.getLogger("novelwriter").setLevel(logging.INFO)
return
+3 -6
View File
@@ -30,8 +30,7 @@ from novelwriter import CONFIG, main, logger
@pytest.mark.base
def testBaseInit_Launch(caplog, monkeypatch, fncPath):
"""Check launching the main GUI.
"""
"""Check launching the main GUI."""
monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain)
# TestMode Launch
@@ -80,8 +79,7 @@ def testBaseInit_Launch(caplog, monkeypatch, fncPath):
@pytest.mark.base
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(sys, "argv", [
"novelWriter.py", "--testmode", f"--config={fncPath}", f"--data={fncPath}"
@@ -146,8 +144,7 @@ def testBaseInit_Options(monkeypatch, fncPath):
@pytest.mark.base
def testBaseInit_Imports(caplog, monkeypatch, fncPath):
"""Check import error handling.
"""
"""Check import error handling."""
monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *a: None)
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