diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 82ce11f8..238319a5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -48,4 +48,16 @@ jobs: - name: Full Tests run: | - python -m pytest -rxs --cov=erddapy tests + python -m pytest -rxs \ + --cov=erddapy \ + --cov-report=term-missing \ + --cov-report=xml \ + tests + + - name: Store coverage report + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' + uses: actions/upload-artifact@604373da6383bf45833079f984012d1253fd6154 # v4.4.3 + with: + name: coverage-xml-py3.12-ubuntu + path: coverage.xml + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 0d5b6cdd..291cab27 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ *.egg* .coverage +coverage.xml +htmlcov/ .ipynb_checkpoints .mypy_cache/ .pytest_cache/ diff --git a/tests/test_griddap.py b/tests/test_griddap.py new file mode 100644 index 00000000..c755b2dc --- /dev/null +++ b/tests/test_griddap.py @@ -0,0 +1,308 @@ +"""Test GridDAP functionality.""" + +import pytest + +from erddapy import ERDDAP +from erddapy.core.griddap import ( + _griddap_check_constraints, + _griddap_check_variables, + _griddap_get_constraints, +) + + +@pytest.fixture +def griddap_erddap(): + """ERDDAP instance for GridDAP testing.""" + return ERDDAP( + server="https://www.neracoos.org/erddap/", + protocol="griddap", + ) + + +class TestGridDAPConstraints: + """Test GridDAP constraint fetching and validation.""" + + @pytest.mark.web + @pytest.mark.vcr + def test__griddap_get_constraints(self): + """Test fetching GridDAP metadata and constraints.""" + dataset_url = ( + "https://www.neracoos.org/erddap/griddap/WW3_EastCoast_latest" + ) + constraints, dim_names, var_names = _griddap_get_constraints( + dataset_url, + step=1, + ) + + # Check that we got constraints + assert isinstance(constraints, dict) + assert len(constraints) > 0 + + # Check dimension names + assert isinstance(dim_names, list) + assert len(dim_names) > 0 + + # Check variable names + assert isinstance(var_names, list) + assert len(var_names) > 0 + + # Check constraint format + for key in constraints: + assert ">=" in key or "<=" in key or "_step" in key + + @pytest.mark.web + @pytest.mark.vcr + def test__griddap_get_constraints_with_step(self): + """Test constraint fetching with custom step size.""" + dataset_url = ( + "https://www.neracoos.org/erddap/griddap/WW3_EastCoast_latest" + ) + constraints_step1, _, _ = _griddap_get_constraints(dataset_url, step=1) + constraints_step5, _, _ = _griddap_get_constraints(dataset_url, step=5) + + # Step constraints should be different + step_keys = [k for k in constraints_step1 if "_step" in k] + for key in step_keys: + assert constraints_step1[key] == 1 + assert constraints_step5[key] == 5 + + +class TestGridDAPConstraintValidation: + """Test constraint validation.""" + + def test__griddap_check_constraints_valid(self): + """Test validation with matching constraints.""" + original = { + "time>=": "2012-01-01", + "time<=": "2021-01-01", + "latitude>=": 21.7, + "latitude<=": 46.49442, + "time_step": 1, + } + user = { + "time>=": "2013-01-01", # Changed value is OK + "time<=": "2020-01-01", + "latitude>=": 25.0, + "latitude<=": 45.0, + "time_step": 1, + } + # Should not raise + _griddap_check_constraints(user, original) + + def test__griddap_check_constraints_missing_key(self): + """Test validation with missing constraint key.""" + original = { + "time>=": "2012-01-01", + "time<=": "2021-01-01", + "latitude>=": 21.7, + } + user = { + "time>=": "2013-01-01", + "time<=": "2020-01-01", + # Missing latitude>= + } + with pytest.raises(ValueError, match="Re-run e.griddap_initialize"): + _griddap_check_constraints(user, original) + + def test__griddap_check_constraints_extra_key(self): + """Test validation with extra constraint key.""" + original = { + "time>=": "2012-01-01", + "time<=": "2021-01-01", + } + user = { + "time>=": "2013-01-01", + "time<=": "2020-01-01", + "latitude>=": 25.0, # Extra key + } + with pytest.raises(ValueError, match="Re-run e.griddap_initialize"): + _griddap_check_constraints(user, original) + + +class TestGridDAPVariableValidation: + """Test variable validation.""" + + def test__griddap_check_variables_valid(self): + """Test validation with valid variables.""" + original = ["temperature", "salinity", "depth"] + user = ["temperature", "salinity"] # Subset is OK + # Should not raise + _griddap_check_variables(user, original) + + def test__griddap_check_variables_single(self): + """Test validation with single variable.""" + original = ["temperature", "salinity"] + user = ["temperature"] + # Should not raise + _griddap_check_variables(user, original) + + def test__griddap_check_variables_invalid(self): + """Test validation with invalid variable.""" + original = ["temperature", "salinity"] + user = ["temperature", "pressure"] # pressure not in original + with pytest.raises(ValueError, match="Re-run e.griddap_initialize"): + _griddap_check_variables(user, original) + + def test__griddap_check_variables_all_invalid(self): + """Test validation with all invalid variables.""" + original = ["temperature", "salinity"] + user = ["pressure", "wind_speed"] + with pytest.raises(ValueError, match="Re-run e.griddap_initialize"): + _griddap_check_variables(user, original) + + +class TestGridDAPInitialization: + """Test ERDDAP griddap_initialize method.""" + + @pytest.mark.web + @pytest.mark.vcr + def test_griddap_initialize(self, griddap_erddap): + """Test GridDAP initialization.""" + griddap_erddap.dataset_id = "WW3_EastCoast_latest" + griddap_erddap.griddap_initialize() + + # Check that constraints were set + assert griddap_erddap.constraints is not None + assert isinstance(griddap_erddap.constraints, dict) + assert len(griddap_erddap.constraints) > 0 + + # Check dimension names + assert griddap_erddap.dim_names is not None + assert isinstance(griddap_erddap.dim_names, list) + + # Check variables + assert griddap_erddap.variables is not None + assert isinstance(griddap_erddap.variables, list) + + @pytest.mark.web + @pytest.mark.vcr + def test_griddap_initialize_with_step(self, griddap_erddap): + """Test GridDAP initialization with custom step.""" + griddap_erddap.dataset_id = "WW3_EastCoast_latest" + griddap_erddap.griddap_initialize(step=5) + + # Check that step was applied + step_keys = [k for k in griddap_erddap.constraints if "_step" in k] + for key in step_keys: + assert griddap_erddap.constraints[key] == 5 + + def test_griddap_initialize_without_dataset_id(self, griddap_erddap): + """Test initialization fails without dataset_id.""" + with pytest.raises(ValueError, match="valid dataset_id"): + griddap_erddap.griddap_initialize() + + @pytest.mark.web + @pytest.mark.vcr + def test_griddap_initialize_opendap_skipped(self): + """Test that OPeNDAP response skips initialization.""" + e = ERDDAP( + server="https://www.neracoos.org/erddap/", + protocol="griddap", + response="opendap", + ) + e.dataset_id = "WW3_EastCoast_latest" + e.griddap_initialize() + + # OPeNDAP should skip initialization + assert e.constraints is None + + +class TestGridDAPDownload: + """Test GridDAP download URL generation.""" + + @pytest.mark.web + @pytest.mark.vcr + def test_griddap_download_url(self, griddap_erddap): + """Test generating GridDAP download URL.""" + griddap_erddap.dataset_id = "WW3_EastCoast_latest" + griddap_erddap.griddap_initialize() + + url = griddap_erddap.get_download_url() + assert "griddap" in url + assert "WW3_EastCoast_latest" in url + + @pytest.mark.web + @pytest.mark.vcr + def test_griddap_download_url_with_subset(self, griddap_erddap): + """Test GridDAP download with spatial/temporal subset.""" + griddap_erddap.dataset_id = "WW3_EastCoast_latest" + griddap_erddap.griddap_initialize() + + # Modify constraints to subset data + if "time>=" in griddap_erddap.constraints: + # Keep only first half of time range + original_constraints = griddap_erddap._constraints_original.copy() + griddap_erddap.constraints["time>="] = ( + original_constraints["time>="] + ) + + url = griddap_erddap.get_download_url() + assert "griddap" in url + + @pytest.mark.web + @pytest.mark.vcr + def test_griddap_download_url_select_variables(self, griddap_erddap): + """Test GridDAP download with variable selection.""" + griddap_erddap.dataset_id = "WW3_EastCoast_latest" + griddap_erddap.griddap_initialize() + + # Select subset of variables + if griddap_erddap.variables and len(griddap_erddap.variables) > 1: + griddap_erddap.variables = [griddap_erddap.variables[0]] + + url = griddap_erddap.get_download_url() + assert "griddap" in url + + +class TestGridDAPIntegration: + """Test GridDAP integration with data conversion.""" + + @pytest.mark.web + @pytest.mark.vcr + def test_griddap_to_xarray(self, griddap_erddap): + """Test converting GridDAP to xarray.""" + import xarray as xr # noqa: PLC0415 + + griddap_erddap.dataset_id = "WW3_EastCoast_latest" + griddap_erddap.griddap_initialize() + + ds = griddap_erddap.to_xarray() + assert isinstance(ds, xr.Dataset) + + +class TestGridDAPEdgeCases: + """Test GridDAP edge cases.""" + + @pytest.mark.web + @pytest.mark.vcr + def test_griddap_constraint_modification(self, griddap_erddap): + """Test modifying constraints after initialization.""" + griddap_erddap.dataset_id = "WW3_EastCoast_latest" + griddap_erddap.griddap_initialize() + + original = griddap_erddap.constraints.copy() + + # Modify a constraint value (should be OK) + if "time>=" in griddap_erddap.constraints: + griddap_erddap.constraints["time>="] = "2020-01-01" + + # Should still be able to generate URL + url = griddap_erddap.get_download_url() + assert url is not None + + @pytest.mark.web + @pytest.mark.vcr + def test_griddap_variable_modification(self, griddap_erddap): + """Test modifying variables after initialization.""" + griddap_erddap.dataset_id = "WW3_EastCoast_latest" + griddap_erddap.griddap_initialize() + + original_vars = griddap_erddap.variables.copy() + + # Select subset (should be OK) + if len(griddap_erddap.variables) > 1: + griddap_erddap.variables = [griddap_erddap.variables[0]] + + # Should still work + url = griddap_erddap.get_download_url() + assert url is not None diff --git a/tests/test_xarray_erddap.py b/tests/test_xarray_erddap.py new file mode 100644 index 00000000..6d1f04b4 --- /dev/null +++ b/tests/test_xarray_erddap.py @@ -0,0 +1,455 @@ +"""Test xarray backend engine integration.""" + +import pytest +import xarray as xr + +from erddapy import ERDDAP +from erddapy.xarray_erddap import ( + ERDDAPyBackendEntrypoint, + _is_netcdf, + _is_url, + _make_opendap, + open_erddap_dataset, +) + + +class TestURLValidation: + """Test URL validation helper functions.""" + + def test__is_url_valid_http(self): + """Test valid HTTP ERDDAP URL.""" + url = "http://erddap.server.com/erddap/tabledap/dataset.nc" + assert _is_url(url) is True + + def test__is_url_valid_https(self): + """Test valid HTTPS ERDDAP URL.""" + url = "https://gliders.ioos.us/erddap/tabledap/dataset.nc" + assert _is_url(url) is True + + def test__is_url_invalid_no_erddap(self): + """Test URL without /erddap/ path.""" + url = "https://example.com/data/dataset.nc" + assert _is_url(url) is False + + def test__is_url_invalid_not_http(self): + """Test non-HTTP URL.""" + url = "ftp://erddap.server.com/erddap/tabledap/dataset.nc" + assert _is_url(url) is False + + def test__is_url_invalid_non_string(self): + """Test non-string input.""" + assert _is_url(123) is False + assert _is_url(None) is False + assert _is_url([]) is False + + +class TestNetCDFDetection: + """Test NetCDF format detection.""" + + def test__is_netcdf_nc_extension(self): + """Test .nc extension detection.""" + assert _is_netcdf("https://server/erddap/tabledap/data.nc") is True + + def test__is_netcdf_nc_with_query(self): + """Test .nc with query parameters.""" + assert _is_netcdf("https://server/erddap/tabledap/data.nc?var1,var2") is True + + def test__is_netcdf_nccf_extension(self): + """Test .ncCF extension detection.""" + assert _is_netcdf("https://server/erddap/tabledap/data.ncCF") is True + + def test__is_netcdf_nccf_with_query(self): + """Test .ncCF with query parameters.""" + assert _is_netcdf("https://server/erddap/tabledap/data.ncCF?var1") is True + + def test__is_netcdf_nccfma_extension(self): + """Test .ncCFMA extension detection.""" + assert _is_netcdf("https://server/erddap/tabledap/data.ncCFMA") is True + + def test__is_netcdf_nccfma_with_query(self): + """Test .ncCFMA with query parameters.""" + assert _is_netcdf("https://server/erddap/tabledap/data.ncCFMA?var1") is True + + def test__is_netcdf_invalid_extension(self): + """Test non-NetCDF extension.""" + assert _is_netcdf("https://server/erddap/tabledap/data.csv") is False + assert _is_netcdf("https://server/erddap/tabledap/data.json") is False + + def test__is_netcdf_opendap_url(self): + """Test OPeNDAP URL (no .nc extension).""" + assert _is_netcdf("https://server/erddap/griddap/dataset") is False + + +class TestOPeNDAPConversion: + """Test OPeNDAP URL conversion.""" + + def test__make_opendap_simple_nc(self): + """Test conversion of simple .nc URL.""" + url = "https://server/erddap/tabledap/dataset.nc" + opendap_url = _make_opendap(url) + assert opendap_url == "https://server/erddap/tabledap/dataset" + assert ".nc" not in opendap_url + + def test__make_opendap_with_query(self): + """Test conversion with query parameters.""" + url = "https://server/erddap/tabledap/dataset.nc?var1,var2" + opendap_url = _make_opendap(url) + assert opendap_url == "https://server/erddap/tabledap/dataset" + assert "?" not in opendap_url + + def test__make_opendap_nccf(self): + """Test conversion of .ncCF URL.""" + url = "https://server/erddap/tabledap/dataset.ncCF?var1" + opendap_url = _make_opendap(url) + assert opendap_url == "https://server/erddap/tabledap/dataset" + + def test__make_opendap_preserves_path(self): + """Test that conversion preserves the full path.""" + url = "https://server/erddap/griddap/complex/path/dataset.nc" + opendap_url = _make_opendap(url) + assert "complex/path" in opendap_url + assert opendap_url.startswith("https://server/erddap/griddap/") + + +class TestBackendEntrypoint: + """Test xarray backend entrypoint class.""" + + def test_backend_entrypoint_exists(self): + """Test that backend entrypoint can be instantiated.""" + backend = ERDDAPyBackendEntrypoint() + assert isinstance(backend, xr.backends.BackendEntrypoint) + + def test_backend_has_description(self): + """Test backend has description attribute.""" + backend = ERDDAPyBackendEntrypoint() + assert hasattr(backend, "description") + assert isinstance(backend.description, str) + assert "ERDDAP" in backend.description + + def test_backend_has_open_dataset_method(self): + """Test backend has open_dataset method.""" + backend = ERDDAPyBackendEntrypoint() + assert hasattr(backend, "open_dataset") + assert callable(backend.open_dataset) + + def test_backend_open_dataset_parameters(self): + """Test backend has correct parameters.""" + backend = ERDDAPyBackendEntrypoint() + assert hasattr(backend, "open_dataset_parameters") + assert "filename_or_obj" in backend.open_dataset_parameters + assert "drop_variables" in backend.open_dataset_parameters + + +class TestOpenERDDAPDataset: + """Test opening ERDDAP datasets.""" + + def test_open_erddap_dataset_invalid_url_type(self): + """Test error with non-URL input.""" + with pytest.raises(ValueError, match="Expected an ERDDAP URL"): + open_erddap_dataset("not_a_url.nc") + + def test_open_erddap_dataset_invalid_url_no_erddap(self): + """Test error with URL missing /erddap/.""" + with pytest.raises(ValueError, match="Expected an ERDDAP URL"): + open_erddap_dataset("https://example.com/data/dataset.nc") + + @pytest.mark.web + @pytest.mark.vcr + def test_open_erddap_dataset_netcdf_tabledap(self): + """Test opening TableDAP .nc URL.""" + url = ( + "https://gliders.ioos.us/erddap/tabledap/" + "amelia-20180501T0000.nc?temperature,time" + "&time>=2018-05-08T00:00:00Z&time<=2018-05-13T21:00:00Z" + ) + ds = open_erddap_dataset(url) + assert isinstance(ds, xr.Dataset) + assert "temperature" in ds.variables + assert "time" in ds.variables + + @pytest.mark.web + @pytest.mark.vcr + def test_open_erddap_dataset_netcdf_griddap(self): + """Test opening GridDAP .nc URL.""" + url = ( + "https://www.neracoos.org/erddap/griddap/" + "WW3_EastCoast_latest.nc" + ) + ds = open_erddap_dataset(url) + assert isinstance(ds, xr.Dataset) + + @pytest.mark.web + @pytest.mark.vcr + def test_open_erddap_dataset_nccf(self): + """Test opening .ncCF URL.""" + url = ( + "https://gliders.ioos.us/erddap/tabledap/" + "amelia-20180501T0000.ncCF?temperature,time" + "&time>=2018-05-08T00:00:00Z&time<=2018-05-13T21:00:00Z" + ) + ds = open_erddap_dataset(url) + assert isinstance(ds, xr.Dataset) + + +class TestXarrayEngineIntegration: + """Test xarray engine integration via xr.open_dataset.""" + + @pytest.mark.web + @pytest.mark.vcr + def test_xarray_open_dataset_with_engine(self): + """Test opening ERDDAP URL with engine='erddap'.""" + url = ( + "https://gliders.ioos.us/erddap/tabledap/" + "amelia-20180501T0000.nc?temperature,time" + "&time>=2018-05-08T00:00:00Z&time<=2018-05-13T21:00:00Z" + ) + ds = xr.open_dataset(url, engine="erddap") + assert isinstance(ds, xr.Dataset) + assert "temperature" in ds.variables + + @pytest.mark.web + @pytest.mark.vcr + def test_xarray_engine_griddap(self): + """Test engine with GridDAP dataset.""" + url = "https://www.neracoos.org/erddap/griddap/WW3_EastCoast_latest.nc" + ds = xr.open_dataset(url, engine="erddap") + assert isinstance(ds, xr.Dataset) + assert len(ds.data_vars) > 0 + + @pytest.mark.web + def test_xarray_engine_vs_default(self): + """Test that engine works where default xarray might not.""" + # This tests the value of the plugin: handling non-OPeNDAP URLs + url = ( + "https://gliders.ioos.us/erddap/tabledap/" + "amelia-20180501T0000.nc?temperature" + "&time>=2018-05-08T00:00:00Z&time<=2018-05-08T01:00:00Z" + ) + # With engine='erddap' should work + ds = xr.open_dataset(url, engine="erddap") + assert isinstance(ds, xr.Dataset) + + # Default xarray may fail (uncomment to test this behavior) + # with pytest.raises(Exception): + # xr.open_dataset(url) + + +class TestXarrayEngineWithERDDAP: + """Test xarray engine with ERDDAP class.""" + + @pytest.fixture + def erddap_tabledap(self): + """ERDDAP instance for TableDAP testing.""" + return ERDDAP( + server="https://gliders.ioos.us/erddap/", + protocol="tabledap", + response="nc", + ) + + @pytest.fixture + def erddap_griddap(self): + """ERDDAP instance for GridDAP testing.""" + return ERDDAP( + server="https://www.neracoos.org/erddap/", + protocol="griddap", + response="nc", + ) + + @pytest.mark.web + @pytest.mark.vcr + def test_engine_with_erddap_tabledap(self, erddap_tabledap): + """Test engine with ERDDAP-generated TableDAP URL.""" + erddap_tabledap.dataset_id = "amelia-20180501T0000" + erddap_tabledap.variables = ["temperature", "time"] + erddap_tabledap.constraints = { + "time>=": "2018-05-08T00:00:00Z", + "time<=": "2018-05-13T21:00:00Z", + } + url = erddap_tabledap.get_download_url() + + ds = xr.open_dataset(url, engine="erddap") + assert isinstance(ds, xr.Dataset) + assert "temperature" in ds.variables + + @pytest.mark.web + @pytest.mark.vcr + def test_engine_with_erddap_griddap(self, erddap_griddap): + """Test engine with ERDDAP-generated GridDAP URL.""" + erddap_griddap.dataset_id = "WW3_EastCoast_latest" + erddap_griddap.griddap_initialize() + url = erddap_griddap.get_download_url() + + ds = xr.open_dataset(url, engine="erddap") + assert isinstance(ds, xr.Dataset) + + +class TestXarrayEngineAttributes: + """Test attribute and coordinate handling.""" + + @pytest.mark.web + @pytest.mark.vcr + def test_attributes_preserved(self): + """Test that variable attributes are preserved.""" + url = ( + "https://gliders.ioos.us/erddap/tabledap/" + "amelia-20180501T0000.nc?temperature" + "&time>=2018-05-08T00:00:00Z&time<=2018-05-08T01:00:00Z" + ) + ds = xr.open_dataset(url, engine="erddap") + + # Check variable has attributes + assert hasattr(ds["temperature"], "attrs") + assert len(ds["temperature"].attrs) > 0 + + @pytest.mark.web + @pytest.mark.vcr + def test_coordinates_detected(self): + """Test that coordinates are properly detected.""" + url = ( + "https://gliders.ioos.us/erddap/tabledap/" + "amelia-20180501T0000.nc?temperature,time" + "&time>=2018-05-08T00:00:00Z&time<=2018-05-08T01:00:00Z" + ) + ds = xr.open_dataset(url, engine="erddap") + + # Check that time is in coordinates + assert "time" in ds.coords or "time" in ds.dims + + +class TestXarrayEngineOperations: + """Test xarray operations on datasets opened with engine.""" + + @pytest.mark.web + @pytest.mark.vcr + def test_selection_operations(self): + """Test that xarray selection works.""" + url = ( + "https://gliders.ioos.us/erddap/tabledap/" + "amelia-20180501T0000.nc?temperature,time" + "&time>=2018-05-08T00:00:00Z&time<=2018-05-13T21:00:00Z" + ) + ds = xr.open_dataset(url, engine="erddap") + + # Test selection works + if len(ds.time) > 0: + subset = ds.isel(time=0) + assert isinstance(subset, xr.Dataset) + + @pytest.mark.web + @pytest.mark.vcr + def test_arithmetic_operations(self): + """Test arithmetic operations on opened data.""" + url = ( + "https://gliders.ioos.us/erddap/tabledap/" + "amelia-20180501T0000.nc?temperature" + "&time>=2018-05-08T00:00:00Z&time<=2018-05-08T01:00:00Z" + ) + ds = xr.open_dataset(url, engine="erddap") + + # Test arithmetic works + temp = ds["temperature"] + temp_kelvin = temp + 273.15 + assert isinstance(temp_kelvin, xr.DataArray) + + @pytest.mark.web + @pytest.mark.vcr + def test_to_dataframe(self): + """Test conversion to pandas DataFrame.""" + import pandas as pd # noqa: PLC0415 + + url = ( + "https://gliders.ioos.us/erddap/tabledap/" + "amelia-20180501T0000.nc?temperature,time" + "&time>=2018-05-08T00:00:00Z&time<=2018-05-08T01:00:00Z" + ) + ds = xr.open_dataset(url, engine="erddap") + df = ds.to_dataframe() + + assert isinstance(df, pd.DataFrame) + + +class TestXarrayEngineResponseFormats: + """Test different response formats.""" + + @pytest.mark.web + @pytest.mark.vcr + def test_nc_response(self): + """Test with .nc response.""" + url = ( + "https://gliders.ioos.us/erddap/tabledap/" + "amelia-20180501T0000.nc?temperature" + "&time>=2018-05-08T00:00:00Z&time<=2018-05-08T01:00:00Z" + ) + ds = xr.open_dataset(url, engine="erddap") + assert isinstance(ds, xr.Dataset) + + @pytest.mark.web + @pytest.mark.vcr + def test_nccf_response(self): + """Test with .ncCF response.""" + url = ( + "https://gliders.ioos.us/erddap/tabledap/" + "amelia-20180501T0000.ncCF?temperature" + "&time>=2018-05-08T00:00:00Z&time<=2018-05-08T01:00:00Z" + ) + ds = xr.open_dataset(url, engine="erddap") + assert isinstance(ds, xr.Dataset) + + +class TestXarrayEngineErrorHandling: + """Test error handling.""" + + def test_invalid_url_format(self): + """Test error with invalid URL format.""" + with pytest.raises(ValueError, match="Expected an ERDDAP URL"): + xr.open_dataset("not_a_valid_url", engine="erddap") + + @pytest.mark.web + def test_nonexistent_dataset(self): + """Test error with non-existent dataset.""" + url = ( + "https://gliders.ioos.us/erddap/tabledap/" + "nonexistent_dataset_12345.nc" + ) + with pytest.raises(Exception): + xr.open_dataset(url, engine="erddap") + + @pytest.mark.web + def test_malformed_erddap_url(self): + """Test error with malformed ERDDAP URL.""" + url = "https://gliders.ioos.us/erddap/tabledap/" # No dataset + with pytest.raises(Exception): + xr.open_dataset(url, engine="erddap") + + +class TestEdgeCases: + """Test edge cases and special scenarios.""" + + def test_url_with_multiple_nc_extensions(self): + """Test URL with .nc appearing multiple times.""" + url = "https://server/erddap/nc_data/dataset.nc.nc?vars" + opendap = _make_opendap(url) + # Should remove only the last .nc + assert opendap.count(".nc") < url.count(".nc") + + def test_is_netcdf_edge_cases(self): + """Test edge cases in NetCDF detection.""" + # Just "nc" in path but not as extension + assert _is_netcdf("https://server/erddap/nc/dataset.csv") is False + + # .nc in query parameter + assert _is_netcdf("https://server/data?file=data.nc") is False + + @pytest.mark.web + @pytest.mark.vcr + def test_empty_dataset(self): + """Test handling of dataset with no matching data.""" + # URL with constraints that return no data + url = ( + "https://gliders.ioos.us/erddap/tabledap/" + "amelia-20180501T0000.nc?temperature" + "&time>=2099-01-01T00:00:00Z&time<=2099-01-01T01:00:00Z" + ) + # Should open successfully but may have empty arrays + ds = xr.open_dataset(url, engine="erddap") + assert isinstance(ds, xr.Dataset)