Skip to content
18 changes: 8 additions & 10 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ orbs:
coverage-reporter: codacy/coverage-reporter@13.13.0

commands:

check_changes:
steps:
- run:
Expand Down Expand Up @@ -76,7 +75,7 @@ commands:
- run:
name: Install git+ssh
environment:
DEBIAN_FRONTEND: noninteractive # needed to install tzdata
DEBIAN_FRONTEND: noninteractive # needed to install tzdata
command: apt update && apt install -y git ssh
- checkout
- check_changes
Expand Down Expand Up @@ -153,9 +152,9 @@ jobs:
- /root/.cache/pip
- .pytest_cache
- coverage-reporter/send_report:
coverage-reports: 'test-reports/coverage.xml'
coverage-reports: "test-reports/coverage.xml"
project-token: $CODACY_PROJECT_TOKEN
skip: true # skip if project-token is not defined (i.e. on a fork)
skip: true # skip if project-token is not defined (i.e. on a fork)

test_installation_from_source_test_mode:
# Test installation from source
Expand Down Expand Up @@ -187,7 +186,7 @@ jobs:
- run:
name: Install git and ssh
environment:
DEBIAN_FRONTEND: noninteractive # needed to install tzdata
DEBIAN_FRONTEND: noninteractive # needed to install tzdata
command: apt update && apt install -y git ssh
- checkout
- run:
Expand All @@ -204,6 +203,8 @@ jobs:
# https://docs.esmvaltool.org/en/latest/quickstart/installation.html#install-from-source
. /opt/conda/etc/profile.d/conda.sh
mkdir /logs
# Temporarily install intake-esgf here until it is an ESMValCore dependency in v2.14.
echo " - intake-esgf" >> environment.yml
conda env create -n esmvaltool -f environment.yml --verbose
conda activate esmvaltool
mamba list >> /logs/conda.txt
Expand All @@ -217,12 +218,9 @@ jobs:
command: |
. /opt/conda/etc/profile.d/conda.sh
conda activate esmvaltool
mkdir -p ~/climate_data
esmvaltool config get_config_user
echo "search_esgf: when_missing" >> ~/.config/esmvaltool/config-user.yml
cat ~/.config/esmvaltool/config-user.yml
esmvaltool config copy data-esmvalcore-esgf.yml
for recipe in esmvaltool/recipes/testing/recipe_*.yml; do
esmvaltool run "$recipe"
esmvaltool run --max-parallel-tasks=2 "$recipe"
done
- store_artifacts:
path: /root/esmvaltool_output
Expand Down
185 changes: 86 additions & 99 deletions tests/integration/test_recipes_loading.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,93 @@
"""Test recipes are well formed."""

from dataclasses import dataclass
from pathlib import Path

import esmvalcore
import esmvalcore._recipe.check
import esmvalcore._recipe.recipe
import esmvalcore.cmor.check
import esmvalcore.dataset
import esmvalcore.typing
import pytest
import pytest_mock
import yaml
from esmvalcore import __version__ as core_ver
from esmvalcore.config import CFG, Session, _config
from packaging import version

import esmvaltool
try:
# Since ESValCore v2.14.0
from esmvalcore.io import local

if version.parse(core_ver) < version.parse("2.8.0"):
from esmvalcore._config import _config
from esmvalcore.experimental.config import CFG
USE_DATA_SOURCES = True
except ImportError:
# Prior to ESMValCore v2.14.0
from esmvalcore import local

# Work around
# https://github.com/ESMValGroup/ESMValCore/issues/1579
def clear(self):
self._mapping.clear()
USE_DATA_SOURCES = False

esmvalcore.experimental.config.Config.clear = clear
else:
from esmvalcore.config import CFG, _config
import esmvaltool


@pytest.fixture
def session(mocker, tmp_path):
mocker.patch.dict(
CFG,
auxiliary_data_dir=str(tmp_path / "auxiliary_data_dir"),
check_level=esmvalcore.cmor.check.CheckLevels["DEFAULT"],
drs={},
search_esgf="never",
rootpath={"default": str(tmp_path)},
)
def session(mocker: pytest_mock.MockerFixture, tmp_path: Path) -> Session:
session = CFG.start_session("test")

