diff --git a/pyproject.toml b/pyproject.toml index 7dae5d73..97964914 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ dependencies = [ "cftime", "ecgtools>=2023.7.13", - "intake==0.7.0", + "intake>=0.7.0", "intake-dataframe-catalog>=0.2.4", "intake-esm>=2023.11.10", "jsonschema", @@ -25,6 +25,12 @@ dependencies = [ ] dynamic = ["version"] +[project.optional-dependencies] +test = [ + "pytest", + "tox", +] + [project.scripts] catalog-build = "access_nri_intake.cli:build" metadata-validate = "access_nri_intake.cli:metadata_validate" diff --git a/tests/conftest.py b/tests/conftest.py index edecd3e7..dfc8a12d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import os +import warnings from pathlib import Path from pytest import fixture @@ -9,6 +10,49 @@ here = os.path.abspath(os.path.dirname(__file__)) +def _get_xfail(): + """ + Get the XFAILS environment variable. We use a default of 1, indicating we expect + to add xfail marker to `test_parse_access_ncfile[AccessOm2Builder-access-om2/output000/ocean/ocean_grid.nc-expected0-True]` + unless specified. + """ + xfails_default = 1 + + try: + return int(os.environ["XFAILS"]) + except KeyError: + warnings.warn( + message=( + "XFAILS enabled by default as coordinate discovery disabled by default. " + "This will be deprecated when coordinate discovery is enabled by default" + ), + category=PendingDeprecationWarning, + ) + return xfails_default + + +_add_xfail = _get_xfail() + + @fixture(scope="session") def test_data(): return Path(os.path.join(here, "data")) + + +def pytest_collection_modifyitems(config, items): + """ + This function is called by pytest to modify the items collected during test + collection. We use it here to mark the xfail tests in + test_builders::test_parse_access_ncfile when we check the file contents & to + ensure we correctly get xfails if we don't have cordinate discovery enabled + in intake-esm. + """ + for item in items: + if ( + item.name + in ( + "test_parse_access_ncfile[AccessOm2Builder-access-om2/output000/ocean/ocean_grid.nc-expected0-True]", + ) + and _add_xfail + ): + item.add_marker("xfail") diff --git a/tests/test_builders.py b/tests/test_builders.py index c28f9eac..fd64c1be 100644 --- a/tests/test_builders.py +++ b/tests/test_builders.py @@ -6,6 +6,9 @@ import intake import pandas as pd import pytest +import xarray as xr +from intake_esm.source import _get_xarray_open_kwargs, _open_dataset +from intake_esm.utils import OPTIONS from access_nri_intake.source import CORE_COLUMNS, builders from access_nri_intake.source.utils import _AccessNCFileInfo @@ -359,6 +362,13 @@ def test_parse_access_filename(builder, filename, expected): assert builder.parse_access_filename(filename) == expected +@pytest.mark.parametrize( + "compare_files", + [ + (True), + (False), + ], +) @pytest.mark.parametrize( "builder, filename, expected", [ @@ -1088,10 +1098,39 @@ def test_parse_access_filename(builder, filename, expected): ), ], ) -def test_parse_access_ncfile(test_data, builder, filename, expected): +def test_parse_access_ncfile(test_data, builder, filename, expected, compare_files): file = str(test_data / Path(filename)) # Set the path to the test data directory expected.path = file assert builder.parse_access_ncfile(file) == expected + + if not compare_files: + return None + + """ + In the rest of this test, we refer to the dataset loaded using intake-esm + as ie_ds and the dataset loaded directly with xarray as xr_ds. + + We also need to perform some additional logic that intake-esm does to avoid + xr.testing.assert_equal from failing due to preprocessing differences. + """ + xarray_open_kwargs = _get_xarray_open_kwargs("netcdf") + + ie_ds = _open_dataset( + urlpath=expected.path, + varname=expected.variable, + xarray_open_kwargs=xarray_open_kwargs, + requested_variables=expected.variable, + ).compute() + ie_ds.set_coords(set(ie_ds.variables) - set(ie_ds.attrs[OPTIONS["vars_key"]])) + + xr_ds = xr.open_dataset(file, **xarray_open_kwargs) + + scalar_variables = [v for v in xr_ds.data_vars if len(xr_ds[v].dims) == 0] + xr_ds = xr_ds.set_coords(scalar_variables) + + xr_ds = xr_ds[expected.variable] + + xr.testing.assert_equal(ie_ds, xr_ds) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..8fc25653 --- /dev/null +++ b/tox.ini @@ -0,0 +1,143 @@ +[tox] +env_list = + py{310,311}-main, + py{310,311}-coords, + py{310,311}-take2, + py{310,311}-take2coords + +minversion = 4.23.0 + +[testenv] +description = Run the tests with pytest +package = wheel +wheel_build_env = .pkg +deps = + pytest + .[test] +commands = + pytest \ + -W ignore::UserWarning \ + ; Unable to parse $N assets + ; Frequency derived from filename does not match frequency determined from file contents + -W ignore::DeprecationWarning \ + ; PydanticDeprecatedSince20 Warning + -W ignore::RuntimeWarning \ + ; Numpy ABI mismatch warning + {tty:--color=yes} {posargs:tests} + +[testenv:coordsdisabled] +setenv = + XFAILS=1 + ; We expect correctness checks to fail here because coordinate discovery isn't + ; enabled in the main branch of intake-esm. This toggles on xfail marks in + ; conftest.py. + +[testenv:coordsenabled] +setenv = + XFAILS=0 + ; We expect correctness checks to pass here because coordinate discovery is + ; enabled in the main branch of intake-esm. This disables xfail marks in conftest.py. + +[testenv:base-main] +description = Pin the intake version to 0.7.0, run pytest +deps = + {[testenv]deps} + git+https://github.com/ACCESS-NRI/intake-dataframe-catalog.git@main#egg=intake_dataframe_catalog + intake==0.7.0 + + +[testenv:base-coords] +description = Use the ACCESS-NRI fork of intake-esm, branch issue_660-coords +deps = + {[testenv]deps} + git+https://github.com/ACCESS-NRI/intake-esm.git@issue_660-coords#egg=intake-esm + git+https://github.com/ACCESS-NRI/intake-dataframe-catalog.git@main#egg=intake_dataframe_catalog + intake==0.7.0 + +[testenv:base-take2] +description = Pin the intake version to 0.7.0, run pytest +deps = + {[testenv]deps} + git+https://github.com/ACCESS-NRI/intake-dataframe-catalog.git@take2#egg=intake_dataframe_catalog + intake>=2.0.0 + + +[testenv:base-take2coords] +description = Use the ACCESS-NRI fork of intake-esm, branch issue_660-coords +deps = + {[testenv]deps} + git+https://github.com/ACCESS-NRI/intake-esm.git@take2-coords#egg=intake-esm + git+https://github.com/ACCESS-NRI/intake-dataframe-catalog.git@take2#egg=intake_dataframe_catalog + intake>=2.0.0 + +[testenv:py39-main] +basepython = python3.9 +deps = {[testenv:base-main]deps} +setenv = {[testenv:coordsdisabled]setenv} +commands = {[testenv]commands} + +[testenv:py310-main] +basepython = python3.10 +deps = {[testenv:base-main]deps} +setenv = {[testenv:coordsdisabled]setenv} +commands = {[testenv]commands} + +[testenv:py311-main] +basepython = python3.11 +deps = {[testenv:base-main]deps} +setenv = {[testenv:coordsdisabled]setenv} +commands = {[testenv]commands} + +[testenv:py39-coords] +basepython = python3.9 +deps = {[testenv:base-coords]deps} +setenv = {[testenv:coordsenabled]setenv} +commands = {[testenv]commands} + +[testenv:py310-coords] +basepython = python3.10 +deps = {[testenv:base-coords]deps} +setenv = {[testenv:coordsenabled]setenv} +commands = {[testenv]commands} + +[testenv:py311-coords] +basepython = python3.11 +deps = {[testenv:base-coords]deps} +setenv = {[testenv:coordsenabled]setenv} +commands = {[testenv]commands} + +[testenv:py39-take2] +basepython = python3.9 +deps = {[testenv:base-take2]deps} +setenv = {[testenv:coordsdisabled]setenv} +commands = {[testenv]commands} + +[testenv:py310-take2] +basepython = python3.10 +deps = {[testenv:base-take2]deps} +setenv = {[testenv:coordsdisabled]setenv} +commands = {[testenv]commands} + +[testenv:py311-take2] +basepython = python3.11 +deps = {[testenv:base-take2]deps} +setenv = {[testenv:coordsdisabled]setenv} +commands = {[testenv]commands} + +[testenv:py39-take2coords] +basepython = python3.9 +deps = {[testenv:base-take2coords]deps} +setenv = {[testenv:coordsenabled]setenv} +commands = {[testenv]commands} + +[testenv:py310-take2coords] +basepython = python3.10 +deps = {[testenv:base-take2coords]deps} +setenv = {[testenv:coordsenabled]setenv} +commands = {[testenv]commands} + +[testenv:py311-take2coords] +basepython = python3.11 +deps = {[testenv:base-take2coords]deps} +setenv = {[testenv:coordsenabled]setenv} +commands = {[testenv]commands}