testsAndMisc/python_pkg/live_calc/tests/test_calc_eval.py
Krzysztof kuhy Rudnicki 2e1e370c0f refactor: extract all inline Python from shell scripts into proper .py files
Move every multi-line python heredoc/`-c` block into a dedicated .py file so
ruff, mypy, pylint, bandit, and pytest can apply to it:
- linux_configuration/zsh/calc-live.zsh → python_pkg/live_calc/calc_eval.py (100% branch cov, 46 tests)
- meta/scripts/check_ai_evidence.sh → meta/scripts/validate_evidence.py
- meta/scripts/check_agent_contract.sh → meta/scripts/validate_contract.py
- phone_focus_mode/lib/monitor.sh → phone_focus_mode/lib/monitor_report.py
- phone_focus_mode/deploy.sh → phone_focus_mode/strip_workout_hosts.py
- linux_configuration/.../analyze_repo.sh → fast_count.py

Also: add zsh-syntax pre-commit hook (zsh -n); exclude zsh from shellcheck;
add tests for all 4 non-python_pkg helpers; update CLAUDE.md Shell Style with
the no-inline-Python rule.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 10:31:48 +02:00

210 lines
7.0 KiB
Python

"""Tests for the live-calc safe arithmetic evaluator.
The public surface is ``evaluate``; most branches are reached through it so the
tests double as behaviour documentation. A handful of internal helpers
(``_format``, ``_apply_limits``, ``_raise_timeout``, ``main``) are exercised
directly where a branch cannot be triggered through ``evaluate`` alone.
"""
from __future__ import annotations
import math
from unittest.mock import patch
import pytest
from python_pkg.live_calc import calc_eval
from python_pkg.live_calc.calc_eval import (
_apply_limits,
_format,
_raise_timeout,
evaluate,
main,
)
_MOD = "python_pkg.live_calc.calc_eval"
class TestArithmetic:
"""Core arithmetic, operators and the calculator-style ``^`` power."""
@pytest.mark.parametrize(
("expr", "expected"),
[
("2+2", "4"),
("20*(3+4)", "140"),
("10-3", "7"),
("7/2", "3.5"),
("7//2", "3"),
("10%3", "1"),
("2^10", "1024"), # ^ is rewritten to **
("2**10", "1024"),
("+5", "5"),
("-5", "-5"),
("3/2", "1.5"),
],
)
def test_evaluates(self, expr: str, expected: str) -> None:
"""Each operator yields the expected formatted result."""
assert evaluate(expr) == expected
class TestFunctionsAndConstants:
"""Whitelisted functions and named constants."""
def test_sqrt(self) -> None:
"""sqrt(4) is 2."""
assert evaluate("sqrt(4)") == "2"
def test_nested_call(self) -> None:
"""Functions compose and trig works against pi."""
assert evaluate("sin(pi/2)") == "1"
def test_constant_pi(self) -> None:
"""The constant pi resolves to math.pi."""
assert evaluate("pi") == format(math.pi, ".12g")
def test_min_max(self) -> None:
"""Multi-argument builtins are allowed."""
assert evaluate("max(2, 9, 4)") == "9"
def test_factorial_ok(self) -> None:
"""A small factorial is computed."""
assert evaluate("factorial(5)") == "120"
class TestRejected:
"""Inputs that must yield '' (invalid, unsafe, or unsupported)."""
@pytest.mark.parametrize(
"expr",
[
"", # empty
"ls -la", # not an expression at all
"2+", # SyntaxError
"x", # unknown name
"sqrt", # function name without a call
"foo(2)", # unknown function
"True", # bool literal rejected
"'a'", # string literal rejected
"5 & 3", # unsupported binary operator
"~5", # unsupported unary operator
"(1+1)(2)", # call target is not a plain name
"round(2.5, ndigits=1)", # keyword arguments rejected
"[1, 2]", # unsupported node type (list)
"factorial()", # factorial needs an argument
"factorial(2.5)", # factorial argument must be int
],
)
def test_returns_empty(self, expr: str) -> None:
"""Anything that is not a permitted arithmetic expression yields ''."""
assert evaluate(expr) == ""
class TestRunawayGuards:
"""Bounds that stop a live keystroke from computing forever."""
def test_huge_exponent_refused(self) -> None:
"""A ** b with an enormous b is refused before computing."""
assert evaluate("2^99999") == ""
def test_huge_factorial_refused(self) -> None:
"""factorial of a huge argument is refused up front."""
assert evaluate("factorial(99999)") == ""
class TestFormatting:
"""Number formatting, including the int/scientific/float branches."""
def test_big_int_uses_scientific(self) -> None:
"""Integers longer than the digit cap fall back to scientific form."""
assert evaluate("10^30") == format(1e30, ".6g")
def test_overflowing_int_yields_empty(self) -> None:
"""An int too large to convert to float formats to ''."""
assert evaluate("10^400") == ""
def test_infinity_yields_empty(self) -> None:
"""A float overflow to infinity formats to ''."""
assert evaluate("1e308*10") == ""
def test_format_bool_is_int(self) -> None:
"""_format coerces bool to its int value (reached only directly)."""
assert _format(value=True) == "1"
def test_format_nan_yields_empty(self) -> None:
"""_format rejects NaN."""
assert _format(float("nan")) == ""
def test_format_inf_yields_empty(self) -> None:
"""_format rejects infinity."""
assert _format(float("inf")) == ""
def test_format_plain_float(self) -> None:
"""A finite float is formatted with the float precision."""
assert _format(1.5) == "1.5"
class TestTimeoutHandler:
"""The SIGALRM handler and the resource-limit installer."""
def test_raise_timeout_raises(self) -> None:
"""The handler converts an alarm into a catchable TimeoutError."""
with pytest.raises(TimeoutError):
_raise_timeout(0, None)
def test_apply_limits_installs(self) -> None:
"""Limits are installed via setrlimit/signal/setitimer."""
with (
patch(f"{_MOD}.resource.setrlimit") as set_rlimit,
patch(f"{_MOD}.signal.signal") as set_signal,
patch(f"{_MOD}.signal.setitimer") as set_timer,
):
_apply_limits()
set_rlimit.assert_called_once()
set_signal.assert_called_once()
set_timer.assert_called_once()
def test_apply_limits_survives_unavailable(self) -> None:
"""If the platform rejects the limits, the error is swallowed."""
with (
patch(f"{_MOD}.resource.setrlimit", side_effect=OSError),
patch(f"{_MOD}.signal.signal", side_effect=ValueError),
patch(f"{_MOD}.signal.setitimer"),
):
_apply_limits() # must not raise
class TestMain:
"""The command-line entry point (argv -> stdout)."""
def test_no_argument(self, capsys: pytest.CaptureFixture[str]) -> None:
"""With no expression argument, nothing is printed and rc is 0."""
with (
patch(f"{_MOD}._apply_limits"),
patch.object(calc_eval.sys, "argv", ["calc_eval"]),
):
assert main() == 0
assert capsys.readouterr().out == ""
def test_valid_expression(self, capsys: pytest.CaptureFixture[str]) -> None:
"""A valid expression is evaluated and written to stdout."""
with (
patch(f"{_MOD}._apply_limits"),
patch.object(calc_eval.sys, "argv", ["calc_eval", "2+2"]),
):
assert main() == 0
assert capsys.readouterr().out == "4"
def test_invalid_expression_writes_nothing(
self,
capsys: pytest.CaptureFixture[str],
) -> None:
"""An invalid expression produces no output."""
with (
patch(f"{_MOD}._apply_limits"),
patch.object(calc_eval.sys, "argv", ["calc_eval", "ls -la"]),
):
assert main() == 0
assert capsys.readouterr().out == ""