diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml deleted file mode 100644 index 15a61d6b6d9ff9f8b6a2dff50887f876ce3e2293..0000000000000000000000000000000000000000 --- a/.github/workflows/github-actions-demo.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: GitHub Actions Demo -run-name: ${{ github.actor }} is testing out GitHub Actions 🚀 -on: [push] -jobs: - Explore-GitHub-Actions: - runs-on: ubuntu-latest - steps: - - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." - - run: echo "🧠This job is now running on a ${{ runner.os }} server hosted by GitHub!" - - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." - - name: Check out repository code - uses: actions/checkout@v4 - - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - - run: echo "ðŸ–¥ï¸ The workflow is now ready to test your code on the runner." - - name: List files in the repository - run: | - ls ${{ github.workspace }} - - run: echo "ðŸ This job's status is ${{ job.status }}." diff --git a/.github/workflows/jobs.yml b/.github/workflows/jobs.yml new file mode 100644 index 0000000000000000000000000000000000000000..39faa7ff18cc00b1022fe3d0e8234097f454a588 --- /dev/null +++ b/.github/workflows/jobs.yml @@ -0,0 +1,118 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - github-actions + pull_request: + branches: + - github-actions + +jobs: + build: + name: ${{ matrix.os }}, Python 3.${{ matrix.python-minor-version }}, QGIS 3.${{ matrix.qgis-minor-version }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + max-parallel: 6 + matrix: + # os: [ubuntu-latest , macos-latest , windows-latest] + # python-minor-version: [11, 12] + # qgis-minor-version: [34, 36, 38] + os: [windows-latest] + python-minor-version: [11] + qgis-minor-version: [38] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Miniconda + uses: conda-incubator/setup-miniconda@v3 + with: + python-version: 3.${{ matrix.python-minor-version }} + channels: conda-forge + auto-update-conda: true + + - name: Set Python Encoding for Windows + if: matrix.os == 'windows-latest' + run: | + set PYTHONIOENCODING=utf-8 + set PYTHONLEGACYWINDOWSSTDIO=utf-8 + + + - name: Install Mamba in Base Environment + run: conda install -n test -c conda-forge mamba --yes + + - name: Set up Environment and Install Dependencies + run: | + mamba create -n pytest python=3.${{ matrix.python-minor-version }} qgis=3.${{ matrix.qgis-minor-version }} --yes + mamba install -n pytest --file requirements.txt --yes + mamba install -n pytest pytest --yes + shell: bash -el {0} + + - name: Run Tests + run: | + conda run -n pytest pytest + shell: bash -el {0} + + # steps: + # - name: Checkout + # uses: actions/checkout@v3 + + # - name: Set up Miniconda + # uses: conda-incubator/setup-miniconda@v3 + # with: + # python-version: 3.${{matrix.python-minor-version}} + # channels: conda-forge + + # - name: Install Mamba + # run: | + # conda init + # conda install -n test -c conda-forge mamba + # - name: Init Mamba shell + # if: matrix.os == 'windows-latest' + # run: | + # eval "$(mamba.exe shell hook --shell powershell)" + # mamba shell init --shell powershell + # mamba shell reinit --shell powershell + # shell: bash -el {0} + + # - name: Set up Environment and Install Dependencies + # run: | + # conda activate test + # mamba create -n pytest python=3.${{matrix.python-minor-version}} qgis=3.${{matrix.qgis-minor-version}} --yes + # mamba activate pytest + # mamba install --file requirements.txt --yes + # mamba install pytest --yes + # shell: bash -el {0} + # - name: Set up Conda + # # uses: conda-incubator/setup-miniconda@v3 + # uses: mamba-org/setup-micromamba@v1 + # # with: + # # python-version: 3.${{matrix.python-minor-version}} + # # channels: conda-forge + # - run: | + # mamba init + # mamba activate test + # # conda install qgis=3.${{matrix.qgis-minor-version}} + # mamba install qgis=3.${{matrix.qgis-minor-version}} + # mamba install --file requirements.txt + # mamba install pytest + # shell: bash -el {0} + + # - name: Check Path in windows + # if: matrix.os == 'windows-latest' + # run: | + # conda activate test + # $conda_env_path = (Split-Path -Path (Split-Path -Path (Get-Command python).Path)) + + # # Update PATH to include the "Library\bin" directory for the conda environment + # Add-Content -Path $Env:GITHUB_ENV -Value "PATH=$conda_env_path\Library\bin;$Env:PATH" + # shell: powershell + + # - name: Tests + # run: | + # mamba activate test + # pytest tests/ + # shell: bash -el {0} diff --git a/requirements-ga.txt b/requirements-ga.txt new file mode 100644 index 0000000000000000000000000000000000000000..066935452485f829bd69401b77c620ac240f9214 --- /dev/null +++ b/requirements-ga.txt @@ -0,0 +1,14 @@ +geopandas >= 0.14.4 +scikit-learn >= 1.5.1 +psutil >= 5.0.0 +# from torchgeo +rasterio >= 1.2 +rtree >= 0.9 +einops >= 0.3 +fiona >= 1.8.19 +kornia >= 0.6.9 +numpy >= 1.19.3 +pyproj >= 3.3 +shapely >= 1.7.1 +timm >= 0.4.12 +pytest diff --git a/requirements.txt b/requirements.txt index 9e22d502d0f5b04223f31aa1de69e293566197be..a2d31d0b5a68a42242e7d6deda18a7a2bc829f1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ psutil >= 5.0.0 # torchgeo == 0.5.2 # from torchgeo rasterio >= 1.2 -rtree <= 0.9 +rtree >= 1 einops >= 0.3 fiona >= 1.8.19 kornia >= 0.6.9 diff --git a/tests/__init__.py b/tests/__init__.py index 2c76a827efbaa5322459ed6b4497e14f31e27c3d..c0a9a565b53893ed7f0ac80e4786c3b9db9c7ecc 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -4,8 +4,13 @@ import os PYTHON_VERSION = sys.version_info SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) PLUGIN_ROOT_DIR = os.path.realpath(os.path.abspath(os.path.join(SCRIPT_DIR, ".."))) +QGIS_PYTHON_DIR = os.path.realpath(os.path.abspath(os.path.join(PLUGIN_ROOT_DIR, ".."))) PACKAGES_INSTALL_DIR = os.path.join( PLUGIN_ROOT_DIR, f"python{PYTHON_VERSION.major}.{PYTHON_VERSION.minor}" ) sys.path.append(PACKAGES_INSTALL_DIR) # TODO: check for a less intrusive way to do this + +qgis_python_path = os.getenv("PYTHONPATH") +if qgis_python_path and qgis_python_path not in sys.path: + sys.path.append(qgis_python_path) diff --git a/tests/test_common.py b/tests/test_common.py index 22c5b22b3b37bec93687161f07425e04dec5cb18..792cc558d2afd87d5674f93df194a4d11b88c37d 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,4 +1,5 @@ import os +import pytest from pathlib import Path import tempfile import unittest @@ -12,6 +13,8 @@ from ..similarity import SimilarityAlgorithm from ..clustering import ClusterAlgorithm from ..reduction import ReductionAlgorithm from ..utils.misc import get_file_md5_hash, remove_files_with_extensions +from ..utils.geo import validate_geotiff + INPUT = os.path.join(Path(__file__).parent.parent.absolute(), "assets", "test.tif") OUTPUT = os.path.join(tempfile.gettempdir(), "iamap_test") @@ -36,6 +39,8 @@ class TestReductionAlgorithm(unittest.TestCase): "d7a32c6b7a4cee1af9c73607561d7b25", "e04f8c86d9aad81dd9c625b9cd8f9824", ] + output_size = 4405122 + output_wh = (968,379) out_name = "proj.tif" def setUp(self): @@ -48,22 +53,25 @@ class TestReductionAlgorithm(unittest.TestCase): self.default_parameters, self.context, self.feedback ) expected_result_path = os.path.join(self.algorithm.output_dir, self.out_name) - result_file_hash = get_file_md5_hash(expected_result_path) + @pytest.mark.parametrize("output_file", expected_result_path, "expected_output_size", self.output_size, "expected_wh", self.output_wh) + def test_geotiff_validity(output_file): + validate_geotiff(output_file) remove_files_with_extensions(self.algorithm.output_dir, EXTENSIONS_TO_RM) - assert result_file_hash in self.possible_hashes class TestClusteringAlgorithm(TestReductionAlgorithm): algorithm = ClusterAlgorithm() - possible_hashes = ["0c47b0c4b4c13902db5da3ee6e5d4aef"] + # possible_hashes = ["0c47b0c4b4c13902db5da3ee6e5d4aef"] out_name = "cluster.tif" + output_size = 4405122 class TestSimAlgorithm(TestReductionAlgorithm): algorithm = SimilarityAlgorithm() default_parameters = {"INPUT": INPUT, "OUTPUT": OUTPUT, "TEMPLATE": TEMPLATE} - possible_hashes = ["f76eb1f0469725b49fe0252cfe86829a"] + # possible_hashes = ["f76eb1f0469725b49fe0252cfe86829a"] out_name = "similarity.tif" + output_size = 1468988 class TestMLAlgorithm(TestReductionAlgorithm): @@ -74,8 +82,9 @@ class TestMLAlgorithm(TestReductionAlgorithm): "TEMPLATE": TEMPLATE_RF, "GT_COL": GT_COL, } - possible_hashes = ["bd22d66180347e043fca58d494876184"] + # possible_hashes = ["bd22d66180347e043fca58d494876184"] out_name = "ml.tif" + output_size = 367520 if __name__ == "__main__": diff --git a/tests/test_encoder.py b/tests/test_encoder.py index f386f346c4d45cb226e21586d50258a9a9214fb7..ead5a66275b2c2538e47a1841a718baf262304c1 100644 --- a/tests/test_encoder.py +++ b/tests/test_encoder.py @@ -16,6 +16,7 @@ from ..tg.datasets import RasterDataset from ..encoder import EncoderAlgorithm from ..utils.misc import get_file_md5_hash +from ..utils.geo import validate_geotiff INPUT = os.path.join(Path(__file__).parent.parent.absolute(), "assets", "test.tif") @@ -59,42 +60,11 @@ class TestEncoderAlgorithm(unittest.TestCase): self.default_parameters, self.context, self.feedback ) expected_result_path = os.path.join(self.algorithm.output_subdir, "merged.tif") - result_file_hash = get_file_md5_hash(expected_result_path) - - ## different rasterio versions lead to different hashes ? - ## GPU and quantization as well - possible_hashes = [ - "0fb32cc57a0dd427d9f0165ec6d5418f", - "48c3a78773dbc2c4c7bb7885409284ab", - "431e034b842129679b99a067f2bd3ba4", - "60153535214eaa44458db4e297af72b9", - "f1394d1950f91e4f8277a8667ae77e85", - "a23837caa3aca54aaca2974d546c5123", - "43ac54811a1892f81a4793de2426b43f", - ] - assert result_file_hash in possible_hashes + @pytest.mark.parametrize("output_file", expected_result_path) + def test_geotiff_validity(output_file): + validate_geotiff(output_file) os.remove(expected_result_path) - @pytest.mark.slow - def test_data_types(self): - self.algorithm.initAlgorithm() - parameters = self.default_parameters - parameters["OUT_DTYPE"] = 1 - _ = self.algorithm.processAlgorithm(parameters, self.context, self.feedback) - expected_result_path = os.path.join(self.algorithm.output_subdir, "merged.tif") - result_file_hash = get_file_md5_hash(expected_result_path) - - ## different rasterio versions lead to different hashes ? - possible_hashes = [ - "ef0c4b0d57f575c1cd10c0578c7114c0", - "ebfad32752de71c5555bda2b40c19b2e", - "d3705c256320b7190dd4f92ad2087247", - "65fa46916d6d0d08ad9656d7d7fabd01", - "43ac54811a1892f81a4793de2426b43f", - ] - if result_file_hash in possible_hashes: - os.remove(expected_result_path) - assert result_file_hash in possible_hashes def test_timm_create_model(self): archs = [ @@ -173,5 +143,4 @@ if __name__ == "__main__": test_encoder.test_timm_create_model() test_encoder.test_RasterDataset() test_encoder.test_valid_parameters() - test_encoder.test_data_types() test_encoder.test_cuda() diff --git a/utils/geo.py b/utils/geo.py index a0847c05214d95dd2cb2ae96314cfc3c84523d59..cfd410fe3db172be6855003991603ee9fc9f5203 100644 --- a/utils/geo.py +++ b/utils/geo.py @@ -1,5 +1,7 @@ +import os from typing import Callable, Union import rasterio +import rasterio.errors import geopandas as gpd import numpy as np from rasterio.merge import merge @@ -181,6 +183,41 @@ def get_unique_col_name(gdf, base_name="fold"): return column_name +def validate_geotiff(output_file, expected_output_size=4428850, expected_wh=(60,24)): + """ + tests geotiff validity by opening with rasterio, + checking if the file weights as expected and has the correct width and height. + Additionaly, it is checked if there is more than one value in the raster. + """ + + expected_size_min = .8*expected_output_size + expected_size_max = 1.2*expected_output_size + # 1. Check if the output file is a valid GeoTIFF + try: + with rasterio.open(output_file) as src: + assert src.meta['driver'] == 'GTiff', "File is not a valid GeoTIFF." + width = src.width + height = src.height + # 2. Read the data and check width/height + assert width == expected_wh[0], f"Expected width {expected_wh[0]}, got {width}." + assert height == expected_wh[1], f"Expected height {expected_wh[1]}, got {height}." + # 3. Read the data and check for unique values + data = src.read(1) # Read the first band + unique_values = np.unique(data) + + assert len(unique_values) > 1, "The GeoTIFF contains only one unique value." + + except rasterio.errors.RasterioIOError: + print("The file could not be opened as a GeoTIFF, indicating it is invalid.") + assert False + + # 4. Check if the file size is within the expected range + file_size = os.path.getsize(output_file) + assert expected_size_min <= file_size <= expected_size_max, ( + f"File size {file_size} is outside the expected range." + ) + return + if __name__ == "__main__": gdf = gpd.read_file("assets/ml_poly.shp") print(gdf)