# The patched_datafinder fixture does not return the correct input
# directory structure, so make sure it is set to flat for every project
for project in _config.CFG:
mocker.patch.dict(_config.CFG[project]["input_dir"], default="/")
session["auxiliary_data_dir"] = str(tmp_path / "auxiliary_data_dir")
session["check_level"] = esmvalcore.cmor.check.CheckLevels["DEFAULT"]
if USE_DATA_SOURCES:
for project in session["projects"]:
session["projects"][project]["data"] = {
"mock_data_source": {
"type": "tests.integration.test_recipes_loading.MockDataSource",
"priority": 1,
"rootpath": tmp_path / project,
},
}
else:
# Mock input file finding.
mocker.patch.dict(
CFG,
drs={},
search_esgf="never",
rootpath={"default": str(tmp_path)},
)
mocker.patch.object(
local,
"glob",
autospec=True,
side_effect=lambda *_, **__: [
"test_0001-1849.nc",
"test_1850-9999.nc",
],
)
# The patched_datafinder fixture does not return the correct input
# directory structure, so make sure it is set to flat for every project
for project in _config.CFG:
mocker.patch.dict(_config.CFG[project]["input_dir"], default="/")

return session


@dataclass
class MockDataSource:
name: str
project: str
priority: int
rootpath: Path
debug_info: str = "Mock data source for testing."

def find_data(
self,
**facets: esmvalcore.typing.FacetValue, # noqa: ARG002
) -> local.LocalFile:
file1 = local.LocalFile(self.rootpath / "test_0001-1849.nc")
file1.facets["timerange"] = "0001/1849"
file2 = local.LocalFile(self.rootpath / "test_1850-9999.nc")
file2.facets["timerange"] = "1850/9999"
return [file1, file2]


def _get_recipes():
recipes_path = Path(esmvaltool.__file__).absolute().parent / "recipes"
recipes = sorted(recipes_path.glob("**/recipe*.yml"))
Expand All @@ -58,87 +101,31 @@ def _get_recipes():
@pytest.mark.parametrize("recipe_file", RECIPES, ids=IDS)
def test_recipe_valid(recipe_file, session, mocker):
"""Check that recipe files are valid ESMValTool recipes."""
# Mock input files
try:
# Since ESValCore v2.8.0
import esmvalcore.local

module = esmvalcore.local
method = "glob"
except ImportError:
# Prior to ESMValCore v2.8.0
import esmvalcore._data_finder

module = esmvalcore._data_finder
method = "find_files"

# Do not remove unexpanded supplementaries. These cannot be expanded
# because the mocked file finding above does not produce facets.
mocker.patch.object(
module,
method,
esmvalcore.dataset.Dataset,
"_remove_unexpanded_supplementaries",
autospec=True,
side_effect=lambda *_, **__: [
"test_0001-1849.nc",
"test_1850-9999.nc",
],
spec_set=True,
)

# Do not remove unexpanded supplementaries. These cannot be expanded
# because the mocked file finding above does not produce facets.
try:
import esmvalcore.dataset
except ImportError:
pass
else:
mocker.patch.object(
esmvalcore.dataset.Dataset,
"_remove_unexpanded_supplementaries",
autospec=True,
spec_set=True,
)

# Mock vertical levels
# Account for module change after esmvalcore=2.7
if version.parse(core_ver) <= version.parse("2.7.1"):
import esmvalcore._recipe

mocker.patch.object(
esmvalcore._recipe,
"get_reference_levels",
autospec=True,
spec_set=True,
side_effect=lambda *_, **__: [1, 2],
)
else:
import esmvalcore._recipe.recipe

mocker.patch.object(
esmvalcore._recipe.recipe,
"get_reference_levels",
autospec=True,
spec_set=True,
side_effect=lambda *_, **__: [1, 2],
)
mocker.patch.object(
esmvalcore._recipe.recipe,
"get_reference_levels",
autospec=True,
spec_set=True,
side_effect=lambda *_, **__: [1, 2],
)

# Mock valid NCL version
# Account for module change after esmvalcore=2.7
if version.parse(core_ver) <= version.parse("2.7.1"):
import esmvalcore._recipe_checks

mocker.patch.object(
esmvalcore._recipe_checks,
"ncl_version",
autospec=True,
spec_set=True,
)
else:
import esmvalcore._recipe.check

mocker.patch.object(
esmvalcore._recipe.check,
"ncl_version",
autospec=True,
spec_set=True,
)
mocker.patch.object(
esmvalcore._recipe.check,
"ncl_version",
autospec=True,
spec_set=True,
)

# Mock interpreters installed
def which(executable):
Expand Down