diff --git a/CHANGELOG.md b/CHANGELOG.md index e570aba6e..7df859d60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Support comparison of GeoJSON vector cube assets in `assert_job_results_allclose`. ([#862](https://github.com/Open-EO/openeo-python-client/issues/862)) - `DataCube.resample_spatial()` now supports parameterized `resolution` and `projection` arguments. ([#897](https://github.com/Open-EO/openeo-python-client/issues/897)) - Sanitize asset download filenames (e.g. strip slashes, (semi)colon, hash, ...), instead of blindly using the asset key as filename. ([#820](https://github.com/Open-EO/openeo-python-client/issues/820)) - Support parameters in `DataCube` apply- and band-math operations ([#903](https://github.com/Open-EO/openeo-python-client/issues/903)) diff --git a/openeo/testing/results.py b/openeo/testing/results.py index f04ee702e..c31ac84ed 100644 --- a/openeo/testing/results.py +++ b/openeo/testing/results.py @@ -10,6 +10,7 @@ from typing import List, Optional, Union import numpy +import pandas import xarray import xarray.testing from xarray import DataArray @@ -95,6 +96,55 @@ def _as_xarray_dataarray(data: Union[str, Path, xarray.DataArray]) -> xarray.Dat return data +def _load_geodataframe(path: Union[str, Path]): + """Load a vector data file as a GeoPandas GeoDataFrame.""" + try: + import geopandas + except ImportError as e: + raise ImportError("Comparing vector data requires the 'geopandas' dependency.") from e + return geopandas.read_file(path) + + +def _compare_geodataframes( + actual: Union[str, Path], + expected: Union[str, Path], + *, + rtol: float = _DEFAULT_RTOL, + atol: float = _DEFAULT_ATOL, +) -> List[str]: + """Compare vector geometries and their attribute data.""" + actual_gdf = _load_geodataframe(actual) + expected_gdf = _load_geodataframe(expected) + import geopandas.testing + + issues = [] + + try: + pandas.testing.assert_frame_equal( + actual_gdf.drop(columns=actual_gdf.geometry.name), + expected_gdf.drop(columns=expected_gdf.geometry.name), + check_dtype=False, + check_like=True, + rtol=rtol, + atol=atol, + ) + except AssertionError as e: + issues.append(f"Attribute data mismatch:\n{str(e).strip()}") + + try: + geopandas.testing.assert_geoseries_equal( + actual_gdf.geometry, + expected_gdf.geometry, + check_dtype=False, + check_geom_type=True, + check_crs=True, + ) + except AssertionError as e: + issues.append(f"Geometry data mismatch:\n{str(e).strip()}") + + return issues + + def _ascii_art( diff_data: DataArray, *, @@ -459,6 +509,11 @@ def _compare_job_results( if issues: all_issues.append(f"Issues for file {filename!r}:") all_issues.extend(issues) + elif expected_path.suffix.lower() == ".geojson": + issues = _compare_geodataframes(actual=actual_path, expected=expected_path, rtol=rtol, atol=atol) + if issues: + all_issues.append(f"Issues for file {filename!r}:") + all_issues.extend(issues) else: _log.warning(f"Unhandled job result asset {filename!r}") diff --git a/tests/testing/test_results.py b/tests/testing/test_results.py index 04562abe7..bf6e38070 100644 --- a/tests/testing/test_results.py +++ b/tests/testing/test_results.py @@ -432,6 +432,43 @@ def test_allclose_minimal_success(self, tmp_path, actual_dir, expected_dir): ds.to_netcdf(actual_dir / "data.nc") assert_job_results_allclose(actual=actual_dir, expected=expected_dir, tmp_path=tmp_path) + @staticmethod + def _write_geojson(path: Path, *, value: float, coordinates): + path.write_text( + json.dumps( + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"name": "sample", "value": value}, + "geometry": {"type": "Point", "coordinates": coordinates}, + } + ], + } + ) + ) + + def test_allclose_geojson_success(self, tmp_path, actual_dir, expected_dir): + self._write_geojson(expected_dir / "vectorcube.geojson", value=10, coordinates=[3, 51]) + self._write_geojson(actual_dir / "vectorcube.geojson", value=10.000005, coordinates=[3, 51]) + + assert_job_results_allclose(actual=actual_dir, expected=expected_dir, tmp_path=tmp_path) + + def test_allclose_geojson_attribute_mismatch(self, tmp_path, actual_dir, expected_dir): + self._write_geojson(expected_dir / "vectorcube.geojson", value=10, coordinates=[3, 51]) + self._write_geojson(actual_dir / "vectorcube.geojson", value=11, coordinates=[3, 51]) + + with raises_assertion_error_or_not(r"Issues for file 'vectorcube.geojson'.*Attribute data mismatch"): + assert_job_results_allclose(actual=actual_dir, expected=expected_dir, tmp_path=tmp_path) + + def test_allclose_geojson_geometry_mismatch(self, tmp_path, actual_dir, expected_dir): + self._write_geojson(expected_dir / "vectorcube.geojson", value=10, coordinates=[3, 51]) + self._write_geojson(actual_dir / "vectorcube.geojson", value=10, coordinates=[4, 52]) + + with raises_assertion_error_or_not(r"Issues for file 'vectorcube.geojson'.*Geometry data mismatch"): + assert_job_results_allclose(actual=actual_dir, expected=expected_dir, tmp_path=tmp_path) + def test_allclose_xy_success(self, tmp_path, actual_dir, expected_dir): expected_ds = xarray.Dataset( {