diff --git a/src/clib/lib/config/config_parser.cpp b/src/clib/lib/config/config_parser.cpp index 3226d1320ec..fa23f5b9e56 100644 --- a/src/clib/lib/config/config_parser.cpp +++ b/src/clib/lib/config/config_parser.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -657,15 +658,23 @@ config_parse(config_parser_type *config, const char *filename, hash_iter_free(keys); } - if (util_file_readable(filename)) { + bool file_readable_check_succeeded = true; + try { + std::ifstream file_handler; + file_handler.exceptions(std::ifstream::failbit | std::ifstream::badbit); + file_handler.open(filename); + } catch (std::ios_base::failure &err) { + file_readable_check_succeeded = false; + auto error_message = fmt::format( + "could not open file `{}` for parsing - {}", filename, err.what()); + content->parse_errors.push_back(error_message); + } + + if (file_readable_check_succeeded) { path_stack_type *path_stack = path_stack_alloc(); config_parse__(config, content, path_stack, filename, comment_string, include_kw, define_kw, unrecognized_behaviour, validate); path_stack_free(path_stack); - } else { - std::string error_message = - util_alloc_sprintf("Could not open file:%s for parsing", filename); - content->parse_errors.push_back(error_message); } if (content->parse_errors.size() == 0) diff --git a/src/clib/lib/enkf/gen_kw_config.cpp b/src/clib/lib/enkf/gen_kw_config.cpp index 9d657119570..7e2478fb0e8 100644 --- a/src/clib/lib/enkf/gen_kw_config.cpp +++ b/src/clib/lib/enkf/gen_kw_config.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -9,6 +10,7 @@ #include #include +#include #include #include @@ -17,6 +19,7 @@ #include namespace fs = std::filesystem; +static auto logger = ert::get_logger("gen_kw_config"); typedef struct { char *name; @@ -102,6 +105,19 @@ void gen_kw_config_set_parameter_file(gen_kw_config_type *config, config_content_type *content = config_parse(parser, parameter_file, "--", NULL, NULL, NULL, CONFIG_UNRECOGNIZED_ADD, false); + if (!content->valid) { + auto header = fmt::format( + "encountered errors while parsing GEN_KW parameter file {}", + parameter_file); + std::string errors; + for (auto &error : content->parse_errors) { + errors += error; + } + logger->warning("{}\n{}", header, errors); + } + for (auto parse_error : content->parse_errors) { + logger->warning(parse_error); + } for (int item_index = 0; item_index < config_content_get_size(content); item_index++) { const config_content_node_type *node = diff --git a/src/ert/_c_wrappers/enkf/enkf_main.py b/src/ert/_c_wrappers/enkf/enkf_main.py index 0156700c970..ae1dd187f50 100644 --- a/src/ert/_c_wrappers/enkf/enkf_main.py +++ b/src/ert/_c_wrappers/enkf/enkf_main.py @@ -488,9 +488,15 @@ def sample_prior( grid_file = self.ensembleConfig().grid_file assert grid_file is not None grid = xtgeo.grid_from_file(grid_file) - props = xtgeo.gridproperty_from_file( - init_file, name=parameter, grid=grid - ) + try: + props = xtgeo.gridproperty_from_file( + init_file, name=parameter, grid=grid + ) + except PermissionError as err: + context_message = ( + f"Failed to open init file for parameter {parameter!r}" + ) + raise RuntimeError(context_message) from err data = props.values1d.data field_config = config_node.getFieldModelConfig() diff --git a/src/ert/_c_wrappers/enkf/enkf_obs.py b/src/ert/_c_wrappers/enkf/enkf_obs.py index 66fc66807ff..3f76ba2c727 100644 --- a/src/ert/_c_wrappers/enkf/enkf_obs.py +++ b/src/ert/_c_wrappers/enkf/enkf_obs.py @@ -1,3 +1,4 @@ +import os from typing import Iterator, List, Optional, Union from cwrap import BaseCClass @@ -112,6 +113,11 @@ def free(self): self._free() def load(self, config_file: str, std_cutoff: float) -> None: + if not os.access(config_file, os.R_OK): + raise RuntimeError( + "Do not have permission to open observation " + f"config file {config_file!r}" + ) _clib.enkf_obs.load(self, config_file, std_cutoff) @property diff --git a/src/ert/ensemble_evaluator/_wait_for_evaluator.py b/src/ert/ensemble_evaluator/_wait_for_evaluator.py index 102621c3afd..b7a3e63a98e 100644 --- a/src/ert/ensemble_evaluator/_wait_for_evaluator.py +++ b/src/ert/ensemble_evaluator/_wait_for_evaluator.py @@ -8,6 +8,8 @@ logger = logging.getLogger(__name__) +WAIT_FOR_EVALUATOR_TIMEOUT = 60 + def get_ssl_context(cert: Optional[Union[str, bytes]]) -> Optional[ssl.SSLContext]: if cert is None: @@ -41,9 +43,11 @@ async def wait_for_evaluator( # pylint: disable=too-many-arguments token: Optional[str] = None, cert: Optional[Union[str, bytes]] = None, healthcheck_endpoint: str = "/healthcheck", - timeout: float = 60, + timeout: Optional[float] = None, connection_timeout: float = 2, ) -> None: + if timeout is None: + timeout = WAIT_FOR_EVALUATOR_TIMEOUT healthcheck_url = base_url + healthcheck_endpoint start = time.time() sleep_time = 0.2 diff --git a/tests/unit_tests/cli/test_integration_cli.py b/tests/unit_tests/cli/test_integration_cli.py index 8d1621955c3..6065c8a6443 100644 --- a/tests/unit_tests/cli/test_integration_cli.py +++ b/tests/unit_tests/cli/test_integration_cli.py @@ -1,4 +1,5 @@ import asyncio +import logging import os import shutil import threading @@ -9,7 +10,7 @@ import pytest import ert.shared -from ert import LibresFacade +from ert import LibresFacade, ensemble_evaluator from ert.__main__ import ert_parser from ert._c_wrappers.config.config_parser import ConfigValidationError from ert._c_wrappers.enkf import EnKFMain, ErtConfig @@ -20,7 +21,7 @@ ITERATIVE_ENSEMBLE_SMOOTHER_MODE, TEST_RUN_MODE, ) -from ert.cli.main import run_cli +from ert.cli.main import ErtCliError, run_cli from ert.shared.feature_toggling import FeatureToggling from ert.storage import open_storage @@ -386,3 +387,104 @@ def test_that_prior_is_not_overwritten_in_ensemble_experiment( else: pd.testing.assert_frame_equal(parameter_values, prior_values) storage.close() + + +def test_config_parser_fails_gracefully_on_unreadable_config_file(copy_case, caplog): + """we cannot test on the config file directly, as the argument parser already check + if the file is readable. so we use the GEN_KW parameter file which is also parsed + using our config parser.""" + + copy_case("snake_oil_field") + config_file_name = "snake_oil_surface.ert" + + with open(config_file_name, mode="r", encoding="utf-8") as config_file_handler: + content_lines = config_file_handler.read().splitlines() + + index_line_with_gen_kw = [ + index for index, line in enumerate(content_lines) if line.startswith("GEN_KW") + ][0] + gen_kw_parameter_file = content_lines[index_line_with_gen_kw].split(" ")[4] + os.chmod(gen_kw_parameter_file, 0x0) + gen_kw_parameter_file_abs_path = os.path.join(os.getcwd(), gen_kw_parameter_file) + caplog.set_level(logging.WARNING) + + ErtConfig.from_file(config_file_name) + + assert ( + f"could not open file `{gen_kw_parameter_file_abs_path}` for parsing" + in caplog.text + ) + + +def test_field_init_file_not_readable(copy_case, monkeypatch): + monkeypatch.setattr( + ensemble_evaluator._wait_for_evaluator, "WAIT_FOR_EVALUATOR_TIMEOUT", 5 + ) + copy_case("snake_oil_field") + config_file_name = "snake_oil_field.ert" + field_file_rel_path = "fields/permx0.grdecl" + os.chmod(field_file_rel_path, 0x0) + + try: + run_ert_test_run(config_file_name) + except ErtCliError as err: + assert "Failed to open init file for parameter 'PERMX'" in str(err) + + +def test_surface_init_fails_during_forward_model_callback(copy_case): + copy_case("snake_oil_field") + config_file_name = "snake_oil_surface.ert" + parameter_name = "TOP" + with open(config_file_name, mode="r+", encoding="utf-8") as config_file_handler: + content_lines = config_file_handler.read().splitlines() + index_line_with_surface_top = [ + index + for index, line in enumerate(content_lines) + if line.startswith(f"SURFACE {parameter_name}") + ][0] + line_with_surface_top = content_lines[index_line_with_surface_top] + breaking_line_with_surface_top = line_with_surface_top + " FORWARD_INIT:True" + content_lines[index_line_with_surface_top] = breaking_line_with_surface_top + config_file_handler.seek(0) + config_file_handler.write("\n".join(content_lines)) + + try: + run_ert_test_run(config_file_name) + except ErtCliError as err: + assert f"Failed to initialize parameter {parameter_name!r}" in str(err) + + +def test_unopenable_observation_config_fails_gracefully(copy_case): + copy_case("snake_oil_field") + config_file_name = "snake_oil_field.ert" + with open(config_file_name, mode="r", encoding="utf-8") as config_file_handler: + content_lines = config_file_handler.read().splitlines() + index_line_with_observation_config = [ + index + for index, line in enumerate(content_lines) + if line.startswith("OBS_CONFIG") + ][0] + line_with_observation_config = content_lines[index_line_with_observation_config] + observation_config_rel_path = line_with_observation_config.split(" ")[1] + observation_config_abs_path = os.path.join(os.getcwd(), observation_config_rel_path) + os.chmod(observation_config_abs_path, 0x0) + + try: + run_ert_test_run(config_file_name) + except RuntimeError as err: + assert ( + "Do not have permission to open observation config file " + f"{observation_config_abs_path!r}" in str(err) + ) + + +def run_ert_test_run(config_file: str) -> None: + parser = ArgumentParser(prog="test_run") + parsed = ert_parser( + parser, + [ + TEST_RUN_MODE, + config_file, + ], + ) + run_cli(parsed)