Files
calculator/test_calculator.py

353 lines
12 KiB
Python

"""Unit-тесты для калькулятора.
Запуск:
python -m pytest test_calculator.py
python -m unittest test_calculator
"""
from __future__ import annotations
import math
import unittest
from calculator import (
CalcError,
DivisionByZeroError,
SyntaxError_,
is_exit_command,
parse_and_eval,
try_evaluate,
)
class TestBasicOperations(unittest.TestCase):
def test_addition(self):
self.assertAlmostEqual(parse_and_eval("2 + 3"), 5)
def test_subtraction(self):
self.assertAlmostEqual(parse_and_eval("10 - 4"), 6)
def test_multiplication(self):
self.assertAlmostEqual(parse_and_eval("6 * 7"), 42)
def test_division(self):
self.assertAlmostEqual(parse_and_eval("20 / 4"), 5)
def test_division_fraction(self):
self.assertAlmostEqual(parse_and_eval("1 / 4"), 0.25)
def test_negative_and_zero(self):
self.assertAlmostEqual(parse_and_eval("-5 + 5"), 0)
self.assertAlmostEqual(parse_and_eval("-5"), -5)
self.assertAlmostEqual(parse_and_eval("0 - 3"), -3)
def test_unary_signs(self):
self.assertAlmostEqual(parse_and_eval("+5"), 5)
self.assertAlmostEqual(parse_and_eval("3 + -2"), 1)
self.assertAlmostEqual(parse_and_eval("3 * -2"), -6)
class TestPrecedenceAndParentheses(unittest.TestCase):
def test_operator_precedence(self):
# + до *, без скобок: 2 + 3 * 4 = 14
self.assertAlmostEqual(parse_and_eval("2 + 3 * 4"), 14)
def test_parentheses_override(self):
self.assertAlmostEqual(parse_and_eval("(2 + 3) * 4"), 20)
def test_nested_parentheses(self):
self.assertAlmostEqual(parse_and_eval("((1 + 2) * (3 + 4))"), 21)
def test_left_to_right(self):
self.assertAlmostEqual(parse_and_eval("10 - 3 - 2"), 5)
self.assertAlmostEqual(parse_and_eval("20 / 4 / 5"), 1)
def test_chain_operations(self):
self.assertAlmostEqual(parse_and_eval("2 - 2 + 2"), 2)
class TestPercent(unittest.TestCase):
def test_percent(self):
# 50% -> 0.5
self.assertAlmostEqual(parse_and_eval("50 %"), 0.5)
def test_percent_in_term(self):
self.assertAlmostEqual(parse_and_eval("2 * 50 %"), 1.0)
def test_percent_then_plus(self):
# 10% применяется к 10 -> 0.1; затем 1 + 0.1 = 1.1
self.assertAlmostEqual(parse_and_eval("1 + 10 %"), 1.1)
class TestDivisionByZero(unittest.TestCase):
def test_divide_by_zero_raises(self):
with self.assertRaises(DivisionByZeroError):
parse_and_eval("1 / 0")
def test_divide_zero_by_zero_raises(self):
with self.assertRaises(DivisionByZeroError):
parse_and_eval("0 / 0")
def test_try_evaluate_reports_error(self):
self.assertIn("Ошибка", try_evaluate("1 / 0"))
# не падает exception, возвращает строку
self.assertIsInstance(try_evaluate("1 / 0"), str)
class TestSyntaxErrors(unittest.TestCase):
def test_empty_raises(self):
with self.assertRaises(SyntaxError_):
parse_and_eval("")
def test_trailing_operator_raises(self):
with self.assertRaises(SyntaxError_):
parse_and_eval("2 +")
def test_unbalanced_parentheses_raises(self):
with self.assertRaises(SyntaxError_):
parse_and_eval("(2 + 3")
def test_invalid_symbol_raises(self):
with self.assertRaises(SyntaxError_):
parse_and_eval("2 & 3")
def test_try_evaluate_invalid_symbol(self):
self.assertIn("Ошибка", try_evaluate("2 & 3"))
class TestExponent(unittest.TestCase):
def test_power(self):
self.assertAlmostEqual(parse_and_eval("2 ** 10"), 1024)
def test_right_associative(self):
self.assertAlmostEqual(parse_and_eval("2 ** 2 ** 3"), 256)
class TestExitCommands(unittest.TestCase):
def test_exit_commands(self):
for cmd in ("выход", "exit", " EXIT ", "Выход"):
self.assertTrue(is_exit_command(cmd), msg=cmd)
def test_non_exit(self):
for cmd in ("выходить", "exit!", "2 + 2", "exit exit"):
self.assertFalse(is_exit_command(cmd), msg=cmd)
class TestPhase0Regressions(unittest.TestCase):
def test_zero_number_atom(self):
# 0 как атом должен работать (не превращаться в 0.0 через `or`)
self.assertAlmostEqual(parse_and_eval("0"), 0.0)
self.assertAlmostEqual(parse_and_eval("0 * 5"), 0.0)
def test_unbalanced_paren_message(self):
with self.assertRaises(SyntaxError_) as ctx:
parse_and_eval("(2 + 3")
self.assertIn("')'", str(ctx.exception))
self.assertNotIn("'}'", str(ctx.exception))
def test_complex_result_raises_calc_error(self):
with self.assertRaises(CalcError):
parse_and_eval("(-8) ** 0.5")
def test_overflow_raises_calc_error(self):
with self.assertRaises(CalcError):
parse_and_eval("2 ** 100000")
def test_try_evaluate_never_raises_on_overflow(self):
self.assertIn("Ошибка", try_evaluate("2 ** 100000"))
class TestPhase1Exponents(unittest.TestCase):
def test_e_notation(self):
self.assertAlmostEqual(parse_and_eval("1e3"), 1000)
self.assertAlmostEqual(parse_and_eval("2.5E-4"), 0.00025)
self.assertAlmostEqual(parse_and_eval("1e3 + 1"), 1001)
def test_euler_constant(self):
self.assertAlmostEqual(parse_and_eval("e"), math.e)
self.assertAlmostEqual(parse_and_eval("2 * e"), 2 * math.e)
def test_digit_separators(self):
self.assertAlmostEqual(parse_and_eval("1_000_000"), 1_000_000)
self.assertAlmostEqual(parse_and_eval("1_0.5_5"), 10.55)
class TestPhase1Constants(unittest.TestCase):
def test_constants(self):
self.assertAlmostEqual(parse_and_eval("pi"), math.pi)
self.assertAlmostEqual(parse_and_eval("tau"), math.tau)
self.assertAlmostEqual(parse_and_eval("inf * 2"), math.inf)
def test_case_insensitive(self):
self.assertAlmostEqual(parse_and_eval("PI"), math.pi)
self.assertAlmostEqual(parse_and_eval("Sqrt(4)"), 2)
def test_unknown_identifier(self):
with self.assertRaises(SyntaxError_):
parse_and_eval("foo + 1")
class TestPhase1Functions(unittest.TestCase):
def test_basic(self):
self.assertAlmostEqual(parse_and_eval("sqrt(16)"), 4)
self.assertAlmostEqual(parse_and_eval("abs(-3)"), 3)
self.assertAlmostEqual(parse_and_eval("floor(2.7)"), 2)
self.assertAlmostEqual(parse_and_eval("ceil(2.1)"), 3)
self.assertAlmostEqual(parse_and_eval("round(2.567, 2)"), 2.57)
def test_logs_exp(self):
self.assertAlmostEqual(parse_and_eval("ln(e)"), 1)
self.assertAlmostEqual(parse_and_eval("log2(8)"), 3)
self.assertAlmostEqual(parse_and_eval("log10(1000)"), 3)
self.assertAlmostEqual(parse_and_eval("log(8, 2)"), 3)
self.assertAlmostEqual(parse_and_eval("exp(0)"), 1)
def test_trig(self):
self.assertAlmostEqual(parse_and_eval("sin(pi / 2)"), 1)
self.assertAlmostEqual(parse_and_eval("cos(0)"), 1)
self.assertAlmostEqual(parse_and_eval("atan(1) * 4"), math.pi)
def test_composition_precedence(self):
self.assertAlmostEqual(parse_and_eval("sqrt(16) + 1"), 5)
self.assertAlmostEqual(parse_and_eval("2 * sqrt(9)"), 6)
self.assertAlmostEqual(parse_and_eval("sqrt(2) ** 2"), 2)
self.assertAlmostEqual(parse_and_eval("-sqrt(4)"), -2)
def test_domain_error_is_calcerror(self):
with self.assertRaises(CalcError):
parse_and_eval("sqrt(-1)")
with self.assertRaises(CalcError):
parse_and_eval("log(0)")
def test_arity_error(self):
with self.assertRaises(SyntaxError_):
parse_and_eval("sqrt(1, 2)")
with self.assertRaises(SyntaxError_):
parse_and_eval("sqrt()")
def test_missing_parens_message(self):
with self.assertRaises(SyntaxError_) as ctx:
parse_and_eval("sqrt 4")
self.assertIn("sqrt", str(ctx.exception))
def test_unbalanced_call(self):
with self.assertRaises(SyntaxError_):
parse_and_eval("sqrt(16")
class TestPhase1FloorDivMod(unittest.TestCase):
def test_floordiv(self):
self.assertAlmostEqual(parse_and_eval("7 // 2"), 3)
self.assertAlmostEqual(parse_and_eval("-7 // 2"), -4)
def test_binary_mod(self):
self.assertAlmostEqual(parse_and_eval("7 % 3"), 1)
self.assertAlmostEqual(parse_and_eval("10 % 2.5"), 0)
def test_mod_vs_percent_rule(self):
# постфиксный процент без правого операнда
self.assertAlmostEqual(parse_and_eval("50 %"), 0.5)
self.assertAlmostEqual(parse_and_eval("2 * 50 %"), 1.0)
self.assertAlmostEqual(parse_and_eval("1 + 10 %"), 1.1)
def test_mod_with_paren_rhs(self):
self.assertAlmostEqual(parse_and_eval("7 % (3 + 1)"), 3)
def test_div_zero_variants(self):
with self.assertRaises(DivisionByZeroError):
parse_and_eval("5 // 0")
with self.assertRaises(DivisionByZeroError):
parse_and_eval("5 % 0")
class TestPhase1Integration(unittest.TestCase):
def test_combined_expression(self):
self.assertAlmostEqual(parse_and_eval("sqrt(3 ** 2 + 4 ** 2)"), 5)
self.assertAlmostEqual(parse_and_eval("1e3 * 10 %"), 100)
self.assertAlmostEqual(parse_and_eval("round(pi, 3)"), 3.142)
def test_try_evaluate_formats(self):
# огромно, но конечно (.12g)
self.assertEqual(try_evaluate("2 ** 1e2"), "1.26765060023e+30")
self.assertEqual(try_evaluate("sqrt(9)"), "3")
self.assertIn("Ошибка", try_evaluate("unknownvar"))
def test_float_artifact_cleaned(self):
self.assertEqual(try_evaluate("0.1 + 0.2"), "0.3")
def test_ans_in_env(self):
env = {"ans": 41.0}
self.assertAlmostEqual(parse_and_eval("1 + ans", env), 42.0)
def test_env_variable_overrides_constant(self):
env = {"e": 1.0}
self.assertAlmostEqual(parse_and_eval("e", env), 1.0)
def test_error_mentions_position(self):
with self.assertRaises(SyntaxError_) as ctx:
parse_and_eval("2 & 3")
self.assertIn("позиция", str(ctx.exception))
self.assertIn("3", str(ctx.exception))
def test_inf_display(self):
self.assertEqual(try_evaluate("inf"), "inf")
class TestPhase2Session(unittest.TestCase):
"""Поведение Session.process (cli-команды, ans, история)."""
def test_ans_updates_after_success(self):
from cli import Session
s = Session()
self.assertEqual(s.process("2 + 3"), ["5"])
self.assertEqual(s.process("ans * 2"), ["10"])
def test_ans_not_updated_on_error(self):
from cli import Session
s = Session()
s.process("sqrt(16)")
before = s.env["ans"]
s.process("sqrt(-1)")
self.assertEqual(s.env["ans"], before)
def test_vars_command(self):
from cli import Session
s = Session()
s.process("2 + 3")
self.assertIn("ans", s.process("vars")[0])
def test_hist_command_populated(self):
from cli import Session
s = Session()
s.process("sqrt(16)")
s.process("1 + 1")
self.assertGreaterEqual(len(s.process("hist")), 2)
def test_reset_clears_state(self):
from cli import Session
s = Session()
s.process("2 + 3")
self.assertEqual(
s.process("reset"),
["Сессия сброшена (ans = 0, история пуста)."],
)
def test_help_command(self):
from cli import Session
s = Session()
self.assertIn("Функции", s.process("help")[0])
def test_exit_returns_none(self):
from cli import Session
s = Session()
self.assertIsNone(s.process("выход"))
self.assertIsNone(s.process("exit"))
def test_empty_line_ignored(self):
from cli import Session
s = Session()
self.assertEqual(s.process(" "), [])
if __name__ == "__main__":
unittest.main()