From 2465739c7014098b8d39dc6ea3a4883bbd115253 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:46:34 +0000 Subject: [PATCH] Raise ParseError for truncated stoch plane arrays Closes #145 Co-Authored-By: Claude Opus 5.5 --- source_modelling/stoch.py | 54 +++++++++++++++++++++++++++------------ tests/test_stoch.py | 15 +++++++++++ 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/source_modelling/stoch.py b/source_modelling/stoch.py index 0952e3b..1c7dbbe 100644 --- a/source_modelling/stoch.py +++ b/source_modelling/stoch.py @@ -117,6 +117,38 @@ def _read_stoch_header(handle: TextIO) -> StochHeader: ) +def _read_array(handle: TextIO, header: StochHeader, label: str) -> FloatArray2D: + """ + Read a single (ny, nx) data array of a stochastic plane. + + Parameters + ---------- + handle : TextIO + Text file handle positioned at the start of the array. + header : StochHeader + Header of the plane, giving the array dimensions. + label : str + Name of the array being read, used in error messages. + + Returns + ------- + FloatArray2D + The array, with shape (ny, nx). + + Raises + ------ + parse_utils.ParseError + If fewer than nx * ny values could be read. + """ + n = header.nx * header.ny + array = np.fromfile(handle, dtype=np.float32, sep=" ", count=n) + if array.size != n: + raise parse_utils.ParseError( + f"Expected {n} values for {label}, got {array.size}" + ) + return cast(FloatArray2D, array.reshape((header.ny, header.nx))) + + def _read_stoch_plane(handle: TextIO) -> StochPlane: """ Read a complete stochastic plane from a file. @@ -132,20 +164,9 @@ def _read_stoch_plane(handle: TextIO) -> StochPlane: Named tuple containing the plane's header and data arrays (slip, rise, trup). """ header = _read_stoch_header(handle) - slip_array = np.fromfile( - handle, dtype=np.float32, sep=" ", count=header.nx * header.ny - ).reshape((header.ny, header.nx)) - slip = cast(FloatArray2D, slip_array) - - rise_array = np.fromfile( - handle, dtype=np.float32, sep=" ", count=header.nx * header.ny - ).reshape((header.ny, header.nx)) - rise = cast(FloatArray2D, rise_array) - - trup_array = np.fromfile( - handle, dtype=np.float32, sep=" ", count=header.nx * header.ny - ).reshape((header.ny, header.nx)) - trup = cast(FloatArray2D, trup_array) + slip = _read_array(handle, header, "slip") + rise = _read_array(handle, header, "rise") + trup = _read_array(handle, header, "trup") return StochPlane(header, slip, rise, trup) @@ -192,8 +213,9 @@ def from_file(cls, filename: Path) -> Self: Raises ------ - ValueError - If the number of planes specified in the file is not a positive integer. + parse_utils.ParseError + If the number of planes specified in the file is not a positive + integer, or if the file contains truncated or malformed data. """ filename = Path(filename) with open(filename, "r") as handle: diff --git a/tests/test_stoch.py b/tests/test_stoch.py index 42ca856..5dc1d97 100644 --- a/tests/test_stoch.py +++ b/tests/test_stoch.py @@ -327,6 +327,21 @@ def test_stoch_file_truncated_raises_instead_of_hanging(truncated_stoch_file: Pa assert "Unexpected end of file" in queue.get() +def test_stoch_file_truncated_array_raises_parse_error(tmp_path: Path): + """A stoch file truncated inside a plane's arrays should raise ParseError + naming the array, rather than a bare reshape ValueError.""" + stoch_filepath = tmp_path / "truncated_array_stoch_file" + stoch_filepath.write_text( + "1\n" + "174.5 -41.3 2 2 1.0 1.0 45 60 90 0.5 2.5 1.5\n" + "1.0 2.0 3.0 4.0\n" + "0.1 0.2 0.3 0.4\n" + "0.01 0.02 0.03\n" + ) + with pytest.raises(parse_utils.ParseError, match="Expected 4 values for trup"): + StochFile.from_file(stoch_filepath) + + def dump_and_reload(stoch_file: StochFile, tmp_path: Path) -> StochFile: """Dump a StochFile and read the result back in.""" dumped = tmp_path / "dumped.stoch"