diff --git a/src/conductor/duration.py b/src/conductor/duration.py index 1f70d377..4f12560e 100644 --- a/src/conductor/duration.py +++ b/src/conductor/duration.py @@ -14,6 +14,7 @@ from __future__ import annotations +import math import re _DURATION_PATTERN = re.compile(r"^\s*(?P\d+(?:\.\d+)?)\s*(?Pms|s|m|h)?\s*$") @@ -64,6 +65,8 @@ def parse_duration(value: str | int | float) -> float: raise ValueError(f"duration must be a number or duration string, not boolean: {value!r}") if isinstance(value, (int, float)): + if not math.isfinite(value): + raise ValueError(f"duration must be finite, got {value!r}") return float(value) if not isinstance(value, str): @@ -79,5 +82,7 @@ def parse_duration(value: str | int | float) -> float: ) number = float(match.group("value")) + if not math.isfinite(number): + raise ValueError(f"duration must be finite, got {value!r}") unit = match.group("unit") or "s" return number * _UNIT_TO_SECONDS[unit] diff --git a/tests/test_engine/test_duration.py b/tests/test_engine/test_duration.py index ce0038c8..7d4cb2a6 100644 --- a/tests/test_engine/test_duration.py +++ b/tests/test_engine/test_duration.py @@ -6,6 +6,8 @@ from __future__ import annotations +import math + import pytest from conductor.duration import parse_duration @@ -118,3 +120,12 @@ def test_list(self) -> None: def test_number_then_garbage(self) -> None: with pytest.raises(ValueError): parse_duration("5 minutes") + + @pytest.mark.parametrize("value", [math.nan, math.inf, -math.inf]) + def test_nonfinite_number(self, value: float) -> None: + with pytest.raises(ValueError, match="finite"): + parse_duration(value) + + def test_overflowing_string_number(self) -> None: + with pytest.raises(ValueError, match="finite"): + parse_duration("9" * 400)