Source code for science_tests.preflight
"""Pre-submission path checks for science-test production runs.
Checks run on the submit host and in one high-priority worker job using the production
backend configuration. Checked are all absolute context paths and the absolute ``*_PATH``
entries of the job environment file.
"""
import copy
import shlex
from pathlib import Path
from simtools.job_execution.execution import submit_jobs, wait_for_submission
from simtools.job_execution.job import ExecutionOptions, JobSpec
from simtools.runners.simtools_runner import resolve_nested_configuration_files
from simtools.science_tests.acceptance import nonempty
PREFLIGHT_PRIORITY = 1_000_000
_CHECK_PATH_FUNCTION = (
"check_path() {\n"
' if ! test -r "$2" || ! find -L "$2" -type f -size +0c -print -quit | grep -q .; then\n'
' echo "PREFLIGHT_FAILED: $1 ($2)" >&2; exit 1\n'
" fi\n"
' echo "PREFLIGHT_OK: $1 ($2)"\n'
"}"
)
[docs]
def context_paths(context):
"""Return the absolute paths configured in a science-test context.
Parameters
----------
context : dict
Science-test context.
Returns
-------
dict
Absolute paths by context key.
"""
return {
key: Path(value).expanduser()
for key, value in context.items()
if isinstance(value, str) and Path(value).expanduser().is_absolute()
}
[docs]
def validate_context_paths(context):
"""Require every absolute context path to be a non-empty file or directory.
Parameters
----------
context : dict
Science-test context.
"""
errors = [
f"{key} is missing or empty: {path}"
for key, path in context_paths(context).items()
if not nonempty(path)
]
if errors:
raise ValueError("Science-test preflight failed:\n- " + "\n- ".join(errors))
[docs]
def run_worker_canary(test, context, work_dir):
"""Run one worker job checking the context and environment paths in the container.
Parameters
----------
test : simtools.science_tests.execution.PreparedTest
Prepared production test whose first application defines ``backend_config``.
context : dict
Science-test context.
work_dir : pathlib.Path
Work directory for the canary job.
"""
applications = copy.deepcopy(test.workflow["applications"][:1])
resolve_nested_configuration_files(applications, test.replacements)
config = applications[0]["configuration"].get("backend_config") if applications else None
if not isinstance(config, dict) or not config.get("container_image"):
raise ValueError(
f"Preflight requires a backend_config with container_image: {test.result['id']}"
)
config["priority"] = PREFLIGHT_PRIORITY
paths = {key: str(path) for key, path in context_paths(context).items() if path.is_dir()}
paths |= _environment_paths(config.get("environment_file"))
checks = [f"check_path {shlex.quote(key)} {shlex.quote(path)}" for key, path in paths.items()]
command = ("sh", "-c", "\n".join(["set -eu", _CHECK_PATH_FUNCTION, *checks]))
options = ExecutionOptions(backend="htcondor", work_dir=Path(work_dir), backend_config=config)
submission = submit_jobs([JobSpec("science-preflight", 0, command=command)], options)
wait_for_submission(submission, options=options)
def _environment_paths(environment_file):
"""Return absolute ``*_PATH`` entries of a job environment file."""
if not environment_file:
return {}
paths = {}
for line in Path(environment_file).read_text(encoding="utf-8").splitlines():
key, separator, value = line.strip().removeprefix("export ").partition("=")
key, value = key.strip(), value.strip().strip("'\"")
if separator and key.endswith("_PATH") and value.startswith("/") and "$" not in value:
paths[key] = value
return paths