Add test coverage of shared class
This commit is contained in:
@@ -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
@@ -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
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
"""
|
||||||
|
novelWriter – SharedData Class Tester
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
This file is a part of novelWriter
|
||||||
|
Copyright 2018–2023, 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
|
||||||
Reference in New Issue
Block a user