Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .claude/commands/write-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,27 @@ Search `test/` for an existing test file for `$ARGUMENTS`:
If a test file **exists**: extend it, do not replace it.
If no test file exists: create `test/test_<snake_case_name>.py`.

Also check `docs/test-strategy.md` §Module-to-Test Mapping to understand the current test count and documented gaps for this module.

## Step 3 — Consult project rules (mandatory)

Read **all** of these files before writing any code:

1. `ai/core/testing-rules.md` — tier definitions, coverage targets, structure
2. `ai/core/qgis-api-rules.md` — QGIS API compatibility rules
3. `ai/core/constraints.md` — language and naming rules
1. `docs/test-strategy.md` — **authoritative reference**: tier definitions, coverage targets, module mapping, gap backlog, edge case catalog, fixture scope rules
2. `ai/core/testing-rules.md` — tactical rules: geometry checks, structure, framework
3. `ai/core/qgis-api-rules.md` — QGIS API compatibility rules
4. `ai/core/constraints.md` — language and naming rules

Also read:
- `test/utilities.py` — QGIS app initialisation helper
- `test/conftest.py` — shared fixtures
- `test/layer_factories.py` — shared layer/geometry factory helpers (import AFTER `get_qgis_app()`)

For an example of a well-structured test file, read `test/test_siedgr_integration.py`.

## Step 4 — Write the test file

### Tier decision
### Tier decision (from `docs/test-strategy.md` §Test Taxonomy)

