mcp-calculator/tests/test_parsing.py
2026-09-15 20:38:16 +02:00

117 lines
3.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sympy as sp
from mcp_calculator.config import Limits
from mcp_calculator.ops.context import MathError, set_limits
from mcp_calculator.ops.parsing import apply_assumptions, parse, parse_symbols
from tests.support import MathTestCase
x, y = sp.symbols("x y")
VALID = [
("2x + 3(x+1)", 5 * x + 3),
("x^2", x**2),
("x**2", x**2),
("0.1 + 0.2", sp.Rational(3, 10)),
("5!", 120),
("sin(30*deg)", sp.Rational(1, 2)),
("e^(I*pi)", -1),
("ln(e)", 1),
("log(8, 2)", 3),
("log10(1000)", 3),
("abs(-3)", 3),
("max(3, 7)", 7),
("10 % 3", 1),
("1e5", 100000),
("2.5e-3", sp.Rational(1, 400)),
("√8", 2 * sp.sqrt(2)),
("3×4÷2", 6),
("2π", 2 * sp.pi),
("x²", x**2),
("arcsin(1)", sp.pi / 2),
("inf", sp.oo),
]
MALICIOUS = [
"__import__('os').system('ls')",
"().__class__.__bases__",
"x.subs(x, 1)",
"lambda x: x",
"open('/etc/passwd')",
"import os",
"getattr(x, 'y')",
"[1, 2, 3]",
"Symbol('x')",
"_x + 1",
"a; b",
"exec\n('1')",
"{1: 2}",
"x if x else y",
"`ls`",
"\\x41",
]
class ParseTest(MathTestCase):
def test_valid_expressions(self):
for text, expected in VALID:
with self.subTest(text=text):
result = parse(text)
self.assertTrue(result == expected or sp.simplify(result - expected) == 0, f"{text!r} -> {result}")
def test_relations(self):
self.assertEqual(parse("x^2 = 4"), sp.Eq(x**2, 4))
self.assertEqual(parse("x^2 == 4"), sp.Eq(x**2, 4))
self.assertEqual(parse("x != 3"), sp.Ne(x, 3))
self.assertEqual(parse("x <= 3"), sp.Le(x, 3))
self.assertEqual(parse("x^2 > 4"), sp.Gt(x**2, 4))
def test_only_one_equals_sign(self):
self.assertEqual(parse("x = 1"), sp.Eq(x, 1))
with self.assertRaisesRegex(MathError, "only one '='"):
parse("x = 1 = 2")
def test_multi_letter_names_are_single_symbols(self):
rate, time = sp.symbols("rate time")
self.assertEqual(parse("rate*time"), rate * time)
def test_exact_false_keeps_floats(self):
self.assertIsInstance(parse("0.5", exact=False), sp.Float)
def test_empty_expression(self):
with self.assertRaisesRegex(MathError, "empty"):
parse(" ")
def test_syntax_error_message(self):
with self.assertRaisesRegex(MathError, "syntax error|could not parse"):
parse("2 +* 3")
def test_length_limit(self):
set_limits(Limits(max_input_length=10))
with self.assertRaisesRegex(MathError, "longer than 10"):
parse("1+1+1+1+1+1+1")
class SecurityTest(MathTestCase):
def test_code_is_rejected(self):
for text in MALICIOUS:
with self.subTest(text=text), self.assertRaises(MathError):
parse(text)
def test_python_builtins_are_not_reachable(self):
# Unknown names become plain symbols; they never resolve to Python builtins.
self.assertEqual(parse("eval"), sp.Symbol("eval"))
self.assertEqual(parse("exec"), sp.Symbol("exec"))
with self.assertRaises(MathError):
parse("globals()")
class SymbolTest(MathTestCase):
def test_parse_symbols(self):
self.assertEqual(parse_symbols("x, y"), [x, y])
self.assertEqual(parse_symbols(["x"]), [x])
with self.assertRaises(MathError):
parse_symbols(["sin"])
def test_apply_assumptions(self):
expr, mapping = apply_assumptions(sp.sqrt(x**2), "positive")
self.assertEqual(sp.simplify(expr), mapping[x])