diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml index 4543564..cf13a1b 100644 --- a/.github/workflows/tester.yml +++ b/.github/workflows/tester.yml @@ -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 @@ -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 diff --git a/m2l/processing/algorithms/extract_basal_contacts.py b/m2l/processing/algorithms/extract_basal_contacts.py index d7beb34..c78653e 100644 --- a/m2l/processing/algorithms/extract_basal_contacts.py +++ b/m2l/processing/algorithms/extract_basal_contacts.py @@ -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, @@ -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 @@ -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: @@ -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, diff --git a/m2l/processing/algorithms/sorter.py b/m2l/processing/algorithms/sorter.py index 215e79a..849a18a 100644 --- a/m2l/processing/algorithms/sorter.py +++ b/m2l/processing/algorithms/sorter.py @@ -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", ) ) @@ -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 = [] diff --git a/tests/qgis/input/faults_clip.cpg b/tests/qgis/input/faults_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/qgis/input/faults_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/qgis/input/faults_clip.dbf b/tests/qgis/input/faults_clip.dbf new file mode 100644 index 0000000..35f7f0e Binary files /dev/null and b/tests/qgis/input/faults_clip.dbf differ diff --git a/tests/qgis/input/faults_clip.prj b/tests/qgis/input/faults_clip.prj new file mode 100644 index 0000000..51bb0e5 --- /dev/null +++ b/tests/qgis/input/faults_clip.prj @@ -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]] \ No newline at end of file diff --git a/tests/qgis/input/faults_clip.shp b/tests/qgis/input/faults_clip.shp new file mode 100644 index 0000000..7a0e9a2 Binary files /dev/null and b/tests/qgis/input/faults_clip.shp differ diff --git a/tests/qgis/input/faults_clip.shx b/tests/qgis/input/faults_clip.shx new file mode 100644 index 0000000..f129118 Binary files /dev/null and b/tests/qgis/input/faults_clip.shx differ diff --git a/tests/qgis/input/folds_clip.cpg b/tests/qgis/input/folds_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/qgis/input/folds_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/qgis/input/folds_clip.dbf b/tests/qgis/input/folds_clip.dbf new file mode 100644 index 0000000..fc04d6c Binary files /dev/null and b/tests/qgis/input/folds_clip.dbf differ diff --git a/tests/qgis/input/folds_clip.prj b/tests/qgis/input/folds_clip.prj new file mode 100644 index 0000000..51bb0e5 --- /dev/null +++ b/tests/qgis/input/folds_clip.prj @@ -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]] \ No newline at end of file diff --git a/tests/qgis/input/folds_clip.shp b/tests/qgis/input/folds_clip.shp new file mode 100644 index 0000000..509566c Binary files /dev/null and b/tests/qgis/input/folds_clip.shp differ diff --git a/tests/qgis/input/folds_clip.shx b/tests/qgis/input/folds_clip.shx new file mode 100644 index 0000000..981c6ce Binary files /dev/null and b/tests/qgis/input/folds_clip.shx differ diff --git a/tests/qgis/input/geol_clip_no_gaps.cpg b/tests/qgis/input/geol_clip_no_gaps.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/qgis/input/geol_clip_no_gaps.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/qgis/input/geol_clip_no_gaps.dbf b/tests/qgis/input/geol_clip_no_gaps.dbf new file mode 100644 index 0000000..832c68c Binary files /dev/null and b/tests/qgis/input/geol_clip_no_gaps.dbf differ diff --git a/tests/qgis/input/geol_clip_no_gaps.prj b/tests/qgis/input/geol_clip_no_gaps.prj new file mode 100644 index 0000000..51bb0e5 --- /dev/null +++ b/tests/qgis/input/geol_clip_no_gaps.prj @@ -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]] \ No newline at end of file diff --git a/tests/qgis/input/geol_clip_no_gaps.shp b/tests/qgis/input/geol_clip_no_gaps.shp new file mode 100644 index 0000000..0d7d669 Binary files /dev/null and b/tests/qgis/input/geol_clip_no_gaps.shp differ diff --git a/tests/qgis/input/geol_clip_no_gaps.shx b/tests/qgis/input/geol_clip_no_gaps.shx new file mode 100644 index 0000000..78d33f6 Binary files /dev/null and b/tests/qgis/input/geol_clip_no_gaps.shx differ diff --git a/tests/qgis/input/structure_clip.cpg b/tests/qgis/input/structure_clip.cpg new file mode 100644 index 0000000..cd89cb9 --- /dev/null +++ b/tests/qgis/input/structure_clip.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/tests/qgis/input/structure_clip.dbf b/tests/qgis/input/structure_clip.dbf new file mode 100644 index 0000000..7f15994 Binary files /dev/null and b/tests/qgis/input/structure_clip.dbf differ diff --git a/tests/qgis/input/structure_clip.prj b/tests/qgis/input/structure_clip.prj new file mode 100644 index 0000000..51bb0e5 --- /dev/null +++ b/tests/qgis/input/structure_clip.prj @@ -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]] \ No newline at end of file diff --git a/tests/qgis/input/structure_clip.shp b/tests/qgis/input/structure_clip.shp new file mode 100644 index 0000000..60a7773 Binary files /dev/null and b/tests/qgis/input/structure_clip.shp differ diff --git a/tests/qgis/input/structure_clip.shx b/tests/qgis/input/structure_clip.shx new file mode 100644 index 0000000..cb743f8 Binary files /dev/null and b/tests/qgis/input/structure_clip.shx differ diff --git a/tests/qgis/test_basal_contacts.py b/tests/qgis/test_basal_contacts.py new file mode 100644 index 0000000..19c56dc --- /dev/null +++ b/tests/qgis/test_basal_contacts.py @@ -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() \ No newline at end of file