Skip to content
1 change: 1 addition & 0 deletions docs/changes/2475.maintenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Resolve simtools-tests directory when using integration tests config files in applications.
52 changes: 49 additions & 3 deletions src/simtools/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
18 changes: 16 additions & 2 deletions src/simtools/io/ascii_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
import logging
import os
import tempfile
import urllib.request
from pathlib import Path
Expand All @@ -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.

Expand All @@ -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
-------
Expand All @@ -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":
Expand Down
11 changes: 7 additions & 4 deletions src/simtools/io/io_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"):
"""
Expand Down
3 changes: 2 additions & 1 deletion src/simtools/testing/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 5 additions & 1 deletion tests/unit_tests/configuration/test_configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
19 changes: 19 additions & 0 deletions tests/unit_tests/io/test_ascii_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
61 changes: 61 additions & 0 deletions tests/unit_tests/io/test_io_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down