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
115 changes: 59 additions & 56 deletions .github/workflows/tester.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@ name: "🎳 Tester"

on:
push:
branches:
- main
paths:
- '**.py'
- .github/workflows/tester.yml
- requirements/testing.txt

pull_request:
branches:
- main
paths:
- '**.py'
- .github/workflows/tester.yml
Expand Down Expand Up @@ -45,55 +41,62 @@ jobs:
- name: Run Unit tests
run: pytest -p no:qgis tests/unit/

# test-qgis:
# runs-on: ubuntu-latest

# container:
# image: qgis/qgis:3.4
# env:
# CI: true
# DISPLAY: ":1"
# MUTE_LOGS: true
# NO_MODALS: 1
# PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:."
# QT_QPA_PLATFORM: "offscreen"
# WITH_PYTHON_PEP: false
# # be careful, things have changed since QGIS 3.40. So if you are using this setup
# # with a QGIS version older than 3.40, you may need to change the way you set up the container
# volumes:
# # Mount the X11 socket to allow GUI applications to run
# - /tmp/.X11-unix:/tmp/.X11-unix
# # Mount the workspace directory to the container
# - ${{ github.workspace }}:/home/root/

# steps:
# - name: Get source code
# uses: actions/checkout@v4

# - name: Print QGIS version
# run: qgis --version

# # Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40
# # - name: Setup plugin
# # run: qgis_setup.sh ${{ env.PROJECT_FOLDER }}

# - name: Install Python requirements
# run: |
# apt update && apt install -y python3-pip python3-venv pipx
# # Create a virtual environment
# cd /home/root/
# pipx run qgis-venv-creator --venv-name ".venv"
# # Activate the virtual environment
# . .venv/bin/activate
# # Install the requirements
# python3 -m pip install -U -r requirements/testing.txt

# - name: Run Unit tests
# run: |
# cd /home/root/
# # Activate the virtual environment
# . .venv/bin/activate
# # Run the tests
# # xvfb-run is used to run the tests in a virtual framebuffer
# # This is necessary because QGIS requires a display to run
# xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml
test-qgis:
runs-on: ubuntu-latest

container:
image: qgis/qgis:latest
env:
CI: true
DISPLAY: ":1"
MUTE_LOGS: true
NO_MODALS: 1
PYTHONPATH: "/usr/share/qgis/python/plugins:/usr/share/qgis/python:."
QT_QPA_PLATFORM: "offscreen"
WITH_PYTHON_PEP: false
# be careful, things have changed since QGIS 3.40. So if you are using this setup
# with a QGIS version older than 3.40, you may need to change the way you set up the container
volumes:
# Mount the X11 socket to allow GUI applications to run
- /tmp/.X11-unix:/tmp/.X11-unix
# Mount the workspace directory to the container
- ${{ github.workspace }}:/home/root/

steps:
- name: Get source code
uses: actions/checkout@v4

- name: Print QGIS version
run: qgis --version

# Uncomment if you need to run a script to set up the plugin in QGIS docker image < 3.40
# - name: Setup plugin
# run: qgis_setup.sh ${{ env.PROJECT_FOLDER }}

- name: Install Python requirements
run: |
apt update && apt install -y python3-pip python3-venv pipx
# Create a virtual environment
cd /home/root/
pipx run qgis-venv-creator --venv-name ".venv"
# Activate the virtual environment
. .venv/bin/activate
# Install the requirements
python3 -m pip install -U -r requirements/testing.txt
python3 -m pip install git+https://github.com/Loop3D/map2loop.git@noelle/contact_extractor

- name: verify input data
run: |
cd /home/root/
. .venv/bin/activate
ls -la tests/qgis/input/ || echo "Input directory not found"

- name: Run Unit tests
run: |
cd /home/root/
# Activate the virtual environment
. .venv/bin/activate
# Run the tests
# xvfb-run is used to run the tests in a virtual framebuffer
# This is necessary because QGIS requires a display to run
xvfb-run python3 -m pytest tests/qgis --junitxml=junit/test-results-qgis.xml --cov-report=xml:coverage-reports/coverage-qgis.xml
31 changes: 27 additions & 4 deletions m2l/processing/algorithms/extract_basal_contacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None:
)
)

self.addParameter(
QgsProcessingParameterField(
'FORMATION_FIELD',
'Formation Field',
parentLayerParameterName=self.INPUT_GEOLOGY,
type=QgsProcessingParameterField.String,
defaultValue='formation',
optional=True
)
)

