diff --git a/docs/changes/2475.maintenance.md b/docs/changes/2475.maintenance.md new file mode 100644 index 0000000000..348fae963b --- /dev/null +++ b/docs/changes/2475.maintenance.md @@ -0,0 +1 @@ +Resolve simtools-tests directory when using integration tests config files in applications. diff --git a/src/simtools/constants.py b/src/simtools/constants.py index 67c62bb0f7..1215ed3e74 100644 --- a/src/simtools/constants.py +++ b/src/simtools/constants.py @@ -33,9 +33,55 @@ # Path to resource files RESOURCE_PATH = files("simtools") / "resources" # Paths to test resources -TEST_RESOURCES_ROOT = Path( - os.environ.get("SIMTOOLS_TEST_RESOURCES", "tests/unit_tests/resources") -).expanduser() +_DEFAULT_TEST_RESOURCES_ROOT = Path("tests/unit_tests/resources") + + +def _configured_test_resources_root(): + """Return the test-resource root configured through environment variables.""" + tests_tag = _configured_test_resources_tag() + configured_path = os.environ.get("SIMTOOLS_TEST_RESOURCES") + if configured_path: + return Path(configured_path).expanduser() + + tests_path = os.environ.get("SIMTOOLS_TESTS_PATH") + if tests_path: + tests_tag = tests_tag or _default_test_resources_tag() + if tests_tag: + return Path(tests_path).expanduser() / tests_tag / "integration_tests" + + return None + + +def _configured_test_resources_tag(): + """Return the configured test-resource tag after validating legacy settings.""" + tests_tag = os.environ.get("SIMTOOLS_TESTS_TAG") + legacy_tag = os.environ.get("SIMTOOLS_TESTS_VERSION") + if tests_tag and legacy_tag and tests_tag != legacy_tag: + raise ValueError( + "SIMTOOLS_TESTS_TAG and SIMTOOLS_TESTS_VERSION must match when both are set." + ) + return tests_tag or legacy_tag + + +def _default_test_resources_tag(): + """Return the catalog default tag for the versioned test resources.""" + # Import lazily to keep constants independent of the dependency catalog during module + # initialization. This also lets installed applications use the same default as pytest. + from simtools import dependency_versions # pylint: disable=import-outside-toplevel + + try: + test_resources = dependency_versions.load_dependency_catalog()["simtools-tests"] + except FileNotFoundError, KeyError: + return None + return test_resources.get("tag", test_resources.get("version")) + + +def get_test_resources_root(): + """Return the active test-resource root.""" + return _configured_test_resources_root() or TEST_RESOURCES_ROOT + + +TEST_RESOURCES_ROOT = _configured_test_resources_root() or _DEFAULT_TEST_RESOURCES_ROOT TEST_RESOURCES_STATIC = str(TEST_RESOURCES_ROOT / "static") TEST_RESOURCES_GENERATED = str(TEST_RESOURCES_ROOT / "generated") TEST_RESOURCES_DOWNLOADED = str(TEST_RESOURCES_ROOT / "downloaded") diff --git a/src/simtools/io/ascii_handler.py b/src/simtools/io/ascii_handler.py index 34d6df9625..ecddccf978 100644 --- a/src/simtools/io/ascii_handler.py +++ b/src/simtools/io/ascii_handler.py @@ -2,6 +2,7 @@ import json import logging +import os import tempfile import urllib.request from pathlib import Path @@ -10,12 +11,13 @@ import numpy as np import yaml +from simtools.io import io_handler from simtools.utils.general import ensure_list, is_url _logger = logging.getLogger(__name__) -def collect_data_from_file(file_name, yaml_document=None): +def collect_data_from_file(file_name, yaml_document=None, test_resources_path=None): """ Collect data from file based on its extension. @@ -25,6 +27,8 @@ def collect_data_from_file(file_name, yaml_document=None): Name of the yaml/json/ascii file. yaml_document: None, int Return list of yaml documents or a single document (for yaml files with several documents). + test_resources_path : str or pathlib.Path, optional + Explicit base directory for resolving test-resource paths in structured files. Returns ------- @@ -37,12 +41,22 @@ def collect_data_from_file(file_name, yaml_document=None): suffix = Path(file_name).suffix.lower() try: with open(file_name, encoding="utf-8") as file: - return _collect_data_from_different_file_types(file, file_name, suffix, yaml_document) + data = _collect_data_from_different_file_types(file, file_name, suffix, yaml_document) + return _resolve_configured_test_resource_paths(data, test_resources_path) # broad exception to catch all possible errors in reading the file except Exception as exc: # pylint: disable=broad-except raise type(exc)(f"Failed to read file {file_name}: {exc}") from exc +def _resolve_configured_test_resource_paths(data, test_resources_path=None): + """Resolve test-resource paths when a test-resource environment is active.""" + if test_resources_path is not None: + return io_handler.resolve_test_resource_paths(data, test_resources_path=test_resources_path) + if os.environ.get("SIMTOOLS_TEST_RESOURCES") or os.environ.get("SIMTOOLS_TESTS_PATH"): + return io_handler.resolve_test_resource_paths(data) + return data + + def _collect_data_from_different_file_types(file, file_name, suffix, yaml_document): """Collect data from different file types.""" if suffix == ".json": diff --git a/src/simtools/io/io_handler.py b/src/simtools/io/io_handler.py index 1c39809664..e4114edd72 100644 --- a/src/simtools/io/io_handler.py +++ b/src/simtools/io/io_handler.py @@ -20,15 +20,18 @@ def resolve_test_resource_paths(value, test_resources_path=None): Configuration value, mapping, or sequence to resolve. test_resources_path : str or pathlib.Path, optional Base directory containing the ``static``, ``generated``, and ``downloaded`` - resource directories. Defaults to ``SIMTOOLS_TEST_RESOURCES`` or the unit-test - resource directory. + resource directories. Defaults to ``SIMTOOLS_TEST_RESOURCES`` or the versioned + ``simtools-tests`` integration-test resources selected by ``SIMTOOLS_TESTS_PATH`` and + ``SIMTOOLS_TESTS_TAG``. Returns ------- object Configuration with absolute test-resource paths. """ - base_path = Path(test_resources_path or constants.TEST_RESOURCES_ROOT).expanduser().resolve() + base_path = ( + Path(test_resources_path or constants.get_test_resources_root()).expanduser().resolve() + ) if isinstance(value, dict): return { key: resolve_test_resource_paths(item, test_resources_path=base_path) @@ -64,7 +67,7 @@ def __init__(self): self.logger = logging.getLogger(__name__) self.output_path = {} self.model_path = None - self.test_resources_path = constants.TEST_RESOURCES_ROOT.resolve() + self.test_resources_path = constants.get_test_resources_root().resolve() def set_paths(self, output_path=None, model_path=None, output_path_label="default"): """ diff --git a/src/simtools/testing/configuration.py b/src/simtools/testing/configuration.py index eca61c8622..f6e6900b30 100644 --- a/src/simtools/testing/configuration.py +++ b/src/simtools/testing/configuration.py @@ -130,7 +130,8 @@ def _resolve(value): return value resolved_data = resolve_test_resource_paths( - ascii_handler.collect_data_from_file(path), test_resources_path=test_resources_path + ascii_handler.collect_data_from_file(path, test_resources_path=test_resources_path), + test_resources_path=test_resources_path, ) resolved_config_file = config_dir / path.name ascii_handler.write_data_to_file( diff --git a/tests/unit_tests/configuration/test_configurator.py b/tests/unit_tests/configuration/test_configurator.py index a213782d51..3e9700008b 100644 --- a/tests/unit_tests/configuration/test_configurator.py +++ b/tests/unit_tests/configuration/test_configurator.py @@ -124,7 +124,11 @@ def test_config_from_file_rejects_inconsistent_unpreserved_by_version_key( config_builder._config_from_file(config_file) -def test_config_from_file_resolves_test_resource_paths(tmp_test_directory): +def test_config_from_file_resolves_test_resource_paths(tmp_test_directory, monkeypatch): + monkeypatch.delenv("SIMTOOLS_TEST_RESOURCES", raising=False) + monkeypatch.delenv("SIMTOOLS_TESTS_PATH", raising=False) + monkeypatch.delenv("SIMTOOLS_TESTS_TAG", raising=False) + monkeypatch.delenv("SIMTOOLS_TESTS_VERSION", raising=False) config_dict = { "applications": [ { diff --git a/tests/unit_tests/io/test_ascii_handler.py b/tests/unit_tests/io/test_ascii_handler.py index 306eb56cbb..be2f60760a 100644 --- a/tests/unit_tests/io/test_ascii_handler.py +++ b/tests/unit_tests/io/test_ascii_handler.py @@ -50,6 +50,25 @@ def test_collect_dict_data(io_handler, simple_test_file): ascii_handler.collect_data_from_file(unsupported_file) +def test_collect_data_from_file_resolves_configured_test_resource_paths( + tmp_test_directory, monkeypatch +): + resources_path = tmp_test_directory / "resources" + config_file = tmp_test_directory / "config.yml" + config_file.write_text( + "input: ${generated:input.ecsv}\nlegacy: tests/resources/static/layout.ecsv\n", + encoding="utf-8", + ) + monkeypatch.setenv("SIMTOOLS_TEST_RESOURCES", str(resources_path)) + + loaded = ascii_handler.collect_data_from_file(config_file) + + assert loaded == { + "input": str(resources_path / "generated/input.ecsv"), + "legacy": str(resources_path / "static/layout.ecsv"), + } + + def test_collect_data_from_file_exceptions(io_handler) -> None: # Create an invalid YAML file test_file = io_handler.get_output_file(file_name="invalid.yml") diff --git a/tests/unit_tests/io/test_io_handler.py b/tests/unit_tests/io/test_io_handler.py index 466e58a317..61123c3ba7 100644 --- a/tests/unit_tests/io/test_io_handler.py +++ b/tests/unit_tests/io/test_io_handler.py @@ -86,6 +86,67 @@ def test_resolve_test_resource_path_macros_nested_structures(tmp_test_directory) assert resolved["plain"] == "no_macro" +def test_resolve_test_resource_paths_uses_versioned_environment_resources( + tmp_test_directory, monkeypatch +): + tests_path = tmp_test_directory / "simtools-tests" + monkeypatch.delenv("SIMTOOLS_TEST_RESOURCES", raising=False) + monkeypatch.setenv("SIMTOOLS_TESTS_PATH", str(tests_path)) + monkeypatch.setenv("SIMTOOLS_TESTS_TAG", "v0.37.0") + + resolved = io_handler_module.resolve_test_resource_paths("${downloaded:corsika_limits.ecsv}") + + expected_root = tests_path / "v0.37.0" / "integration_tests" + assert resolved == str(expected_root / "downloaded/corsika_limits.ecsv") + + +def test_resolve_test_resource_paths_rejects_conflicting_environment_versions( + tmp_test_directory, monkeypatch +): + monkeypatch.delenv("SIMTOOLS_TEST_RESOURCES", raising=False) + monkeypatch.setenv("SIMTOOLS_TESTS_PATH", str(tmp_test_directory / "simtools-tests")) + monkeypatch.setenv("SIMTOOLS_TESTS_TAG", "v0.37.0") + monkeypatch.setenv("SIMTOOLS_TESTS_VERSION", "v0.36.0") + + with pytest.raises( + ValueError, + match="SIMTOOLS_TESTS_TAG and SIMTOOLS_TESTS_VERSION must match", + ): + io_handler_module.resolve_test_resource_paths("${generated:input.ecsv}") + + +def test_resolve_test_resource_paths_accepts_matching_environment_versions( + tmp_test_directory, monkeypatch +): + tests_path = tmp_test_directory / "simtools-tests" + monkeypatch.delenv("SIMTOOLS_TEST_RESOURCES", raising=False) + monkeypatch.setenv("SIMTOOLS_TESTS_PATH", str(tests_path)) + monkeypatch.setenv("SIMTOOLS_TESTS_TAG", "v0.37.0") + monkeypatch.setenv("SIMTOOLS_TESTS_VERSION", "v0.37.0") + + resolved = io_handler_module.resolve_test_resource_paths("${generated:input.ecsv}") + + expected_root = tests_path / "v0.37.0" / "integration_tests" + assert resolved == str(expected_root / "generated/input.ecsv") + + +def test_resolve_test_resource_paths_uses_catalog_default_version(tmp_test_directory, monkeypatch): + tests_path = tmp_test_directory / "simtools-tests" + monkeypatch.delenv("SIMTOOLS_TEST_RESOURCES", raising=False) + monkeypatch.setenv("SIMTOOLS_TESTS_PATH", str(tests_path)) + monkeypatch.delenv("SIMTOOLS_TESTS_TAG", raising=False) + monkeypatch.delenv("SIMTOOLS_TESTS_VERSION", raising=False) + monkeypatch.setattr( + "simtools.dependency_versions.load_dependency_catalog", + lambda: {"simtools-tests": {"tag": "v0.37.0"}}, + ) + + resolved = io_handler_module.resolve_test_resource_paths("${generated:input.ecsv}") + + expected_root = tests_path / "v0.37.0" / "integration_tests" + assert resolved == str(expected_root / "generated/input.ecsv") + + def test_get_model_configuration_directory(args_dict, io_handler): model_version = "1.0.0" label = "test-io-handler"