diff --git a/src/ert/config/_read_summary.py b/src/ert/config/_read_summary.py index a07be9ff112..3a2b8dc24ae 100644 --- a/src/ert/config/_read_summary.py +++ b/src/ert/config/_read_summary.py @@ -61,7 +61,7 @@ def from_keyword(cls, summary_keyword: str) -> _SummaryType: "W": cls.WELL, } if summary_keyword == "": - raise ValueError("Got empty summary_keyword") + raise ValueError("Got empty summary keyword") if any(special in summary_keyword for special in SPECIAL_KEYWORDS): return cls.OTHER if summary_keyword[0] in KEYWORD_TYPE_MAPPING: @@ -200,7 +200,7 @@ def _find_file_matching( raise ValueError(f"Could not find any {kind} matching case path {case}") if len(candidates) > 1: raise ValueError( - f"Ambigous reference to {kind} in {case}, could be any of {candidates}" + f"Ambiguous reference to {kind} in {case}, could be any of {candidates}" ) return os.path.join(dir, candidates[0]) @@ -215,10 +215,13 @@ def read_summary( filepath: str, fetch_keys: Sequence[str] ) -> Tuple[List[str], Sequence[datetime], Any]: summary, spec = _get_summary_filenames(filepath) - date_index, start_date, date_units, keys, indices = _read_spec(spec, fetch_keys) - fetched, time_map = _read_summary( - summary, start_date, date_units, indices, date_index - ) + try: + date_index, start_date, date_units, keys, indices = _read_spec(spec, fetch_keys) + fetched, time_map = _read_summary( + summary, start_date, date_units, indices, date_index + ) + except resfo.ResfoParsingError as err: + raise ValueError(f"Failed to read summary file {filepath}: {err}") from err return (keys, time_map, fetched) @@ -228,6 +231,14 @@ def _key2str(key: Union[bytes, str]) -> str: return ret.strip() +def _check_vals( + kw: str, spec: str, vals: Union[npt.NDArray[Any], resfo.MESS] +) -> npt.NDArray[Any]: + if vals is resfo.MESS or isinstance(vals, resfo.MESS): + raise ValueError(f"{kw.strip()} in {spec} has incorrect type MESS") + return vals + + def _read_spec( spec: str, fetch_keys: Sequence[str] ) -> Tuple[int, datetime, DateUnit, List[str], npt.NDArray[np.int32]]: @@ -274,22 +285,15 @@ def _read_spec( break kw = entry.read_keyword() if kw in arrays: - vals = entry.read_array() - if vals is resfo.MESS or isinstance(vals, resfo.MESS): - raise ValueError(f"{kw} in {spec} was MESS") - arrays[kw] = vals + arrays[kw] = _check_vals(kw, spec, entry.read_array()) if kw == "DIMENS ": - vals = entry.read_array() - if vals is resfo.MESS or isinstance(vals, resfo.MESS): - raise ValueError(f"DIMENS in {spec} was MESS") + vals = _check_vals(kw, spec, entry.read_array()) size = len(vals) n = vals[0] if size > 0 else None nx = vals[1] if size > 1 else None ny = vals[2] if size > 2 else None if kw == "STARTDAT": - vals = entry.read_array() - if vals is resfo.MESS or isinstance(vals, resfo.MESS): - raise ValueError(f"Startdate in {spec} was MESS") + vals = _check_vals(kw, spec, entry.read_array()) size = len(vals) day = vals[0] if size > 0 else 0 month = vals[1] if size > 1 else 0 @@ -297,15 +301,20 @@ def _read_spec( hour = vals[3] if size > 3 else 0 minute = vals[4] if size > 4 else 0 microsecond = vals[5] if size > 5 else 0 - date = datetime( - day=day, - month=month, - year=year, - hour=hour, - minute=minute, - second=microsecond // 10**6, - microsecond=microsecond % 10**6, - ) + try: + date = datetime( + day=day, + month=month, + year=year, + hour=hour, + minute=minute, + second=microsecond // 10**6, + microsecond=microsecond % 10**6, + ) + except Exception as err: + raise ValueError( + f"SMSPEC {spec} contains invalid STARTDAT: {err}" + ) from err keywords = arrays["KEYWORDS"] wgnames = arrays["WGNAMES "] nums = arrays["NUMS "] @@ -315,9 +324,9 @@ def _read_spec( lgr_names = arrays["LGRNAMES"] if date is None: - raise ValueError(f"keyword startdat missing in {spec}") + raise ValueError(f"Keyword startdat missing in {spec}") if keywords is None: - raise ValueError(f"keywords missing in {spec}") + raise ValueError(f"Keywords missing in {spec}") if n is None: n = len(keywords) @@ -367,16 +376,22 @@ def optional_get(arr: Optional[npt.NDArray[Any]], idx: int) -> Any: units = arrays["UNITS "] if units is None: - raise ValueError(f"keyword units missing in {spec}") + raise ValueError(f"Keyword units missing in {spec}") if date_index is None: raise ValueError(f"KEYWORDS did not contain TIME in {spec}") if date_index >= len(units): raise ValueError(f"Unit missing for TIME in {spec}") + unit_key = _key2str(units[date_index]) + try: + date_unit = DateUnit[unit_key] + except KeyError: + raise ValueError(f"Unknown date unit in {spec}: {unit_key}") from None + return ( date_index, date, - DateUnit[_key2str(units[date_index])], + date_unit, list(keys_array), indices_array, ) @@ -403,9 +418,7 @@ def _read_summary( def read_params() -> None: nonlocal last_params, values if last_params is not None: - vals = last_params.read_array() - if vals is resfo.MESS or isinstance(vals, resfo.MESS): - raise ValueError(f"PARAMS in {summary} was MESS") + vals = _check_vals("PARAMS", summary, last_params.read_array()) values.append(vals[indices]) dates.append(start_date + unit.make_delta(float(vals[date_index]))) last_params = None diff --git a/tests/unit_tests/config/config_dict_generator.py b/tests/unit_tests/config/config_dict_generator.py index c6026767dda..5a824a6a647 100644 --- a/tests/unit_tests/config/config_dict_generator.py +++ b/tests/unit_tests/config/config_dict_generator.py @@ -394,6 +394,7 @@ def ert_config_values(draw, use_eclbase=booleans): smspecs( sum_keys=st.just(sum_keys), start_date=st.just(Date.from_datetime(first_date)), + use_days=st.just(True), ) ) std_cutoff = draw(small_floats) diff --git a/tests/unit_tests/config/summary_generator.py b/tests/unit_tests/config/summary_generator.py index bf796fc35c4..832d34f58f9 100644 --- a/tests/unit_tests/config/summary_generator.py +++ b/tests/unit_tests/config/summary_generator.py @@ -281,15 +281,12 @@ def to_file(self, filelike, file_format: resfo.Format = resfo.Format.UNFORMATTED @st.composite -def smspecs( - draw, - sum_keys, - start_date, -): +def smspecs(draw, sum_keys, start_date, use_days=None): """ Strategy for smspec that ensures that the TIME parameter, as required by ert, is in the parameters list. """ + use_days = st.booleans() if use_days is None else use_days use_locals = draw(st.booleans()) sum_keys = draw(sum_keys) n = len(sum_keys) + 1 @@ -297,7 +294,14 @@ def smspecs( ny = draw(small_ints) nz = draw(small_ints) keywords = ["TIME "] + sum_keys - units = ["DAYS "] + draw(st.lists(unit_names, min_size=n - 1, max_size=n - 1)) + if draw(use_days): + units = ["DAYS "] + draw( + st.lists(unit_names, min_size=n - 1, max_size=n - 1) + ) + else: + units = ["HOURS "] + draw( + st.lists(unit_names, min_size=n - 1, max_size=n - 1) + ) well_names = [":+:+:+:+"] + draw(st.lists(names, min_size=n - 1, max_size=n - 1)) if use_locals: # use local lgrs = draw(st.lists(names, min_size=n, max_size=n)) diff --git a/tests/unit_tests/config/test_read_summary.py b/tests/unit_tests/config/test_read_summary.py index 7b99c03d950..5378640337c 100644 --- a/tests/unit_tests/config/test_read_summary.py +++ b/tests/unit_tests/config/test_read_summary.py @@ -3,9 +3,9 @@ import hypothesis.strategies as st import pytest +import resfo from hypothesis import given from resdata.summary import Summary, SummaryVarType -from resfo import Format from ert.config._read_summary import _SummaryType, make_summary_key, read_summary @@ -192,12 +192,12 @@ def test_completion_summary_format_have_cell_index_and_name( ) -@given(summaries(), st.sampled_from(Format)) +@given(summaries(), st.sampled_from(resfo.Format)) def test_that_reading_summaries_returns_the_contents_of_the_file( tmp_path_factory, summary, format ): tmp_path = tmp_path_factory.mktemp("summary") - format_specifier = "F" if format == Format.FORMATTED else "" + format_specifier = "F" if format == resfo.Format.FORMATTED else "" smspec, unsmry = summary unsmry.to_file(tmp_path / f"TEST.{format_specifier}UNSMRY", format) smspec.to_file(tmp_path / f"TEST.{format_specifier}SMSPEC", format) @@ -252,39 +252,106 @@ def to_date(start_date: datetime, offset: float, unit: str) -> datetime: @pytest.mark.parametrize( - "spec_contents, smry_contents", + "spec_contents, smry_contents, error_message", [ - (b"", b""), - (b"1", b"1"), - (b"\x00\x00\x00\x10", b"1"), - (b"\x00\x00\x00\x10UNEXPECTED", b"\x00\x00\x00\x10UNEXPECTED"), + (b"", b"", "Keyword startdat missing"), + (b"1", b"1", "Failed to read summary file"), ( - b"\x00\x00\x00\x10UNEXPECTED", - b"\x00\x00\x00\x10KEYWORD1" + (2200).to_bytes(4, byteorder="big"), - ), - ( - b"\x00\x00\x00\x10FOOOOOOO\x00", - b"\x00\x00\x00\x10FOOOOOOO" - + (2300).to_bytes(4, byteorder="big") - + b"INTE\x00\x00\x00\x10" - + b"\x00" * (4 * 2300 + 4 * 6), + b"\x00\x00\x00\x10FOOOOOOO\x00\x00\x00\x01" + + b"INTE" + + b"\x00\x00\x00\x10" + + (4).to_bytes(4, signed=True, byteorder="big") + + b"\x00" * 4, + b"", + "Keyword startdat missing", ), ( - b"\x00\x00\x00\x10FOOOOOOO\x00\x00\x00\x01" + b"\x00\x00\x00\x10STARTDAT\x00\x00\x00\x01" + b"INTE" + b"\x00\x00\x00\x10" + (4).to_bytes(4, signed=True, byteorder="big") + b"\x00" * 4 + (4).to_bytes(4, signed=True, byteorder="big"), - b"\x00\x00\x00\x10FOOOOOOO\x00", + b"", + "contains invalid STARTDAT", ), ], ) def test_that_incorrect_summary_files_raises_informative_errors( - smry_contents, spec_contents, tmp_path + smry_contents, spec_contents, error_message, tmp_path ): (tmp_path / "test.UNSMRY").write_bytes(smry_contents) (tmp_path / "test.SMSPEC").write_bytes(spec_contents) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=error_message): + read_summary(str(tmp_path / "test"), ["*"]) + + +def test_mess_values_in_summary_files_raises_informative_errors(tmp_path): + resfo.write(tmp_path / "test.SMSPEC", [("KEYWORDS", resfo.MESS)]) + (tmp_path / "test.UNSMRY").write_bytes(b"") + + with pytest.raises(ValueError, match="has incorrect type MESS"): + read_summary(str(tmp_path / "test"), ["*"]) + + +def test_empty_keywords_in_summary_files_raises_informative_errors(tmp_path): + resfo.write( + tmp_path / "test.SMSPEC", + [("STARTDAT", [31, 12, 2012, 00]), ("KEYWORDS", [" "])], + ) + (tmp_path / "test.UNSMRY").write_bytes(b"") + + with pytest.raises(ValueError, match="Got empty summary keyword"): + read_summary(str(tmp_path / "test"), ["*"]) + + +def test_missing_names_keywords_in_summary_files_raises_informative_errors( + tmp_path, +): + resfo.write( + tmp_path / "test.SMSPEC", + [("STARTDAT", [31, 12, 2012, 00]), ("KEYWORDS", ["BART "])], + ) + (tmp_path / "test.UNSMRY").write_bytes(b"") + + with pytest.raises( + ValueError, + match="Found block keyword in summary specification without dimens keyword", + ): + read_summary(str(tmp_path / "test"), ["*"]) + + +def test_unknown_date_unit_in_summary_files_raises_informative_errors( + tmp_path, +): + resfo.write( + tmp_path / "test.SMSPEC", + [ + ("STARTDAT", [31, 12, 2012, 00]), + ("KEYWORDS", ["TIME "]), + ("UNITS ", ["ANNUAL "]), + ], + ) + (tmp_path / "test.UNSMRY").write_bytes(b"") + + with pytest.raises( + ValueError, + match="Unknown date unit .* ANNUAL", + ): + read_summary(str(tmp_path / "test"), ["*"]) + + +def test_that_ambiguous_case_restart_raises_an_informative_error( + tmp_path, +): + (tmp_path / "test.UNSMRY").write_bytes(b"") + (tmp_path / "test.FUNSMRY").write_bytes(b"") + (tmp_path / "test.smspec").write_bytes(b"") + (tmp_path / "test.Smspec").write_bytes(b"") + + with pytest.raises( + ValueError, + match="Ambiguous reference to unified summary", + ): read_summary(str(tmp_path / "test"), ["*"])