self.addParameter(
QgsProcessingParameterFeatureSource(
self.INPUT_FAULTS,
Expand Down Expand Up @@ -127,7 +138,14 @@ def processAlgorithm(
geology = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context)
faults = self.parameterAsVectorLayer(parameters, self.INPUT_FAULTS, context)
strati_column = self.parameterAsMatrix(parameters, self.INPUT_STRATI_COLUMN, context)
ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context)
ignore_units = self.parameterAsMatrix(parameters, self.INPUT_IGNORE_UNITS, context)

if not strati_column or all(isinstance(unit, str) and not unit.strip() for unit in strati_column):
raise QgsProcessingException("no stratigraphic column found")

if not ignore_units or all(isinstance(unit, str) and not unit.strip() for unit in ignore_units):
feedback.pushInfo("no units to ignore specified")

# if strati_column and strati_column.strip():
# strati_column = [unit.strip() for unit in strati_column.split(',')]
# Save stratigraphic column settings
Expand All @@ -138,10 +156,15 @@ def processAlgorithm(
ignore_settings.setValue("m2l/ignore_units", ignore_units)

unit_name_field = self.parameterAsString(parameters, 'UNIT_NAME_FIELD', context)
formation_field = self.parameterAsString(parameters, 'FORMATION_FIELD', context)

geology = qgsLayerToGeoDataFrame(geology)
mask = ~geology['Formation'].astype(str).str.strip().isin(ignore_units)
geology = geology[mask].reset_index(drop=True)
if formation_field and formation_field in geology.columns:
mask = ~geology[formation_field].astype(str).str.strip().isin(ignore_units)
geology = geology[mask].reset_index(drop=True)
feedback.pushInfo(f"filtered by formation field: {formation_field}")
else:
feedback.pushInfo(f"no formation field found: {formation_field}")

faults = qgsLayerToGeoDataFrame(faults) if faults else None
if unit_name_field != 'UNITNAME' and unit_name_field in geology.columns:
Expand All @@ -154,7 +177,7 @@ def processAlgorithm(
feedback.pushInfo("Exporting Basal Contacts Layer...")
basal_contacts = GeoDataFrameToQgsLayer(
self,
contact_extractor.basal_contacts,
basal_contacts,
parameters=parameters,
context=context,
output_key=self.OUTPUT,
Expand Down
4 changes: 2 additions & 2 deletions m2l/processing/algorithms/sorter.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None:
self.addParameter(
QgsProcessingParameterFeatureSink(
self.OUTPUT,
self.tr("Stratigraphic column"),
"Stratigraphic column",
)
)

Expand Down Expand Up @@ -177,7 +177,7 @@ def build_input_frames(layer: QgsVectorLayer, feedback) -> tuple:
(units_df, relationships_df, contacts_df, map_data)
"""
import pandas as pd
from m2l.map2loop.mapdata import MapData # adjust import path if needed
from map2loop.map2loop.mapdata import MapData # adjust import path if needed

# Example: convert the geology layer to a very small units_df
units_records = []
Expand Down
1 change: 1 addition & 0 deletions tests/qgis/input/faults_clip.cpg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ISO-8859-1
Binary file added tests/qgis/input/faults_clip.dbf
Binary file not shown.
1 change: 1 addition & 0 deletions tests/qgis/input/faults_clip.prj
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PROJCS["GDA94_MGA_zone_50",GEOGCS["GCS_GDA94",DATUM["D_Geocentric_Datum_of_Australia_1994",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",10000000.0],PARAMETER["Central_Meridian",117.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]]
Binary file added tests/qgis/input/faults_clip.shp
Binary file not shown.
Binary file added tests/qgis/input/faults_clip.shx
Binary file not shown.
1 change: 1 addition & 0 deletions tests/qgis/input/folds_clip.cpg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ISO-8859-1
Binary file added tests/qgis/input/folds_clip.dbf
Binary file not shown.
1 change: 1 addition & 0 deletions tests/qgis/input/folds_clip.prj
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PROJCS["GDA94_MGA_zone_50",GEOGCS["GCS_GDA94",DATUM["D_Geocentric_Datum_of_Australia_1994",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",10000000.0],PARAMETER["Central_Meridian",117.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]]
Binary file added tests/qgis/input/folds_clip.shp
Binary file not shown.
Binary file added tests/qgis/input/folds_clip.shx
Binary file not shown.
1 change: 1 addition & 0 deletions tests/qgis/input/geol_clip_no_gaps.cpg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ISO-8859-1
Binary file added tests/qgis/input/geol_clip_no_gaps.dbf
Binary file not shown.
1 change: 1 addition & 0 deletions tests/qgis/input/geol_clip_no_gaps.prj
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PROJCS["GDA94_MGA_zone_50",GEOGCS["GCS_GDA94",DATUM["D_Geocentric_Datum_of_Australia_1994",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",10000000.0],PARAMETER["Central_Meridian",117.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]]
Binary file added tests/qgis/input/geol_clip_no_gaps.shp
Binary file not shown.
Binary file added tests/qgis/input/geol_clip_no_gaps.shx
Binary file not shown.
1 change: 1 addition & 0 deletions tests/qgis/input/structure_clip.cpg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ISO-8859-1
Binary file added tests/qgis/input/structure_clip.dbf
Binary file not shown.
1 change: 1 addition & 0 deletions tests/qgis/input/structure_clip.prj
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PROJCS["GDA94_MGA_zone_50",GEOGCS["GCS_GDA94",DATUM["D_Geocentric_Datum_of_Australia_1994",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",10000000.0],PARAMETER["Central_Meridian",117.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]]
Binary file added tests/qgis/input/structure_clip.shp
Binary file not shown.
Binary file added tests/qgis/input/structure_clip.shx
Binary file not shown.
116 changes: 116 additions & 0 deletions tests/qgis/test_basal_contacts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import unittest
from pathlib import Path
from qgis.core import QgsVectorLayer, QgsProcessingContext, QgsProcessingFeedback, QgsMessageLog, Qgis, QgsApplication
from qgis.testing import start_app
from m2l.processing.algorithms.extract_basal_contacts import BasalContactsAlgorithm
from m2l.processing.provider import Map2LoopProvider

class TestBasalContacts(unittest.TestCase):

@classmethod
def setUpClass(cls):
cls.qgs = start_app()

cls.provider = Map2LoopProvider()
QgsApplication.processingRegistry().addProvider(cls.provider)

def setUp(self):
self.test_dir = Path(__file__).parent
self.input_dir = self.test_dir / "input"

self.geology_file = self.input_dir / "geol_clip_no_gaps.shp"
self.faults_file = self.input_dir / "faults_clip.shp"

self.assertTrue(self.geology_file.exists(), f"geology not found: {self.geology_file}")

if not self.faults_file.exists():
QgsMessageLog.logMessage(f"faults not found: {self.faults_file}, will run test without faults", "TestBasalContacts", Qgis.Warning)

def test_basal_contacts_extraction(self):

geology_layer = QgsVectorLayer(str(self.geology_file), "geology", "ogr")

self.assertTrue(geology_layer.isValid(), "geology layer should be valid")
self.assertGreater(geology_layer.featureCount(), 0, "geology layer should have features")

faults_layer = None
if self.faults_file.exists():
faults_layer = QgsVectorLayer(str(self.faults_file), "faults", "ogr")
self.assertTrue(faults_layer.isValid(), "faults layer should be valid")
self.assertGreater(faults_layer.featureCount(), 0, "faults layer should have features")
QgsMessageLog.logMessage(f"faults layer: {faults_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical)

QgsMessageLog.logMessage(f"geology layer: {geology_layer.featureCount()} features", "TestBasalContacts", Qgis.Critical)

strati_column = [
"Turee Creek Group",
"Boolgeeda Iron Formation",
"Woongarra Rhyolite",
"Weeli Wolli Formation",
"Brockman Iron Formation",
"Mount McRae Shale and Mount Sylvia Formation",
"Wittenoom Formation",
"Marra Mamba Iron Formation",
"Jeerinah Formation",
"Bunjinah Formation",
"Pyradie Formation",
"Fortescue Group",
"Hardey Formation",
"Boongal Formation",
"Mount Roe Basalt",
"Rocklea Inlier greenstones",
"Rocklea Inlier metagranitic unit"
]

algorithm = BasalContactsAlgorithm()
algorithm.initAlgorithm()

parameters = {
'GEOLOGY': geology_layer,
'UNIT_NAME_FIELD': 'unitname',
'FORMATION_FIELD': 'formation',
'FAULTS': faults_layer,
'STRATIGRAPHIC_COLUMN': strati_column,
'IGNORE_UNITS': [],
'BASAL_CONTACTS': 'memory:basal_contacts'
}

context = QgsProcessingContext()
feedback = QgsProcessingFeedback()

try:
QgsMessageLog.logMessage("Starting basal contacts algorithm...", "TestBasalContacts", Qgis.Critical)

result = algorithm.processAlgorithm(parameters, context, feedback)

QgsMessageLog.logMessage(f"Result: {result}", "TestBasalContacts", Qgis.Critical)

self.assertIsNotNone(result, "result should not be None")
self.assertIn('BASAL_CONTACTS', result, "Result should contain BASAL_CONTACTS key")

basal_contacts_layer = context.takeResultLayer(result['BASAL_CONTACTS'])
self.assertIsNotNone(basal_contacts_layer, "basal contacts layer should not be None")
self.assertTrue(basal_contacts_layer.isValid(), "basal contacts layer should be valid")
self.assertGreater(basal_contacts_layer.featureCount(), 0, "basal contacts layer should have features")

QgsMessageLog.logMessage(f"Generated {basal_contacts_layer.featureCount()} basal contacts",
"TestBasalContacts", Qgis.Critical)

QgsMessageLog.logMessage("Basal contacts test completed successfully!", "TestBasalContacts", Qgis.Critical)

except Exception as e:
QgsMessageLog.logMessage(f"Basal contacts test error: {str(e)}", "TestBasalContacts", Qgis.Critical)
QgsMessageLog.logMessage(f"Error type: {type(e).__name__}", "TestBasalContacts", Qgis.Critical)
import traceback
QgsMessageLog.logMessage(f"Full traceback:\n{traceback.format_exc()}", "TestBasalContacts", Qgis.Critical)
raise

finally:
QgsMessageLog.logMessage("=" * 50, "TestBasalContacts", Qgis.Critical)

@classmethod
def tearDownClass(cls):
QgsApplication.processingRegistry().removeProvider(cls.provider)

if __name__ == '__main__':
unittest.main()