Skip to content

Commit 6cf18d2

Browse files
authored
add integration tests (#158)
* add integration tests * fix uninstall button not disabled + fix test setup * fix dnl libs * reuse wheel workflow * install deps * fix install * python path * fix other test * tests cleanup * fix test
1 parent 91a6e09 commit 6cf18d2

File tree

23 files changed

+937
-55
lines changed

23 files changed

+937
-55
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
name: 🧪 Integration tests
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches:
7+
- main
8+
pull_request:
9+
branches:
10+
- main
11+
12+
jobs:
13+
build-wheel:
14+
name: Build Python wheel
15+
uses: ./.github/workflows/python-wheel-package.yml
16+
17+
integration-tests:
18+
name: Integration tests
19+
runs-on: ubuntu-latest
20+
needs: build-wheel
21+
22+
services:
23+
postgres:
24+
image: postgis/postgis:17-3.5
25+
env:
26+
POSTGRES_USER: postgres
27+
POSTGRES_PASSWORD: postgres
28+
POSTGRES_DB: postgres
29+
ports:
30+
- 5432:5432
31+
options: >-
32+
--health-cmd "pg_isready -U postgres"
33+
--health-interval 10s
34+
--health-timeout 5s
35+
--health-retries 5
36+
37+
env:
38+
PGUSER: postgres
39+
PGPASSWORD: postgres
40+
PGHOST: localhost
41+
42+
steps:
43+
- uses: actions/checkout@v6
44+
45+
- name: Set up Python
46+
uses: actions/setup-python@v5
47+
with:
48+
python-version: "3.12"
49+
50+
- name: Install system dependencies for PyQt6
51+
run: |
52+
sudo apt-get update
53+
sudo apt-get install -y \
54+
libegl1 \
55+
libxkbcommon-x11-0 \
56+
libxcb-icccm4 \
57+
libxcb-image0 \
58+
libxcb-keysyms1 \
59+
libxcb-randr0 \
60+
libxcb-render-util0 \
61+
libxcb-xinerama0 \
62+
libxcb-xfixes0 \
63+
libxcb-shape0
64+
65+
- name: Download wheel artifact
66+
uses: actions/download-artifact@v6
67+
with:
68+
name: oqtopus-wheel
69+
path: dist/
70+
71+
- name: Install dependencies
72+
run: |
73+
pip install --upgrade pip
74+
pip install -r requirements.txt
75+
pip install -r requirements-standalone.txt
76+
pip install pytest
77+
# Extract bundled libs from the wheel into the local source tree
78+
# (the local oqtopus/ shadows the installed package, so libs must be present locally)
79+
unzip -o dist/*.whl "oqtopus/libs/*" -d .
80+
81+
- name: Setup test database
82+
run: bash scripts/setup_test_db.sh
83+
84+
- name: Run integration tests
85+
env:
86+
QT_QPA_PLATFORM: offscreen
87+
PYTHONPATH: oqtopus/libs
88+
run: >-
89+
pytest
90+
tests/
91+
--ignore=tests/test_plugin_load.py
92+
-v

.github/workflows/plugin-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,4 @@ jobs:
6868
-e PYTHONPATH=/usr/share/qgis/python/plugins:/usr/share/qgis/python
6969
-v $(pwd):/usr/src
7070
-w /usr/src qgis/qgis:${QGIS_TEST_VERSION}
71-
sh -c 'pip3 install -r requirements.txt || pip3 install -r requirements.txt --break-system-packages;xvfb-run pytest'
71+
sh -c 'pip3 install -r requirements.txt || pip3 install -r requirements.txt --break-system-packages;xvfb-run pytest --ignore=tests/test_module_widget.py'

oqtopus/__init__.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,40 @@
1+
import sys
2+
import types
3+
4+
# Create fake qgis.PyQt modules that point to PyQt5/6 modules
5+
# This allows oqtopus to run standalone without QGIS installed
6+
if "qgis" not in sys.modules:
7+
try:
8+
pyqt_core = __import__("PyQt6.QtCore", fromlist=[""])
9+
pyqt_gui = __import__("PyQt6.QtGui", fromlist=[""])
10+
pyqt_network = __import__("PyQt6.QtNetwork", fromlist=[""])
11+
pyqt_widgets = __import__("PyQt6.QtWidgets", fromlist=[""])
12+
pyqt_uic = __import__("PyQt6.uic", fromlist=[""])
13+
except ModuleNotFoundError:
14+
pyqt_core = __import__("PyQt5.QtCore", fromlist=[""])
15+
pyqt_gui = __import__("PyQt5.QtGui", fromlist=[""])
16+
pyqt_network = __import__("PyQt5.QtNetwork", fromlist=[""])
17+
pyqt_widgets = __import__("PyQt5.QtWidgets", fromlist=[""])
18+
pyqt_uic = __import__("PyQt5.uic", fromlist=[""])
19+
20+
qgis = types.ModuleType("qgis")
21+
pyqt = types.ModuleType("qgis.PyQt")
22+
pyqt.QtCore = pyqt_core
23+
pyqt.QtGui = pyqt_gui
24+
pyqt.QtNetwork = pyqt_network
25+
pyqt.QtWidgets = pyqt_widgets
26+
pyqt.uic = pyqt_uic
27+
28+
qgis.PyQt = pyqt
29+
sys.modules["qgis"] = qgis
30+
sys.modules["qgis.PyQt"] = pyqt
31+
sys.modules["qgis.PyQt.QtCore"] = pyqt_core
32+
sys.modules["qgis.PyQt.QtGui"] = pyqt_gui
33+
sys.modules["qgis.PyQt.QtNetwork"] = pyqt_network
34+
sys.modules["qgis.PyQt.QtWidgets"] = pyqt_widgets
35+
sys.modules["qgis.PyQt.uic"] = pyqt_uic
36+
37+
138
def classFactory(iface):
239
from .oqtopus_plugin import OqtopusPlugin
340

oqtopus/core/module_package.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class Type:
1313
BRANCH = "branch"
1414
PULL_REQUEST = "pull_request"
1515
FROM_ZIP = "from_zip"
16+
FROM_DIRECTORY = "from_directory"
1617

1718
def __init__(
1819
self,
@@ -50,6 +51,8 @@ def __init__(
5051
self.__parse_pull_request(json_payload)
5152
elif self.type == ModulePackage.Type.FROM_ZIP:
5253
return
54+
elif self.type == ModulePackage.Type.FROM_DIRECTORY:
55+
return
5356
else:
5457
raise ValueError(f"Unknown type '{type}'")
5558

oqtopus/core/package_prepare_task.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def __init__(self, parent=None):
5050

5151
self.module_package = None
5252
self.from_zip_file = None
53+
self.from_directory = None
5354

5455
self.__destination_directory = None
5556

@@ -64,13 +65,23 @@ def __init__(self, parent=None):
6465
def startFromZip(self, module_package, zip_file: str):
6566
self.module_package = module_package
6667
self.from_zip_file = zip_file
68+
self.from_directory = None
69+
70+
self.__canceled = False
71+
self.start()
72+
73+
def startFromDirectory(self, module_package, directory: str):
74+
self.module_package = module_package
75+
self.from_zip_file = None
76+
self.from_directory = directory
6777

6878
self.__canceled = False
6979
self.start()
7080

7181
def startFromModulePackage(self, module_package):
7282
self.module_package = module_package
7383
self.from_zip_file = None
84+
self.from_directory = None
7485

7586
self.__canceled = False
7687
self.start()
@@ -87,6 +98,16 @@ def run(self):
8798
if self.module_package is None:
8899
raise Exception(self.tr("No module version provided."))
89100

101+
# For directory loading, just point source_package_dir directly
102+
if self.from_directory:
103+
if not os.path.isdir(self.from_directory):
104+
raise Exception(
105+
self.tr(f"The directory '{self.from_directory}' does not exist.")
106+
)
107+
self.module_package.source_package_dir = self.from_directory
108+
self.lastError = None
109+
return
110+
90111
self.__destination_directory = self.__prepare_destination_directory()
91112
logger.info(f"Destination directory: {self.__destination_directory}")
92113

oqtopus/gui/module_selection_widget.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,13 @@ def __init__(self, modules_config_path, parent=None):
5252
self.module_seeChangeLog_pushButton.setEnabled(False)
5353

5454
self.module_zipPackage_groupBox.setVisible(False)
55+
self.module_directoryPackage_groupBox.setVisible(False)
5556

5657
self.module_module_comboBox.currentIndexChanged.connect(self.__moduleChanged)
5758
self.module_package_comboBox.currentIndexChanged.connect(self.__moduleVersionChanged)
5859
self.module_seeChangeLog_pushButton.clicked.connect(self.__seeChangeLogClicked)
5960
self.module_browseZip_toolButton.clicked.connect(self.__moduleBrowseZipClicked)
61+
self.module_browseDirectory_toolButton.clicked.connect(self.__moduleBrowseDirectoryClicked)
6062

6163
self.__packagePrepareTask = PackagePrepareTask(self)
6264
self.__packagePrepareTask.finished.connect(self.__packagePrepareTaskFinished)
@@ -213,9 +215,17 @@ def __moduleVersionChanged(self, index):
213215

214216
if self.__current_module_package.type == self.__current_module_package.Type.FROM_ZIP:
215217
self.module_zipPackage_groupBox.setVisible(True)
218+
self.module_directoryPackage_groupBox.setVisible(False)
219+
return
220+
elif (
221+
self.__current_module_package.type == self.__current_module_package.Type.FROM_DIRECTORY
222+
):
223+
self.module_zipPackage_groupBox.setVisible(False)
224+
self.module_directoryPackage_groupBox.setVisible(True)
216225
return
217226
else:
218227
self.module_zipPackage_groupBox.setVisible(False)
228+
self.module_directoryPackage_groupBox.setVisible(False)
219229

220230
loading_text = self.tr("Loading package...")
221231
self.module_information_label.setText(loading_text)
@@ -278,6 +288,35 @@ def __loadModuleFromZip(self, filename):
278288
self.signal_loadingStarted.emit()
279289
self.module_progressBar.setMaximum(100)
280290
self.module_progressBar.setValue(0)
291+
292+
def __moduleBrowseDirectoryClicked(self):
293+
directory = QFileDialog.getExistingDirectory(self, self.tr("Open from directory"), None)
294+
295+
if directory == "":
296+
return
297+
298+
self.module_fromDirectory_lineEdit.setText(directory)
299+
300+
try:
301+
with OverrideCursor(Qt.CursorShape.WaitCursor):
302+
self.__loadModuleFromDirectory(directory)
303+
except Exception as exception:
304+
CriticalMessageBox(
305+
self.tr("Error"), self.tr("Can't load module from directory:"), exception, self
306+
).exec()
307+
return
308+
309+
def __loadModuleFromDirectory(self, directory):
310+
311+
if self.__packagePrepareTask.isRunning():
312+
self.__packagePrepareTask.cancel()
313+
self.__packagePrepareTask.wait()
314+
315+
self.__packagePrepareTask.startFromDirectory(self.__current_module_package, directory)
316+
317+
self.signal_loadingStarted.emit()
318+
self.module_progressBar.setMaximum(0)
319+
self.module_progressBar.setValue(0)
281320
self.module_progressBar.setVisible(True)
282321

283322
def __packagePrepareTaskFinished(self):
@@ -361,11 +400,14 @@ def __seeChangeLogClicked(self):
361400
)
362401
return
363402

364-
if self.__current_module_package.type == ModulePackage.Type.FROM_ZIP:
403+
if self.__current_module_package.type in (
404+
ModulePackage.Type.FROM_ZIP,
405+
ModulePackage.Type.FROM_DIRECTORY,
406+
):
365407
QMessageBox.warning(
366408
self,
367409
self.tr("Can't open changelog"),
368-
self.tr("Changelog is not available for Zip packages."),
410+
self.tr("Changelog is not available for local packages."),
369411
)
370412
return
371413

@@ -438,6 +480,17 @@ def __loadVersionsFinished(self, error):
438480
name="from_zip",
439481
),
440482
)
483+
self.module_package_comboBox.addItem(
484+
self.tr("Load from directory"),
485+
ModulePackage(
486+
module=self.__current_module,
487+
organisation=self.__current_module.organisation,
488+
repository=self.__current_module.repository,
489+
json_payload=None,
490+
type=ModulePackage.Type.FROM_DIRECTORY,
491+
name="from_directory",
492+
),
493+
)
441494

442495
self.module_package_comboBox.insertSeparator(self.module_package_comboBox.count())
443496

@@ -462,8 +515,9 @@ def __loadDevelopmentVersionsFinished(self, error):
462515
QApplication.restoreOverrideCursor()
463516
self.module_progressBar.setVisible(False)
464517

465-
# Hide zip widget when loading development versions
518+
# Hide zip/directory widgets when loading development versions
466519
self.module_zipPackage_groupBox.setVisible(False)
520+
self.module_directoryPackage_groupBox.setVisible(False)
467521

468522
# Clear current module package - user needs to select a specific version
469523
self.__current_module_package = None

oqtopus/gui/module_widget.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,6 @@ def __updateModuleInfo(self):
897897
sm = SchemaMigrations(self.__pum_config)
898898

899899
self.moduleInfo_stackedWidget.setEnabled(True)
900-
self.__configure_uninstall_button()
901900

902901
# Wrap read-only queries in transaction to prevent idle connections
903902
with self.__database_connection.transaction():
@@ -954,6 +953,9 @@ def __updateModuleInfo(self):
954953
# Module not installed - show install page
955954
self.__show_install_page(target_version)
956955

956+
# Configure uninstall button after determining which page to show
957+
self.__configure_uninstall_button()
958+
957959
def __startOperation(self, operation: str, parameters: dict, options: dict):
958960
"""Start a background module operation."""
959961
# Disable UI during operation

oqtopus/oqtopus.py

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,16 @@
11
import sys
2-
import types
32
from pathlib import Path
43

5-
# Create fake qgis.PyQt modules that point to PyQt5 modules
6-
try:
7-
pyqt_core = __import__("PyQt6.QtCore", fromlist=[""])
8-
pyqt_gui = __import__("PyQt6.QtGui", fromlist=[""])
9-
pyqt_network = __import__("PyQt6.QtNetwork", fromlist=[""])
10-
pyqt_widgets = __import__("PyQt6.QtWidgets", fromlist=[""])
11-
pyqt_uic = __import__("PyQt6.uic", fromlist=[""])
12-
except ModuleNotFoundError:
13-
pyqt_core = __import__("PyQt5.QtCore", fromlist=[""])
14-
pyqt_gui = __import__("PyQt5.QtGui", fromlist=[""])
15-
pyqt_network = __import__("PyQt5.QtNetwork", fromlist=[""])
16-
pyqt_widgets = __import__("PyQt5.QtWidgets", fromlist=[""])
17-
pyqt_uic = __import__("PyQt5.uic", fromlist=[""])
18-
19-
# Create the qgis, qgis.PyQt, and submodules in sys.modules
20-
qgis = types.ModuleType("qgis")
21-
pyqt = types.ModuleType("qgis.PyQt")
22-
pyqt.QtCore = pyqt_core
23-
pyqt.QtGui = pyqt_gui
24-
pyqt.QtNetwork = pyqt_network
25-
pyqt.QtWidgets = pyqt_widgets
26-
pyqt.uic = pyqt_uic
27-
28-
qgis.PyQt = pyqt
29-
sys.modules["qgis"] = qgis
30-
sys.modules["qgis.PyQt"] = pyqt
31-
sys.modules["qgis.PyQt.QtCore"] = pyqt_core
32-
sys.modules["qgis.PyQt.QtGui"] = pyqt_gui
33-
sys.modules["qgis.PyQt.QtNetwork"] = pyqt_network
34-
sys.modules["qgis.PyQt.QtWidgets"] = pyqt_widgets
35-
sys.modules["qgis.PyQt.uic"] = pyqt_uic
36-
37-
from qgis.PyQt.QtGui import QIcon # noqa: E402
38-
39-
from .gui.main_dialog import MainDialog # noqa: E402
40-
from .utils.plugin_utils import PluginUtils # noqa: E402
4+
# The qgis.PyQt shim is set up in oqtopus/__init__.py
5+
from qgis.PyQt.QtGui import QIcon
6+
from qgis.PyQt.QtWidgets import QApplication
7+
8+
from .gui.main_dialog import MainDialog
9+
from .utils.plugin_utils import PluginUtils
4110

4211

4312
def main():
44-
app = pyqt_widgets.QApplication(sys.argv)
13+
app = QApplication(sys.argv)
4514
icon = QIcon("oqtopus/icons/oqtopus-logo.png")
4615
app.setWindowIcon(icon)
4716

0 commit comments

Comments
 (0)