```
Does the function under test call processing.run()?
Expand Down
53 changes: 49 additions & 4 deletions .claude/pre_commit_checks.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Qualitätsprüfung: flake8, pylint, bandit, detect-secrets, pytest
# Qualitätsprüfung: qgis_plugin_validate, flake8, pylint, bandit, detect-secrets, pytest
# Gibt {"continue":false,...} aus und beendet mit Exit-Code 1, wenn Fehler gefunden werden.

$env:PYTHONIOENCODING = 'utf-8'
Expand All @@ -8,13 +8,54 @@ $allFiles = git diff --cached --name-only --diff-filter=ACM

$ok = $true

# Plain "python"/"pylint" on PATH resolve to an interpreter without 'qgis'
# on its path and (observed in practice) an older pylint that doesn't even
# know newer message IDs (e.g. possibly-used-before-assignment) - that
# produces both false import-error findings for every qgis.* import and a
# spurious "unknown-option-value" warning for the disable comments guarding
# them, blocking commits for no real reason. Use the QGIS-bundled Python
# instead wherever a check needs to resolve 'qgis' correctly, with the same
# env vars verified to work for a full local pytest run. Candidate install
# locations mirror setup_qgis_path.py.
$qgisCandidates = @(
"C:\Program Files\QGIS 3.40.0",
"C:\Program Files\QGIS 3.38.3",
"C:\Program Files\QGIS 3.36.3",
"C:\OSGeo4W64"
)
$qgisBase = $qgisCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1
$qgisPython = $null
if ($qgisBase) {
$candidatePython = Join-Path $qgisBase "apps\Python312\python.exe"
if (Test-Path $candidatePython) { $qgisPython = $candidatePython }
}
if ($qgisPython) {
$env:PYTHONPATH = "$qgisBase\apps\qgis\python;$qgisBase\apps\qgis\python\plugins;" + (Get-Location).Path + "\.."
$env:QGIS_PREFIX_PATH = "$qgisBase\apps\qgis"
$env:QT_QPA_PLATFORM = "offscreen"
$env:PATH = "$qgisBase\bin;$qgisBase\apps\qgis\bin;$env:PATH"
} else {
Write-Host "WARNUNG: keine QGIS-Installation gefunden (siehe setup_qgis_path.py) - pylint/pytest fallen auf 'python' zurueck, QGIS-Importe werden vermutlich fehlschlagen."
}

Write-Host "--- qgis_plugin_validate ---"
# Pure stdlib (argparse/re/zipfile/pathlib) - no QGIS import needed, plain
# python is fine. Same check as qgis-plugin-ci.yml's "structure + metadata
# validator" step.
python ci/qgis_plugin_validate.py --auto
if ($LASTEXITCODE -ne 0) { $ok = $false }

if ($pyFiles) {
Write-Host "--- flake8 ---"
flake8 $pyFiles
if ($LASTEXITCODE -ne 0) { $ok = $false }

Write-Host "--- pylint ---"
pylint $pyFiles
if ($qgisPython) {
& $qgisPython -m pylint --rcfile=pylintrc $pyFiles
} else {
pylint $pyFiles
}
if ($LASTEXITCODE -ne 0) { $ok = $false }

Write-Host "--- bandit ---"
Expand All @@ -38,11 +79,15 @@ if ($allFiles) {
}

Write-Host "--- pytest ---"
python -m pytest test/ --tb=short -q
if ($qgisPython) {
& $qgisPython -m pytest test/ --tb=short -q
} else {
python -m pytest test/ --tb=short -q
}
if ($LASTEXITCODE -ne 0) { $ok = $false }

if (-not $ok) {
$msg = '{"continue":false,"stopReason":"Commit blockiert: flake8/pylint/bandit/detect-secrets/pytest haben Fehler gemeldet"}'
$msg = '{"continue":false,"stopReason":"Commit blockiert: qgis_plugin_validate/flake8/pylint/bandit/detect-secrets/pytest haben Fehler gemeldet"}'
Write-Output $msg
exit 1
}
23 changes: 22 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,35 @@ jobs:
run: |
docker build --pull -t qgis-plugin-test .

- name: Run tests
- name: Run tests with coverage
id: run_tests
continue-on-error: true
run: |
# Run container; coverage.xml is written to /plugins/ibtoolpartion/
# which maps to $(pwd) via the volume mount, so it appears on the
# host automatically.
docker run --rm \
-v $(pwd):/plugins/ibtoolpartion \
qgis-plugin-test

- name: Verify and fix coverage report
run: |
if [ ! -f coverage.xml ]; then
echo "ERROR: coverage.xml not found after test run"
exit 1
fi
# Fix absolute container paths → relative repo paths for Codecov
sed -i 's|/plugins/ibtoolpartion/||g' coverage.xml
echo "coverage.xml found and paths fixed"
head -5 coverage.xml

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
fail_ci_if_error: true

- name: Fail job if tests failed
if: steps.run_tests.outcome == 'failure'
run: |
Expand Down
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends \
xvfb \
python3-pytest \
python3-pytest-cov \
python3-coverage \
python3-pip \
&& rm -rf /var/lib/apt/lists/* \
&& pip3 install --no-cache-dir --break-system-packages pytest-timeout
Expand Down Expand Up @@ -44,5 +46,5 @@ WORKDIR /plugins/ibtoolpartion
# CI-Logs an dieser Stelle NULL Ausgabe, selbst nach PYTHONUNBUFFERED=1.
# Damit lässt sich beim nächsten Lauf sehen, ob schon xvfb-run/Xvfb
# hängt oder erst pytest (Collection oder ein einzelner Test).
CMD ["sh", "-c", "echo '[ci] launching xvfb-run'; xvfb-run -a sh -c 'echo \"[ci] Xvfb ready - starting pytest\"; python3 -m pytest --tb=short --timeout=300'"]
CMD ["sh", "-c", "echo '[ci] launching xvfb-run'; xvfb-run -a sh -c 'echo \"[ci] Xvfb ready - starting pytest\"; python3 -m pytest --tb=short --timeout=300 --cov --cov-report=xml --cov-report=html'"]

7 changes: 4 additions & 3 deletions IbToolPartion.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ def select_output_file(self):

filename, _filter = QFileDialog.getSaveFileName(
self.dlg,
"Select output file",
self.tr("Select output file"),
"",
'Shapefiles (*.shp);;GeoPackage (*.gpkg);;All Files (*)'
self.tr('Shapefiles (*.shp);;GeoPackage (*.gpkg);;All Files (*)')
)

if filename:
Expand All @@ -195,7 +195,8 @@ def select_output_file(self):
def select_input_file(self):
"""Open an open-file dialog and write the chosen path to the dialog."""
filename, _filter = QFileDialog.getOpenFileName(
self.dlg, "Select input file ", "", '*.shp, *.gpkg')
self.dlg, self.tr("Select input file"),
"", self.tr('Vector files (*.shp *.gpkg)'))
self.dlg.Input_HU.setText(filename)

def siedgr(self, input_hu, cell_size, filename): # pylint: disable=too-many-locals
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
#Add iso code for any locales you want to support here (space separated)
# default is no locales
# LOCALES = af
LOCALES =
# NOTE: scripts/compile-strings.sh and update-strings.sh build the path as
# i18n/$(LOCALE).ts, so this must be the .ts file's basename, not just the
# ISO code (the plugin's translator loads i18n/IbToolPartition_<locale>.qm).
LOCALES = IbToolPartition_de

# If locales are enabled, set the name of the lrelease binary on your system. If
# you have trouble compiling the translations, you may have to specify the full path to
Expand Down
11 changes: 9 additions & 2 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,15 @@ pytest test/ -v -m unit
| `@pytest.mark.unit` | No `processing.run()` calls — fast, no QGIS needed |
| `@pytest.mark.integration` | Calls `processing.run()` — requires QGIS |
| `@pytest.mark.edge_case` | Boundary / degenerate inputs |
| `@pytest.mark.slow` | Runtime > 1 s or large synthetic datasets |
| `@pytest.mark.performance` | Measures runtime/scaling behaviour |
| `@pytest.mark.slow` | Long-running test, excluded via `-m "not slow"` |

See `ai/core/testing-rules.md` for full testing conventions.
Coverage is configured in `.coveragerc` (`source = ibtoolpartion, scripts`;
`Partitioning.pyt`, `resources.py`, and `test/` are omitted).

See [`docs/test-strategy.md`](test-strategy.md) for the authoritative test
strategy (tier definitions, coverage targets, module-to-test mapping, gap
backlog) and `ai/core/testing-rules.md` for the tactical rules.

---

Expand All @@ -153,6 +159,7 @@ The `ai/` directory contains rules and task templates for AI-assisted developmen
| File | Content |
|------|---------|
| [`docs/CHANGELOG.md`](CHANGELOG.md) | Version history |
| [`docs/test-strategy.md`](test-strategy.md) | Authoritative test strategy: tiers, coverage targets, module mapping, gap backlog |
| [`ai/core/testing-rules.md`](../ai/core/testing-rules.md) | Test conventions |
| [`ai/core/constraints.md`](../ai/core/constraints.md) | Language and code constraints |
| [`ci/qgis_plugin_validate.py`](../ci/qgis_plugin_validate.py) | Plugin structure validator |
Binary file modified i18n/IbToolPartition_de.qm
Binary file not shown.
16 changes: 16 additions & 0 deletions i18n/IbToolPartition_de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@
<source>Output file written at {}</source>
<translation>Ausgabedatei gespeichert unter {}</translation>
</message>
<message>
<source>Select output file</source>
<translation>Ausgabedatei auswählen</translation>
</message>
<message>
<source>Select input file</source>
<translation>Eingabedatei auswählen</translation>
</message>
<message>
<source>Shapefiles (*.shp);;GeoPackage (*.gpkg);;All Files (*)</source>
<translation>Shapefiles (*.shp);;GeoPackage (*.gpkg);;Alle Dateien (*)</translation>
</message>
<message>
<source>Vector files (*.shp *.gpkg)</source>
<translation>Vektordateien (*.shp *.gpkg)</translation>
</message>
</context>
<context>
<name>IbToolPartitionDialogBase</name>
Expand Down
2 changes: 1 addition & 1 deletion pb_tool.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ extra_dirs:

# ISO code(s) for any locales (translations), separated by spaces.
# Corresponding .ts files must exist in the i18n directory
locales:
locales: de

[help]
# the built help directory that should be deployed with the plugin
Expand Down
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ markers =
unit: fast unit test - no real QGIS required
integration: requires a full QGIS Processing environment (Docker)
edge_case: boundary or degenerate input scenario
performance: measures runtime/scaling behaviour, may be slow
slow: long-running test, excluded from default fast-feedback runs
filterwarnings =
ignore::DeprecationWarning
ignore::PendingDeprecationWarning
1 change: 1 addition & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pytest>=7.0.0
pytest-mock>=3.10.0
pytest-timeout>=2.1.0
pytest-cov>=4.0
5 changes: 5 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,10 @@ exclude =
__pycache__,
.git,
.idea,
.venv,
venv,
.pytest_cache,
dist,
build,
resources.py,
help/,
47 changes: 17 additions & 30 deletions test/conftest.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
# -*- coding: utf-8 -*-
"""
Pytest configuration and fixtures for IbToolPartition plugin tests.

CRITICAL: no QGIS imports in this file. conftest.py is loaded as a pytest
plugin before test collection; importing qgis.core here triggers QGIS' own
import hook (qgis.utils._import) and causes a circular-import error. QGIS
imports belong in the test modules themselves, after get_qgis_app().
"""
import sys
import tempfile
import shutil
from pathlib import Path
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock

import pytest

# The plugin folder name ('ibtoolpartion') is already a valid Python
# identifier, so - unlike IB-Tool-3 - no types.ModuleType alias stub is
# needed here. Adding the parent directory to sys.path makes
# 'import ibtoolpartion.X' resolve locally exactly as it does in the
# container, where PYTHONPATH=/plugins and the plugin lives at
# /plugins/ibtoolpartion.
_PLUGIN_PARENT = str(Path(__file__).resolve().parent.parent.parent)
if _PLUGIN_PARENT not in sys.path:
sys.path.insert(0, _PLUGIN_PARENT)


@pytest.fixture
def temp_dir():
Expand All @@ -20,15 +36,6 @@ def temp_dir():
shutil.rmtree(temp_dir, ignore_errors=True)


@pytest.fixture
def sample_shapefile_path():
"""
Fixture that provides path to test shapefile.
"""
test_data_dir = Path(__file__).parent.parent / "Test_data"
return test_data_dir


@pytest.fixture
def mock_qgis_interface():
"""
Expand All @@ -50,23 +57,3 @@ def plugin_dir():
Fixture that provides the plugin directory path.
"""
return Path(__file__).parent.parent


@pytest.fixture
def mock_qgis_modules():
"""
Fixture that provides mocked QGIS modules.
"""
qgis_mocks = {
'qgis': MagicMock(),
'qgis.PyQt': MagicMock(),
'qgis.PyQt.QtCore': MagicMock(),
'qgis.PyQt.QtGui': MagicMock(),
'qgis.PyQt.QtWidgets': MagicMock(),
'qgis.core': MagicMock(),
'qgis.gui': MagicMock(),
'qgis.processing': MagicMock(),
}

with patch.dict('sys.modules', qgis_mocks):
yield qgis_mocks
Loading
Loading