import unittest from calc.lexer import tokenize from calc.parser import parse, ParseError, Num, BinOp, Unary def p(src): return parse(tokenize(src)) class TestPrecedence(unittest.TestCase): def test_mul_over_add(self): # 1+2*3 → 1+(2*3) self.assertEqual(repr(p('1+2*3')), 'BinOp(PLUS, Num(1), BinOp(STAR, Num(2), Num(3)))') def test_div_over_sub(self): # 6-4/2 → 6-(4/2) self.assertEqual(repr(p('6-4/2')), 'BinOp(MINUS, Num(6), BinOp(SLASH, Num(4), Num(2)))') def test_mul_over_add_reversed(self): # 2*3+1 → (2*3)+1 self.assertEqual(repr(p('2*3+1')), 'BinOp(PLUS, BinOp(STAR, Num(2), Num(3)), Num(1))') class TestLeftAssociativity(unittest.TestCase): def test_subtraction_left(self): # 8-3-2 → (8-3)-2 self.assertEqual(repr(p('8-3-2')), 'BinOp(MINUS, BinOp(MINUS, Num(8), Num(3)), Num(2))') def test_division_left(self): # 8/4/2 → (8/4)/2 self.assertEqual(repr(p('8/4/2')), 'BinOp(SLASH, BinOp(SLASH, Num(8), Num(4)), Num(2))') def test_addition_left(self): # 1+2+3 → (1+2)+3 self.assertEqual(repr(p('1+2+3')), 'BinOp(PLUS, BinOp(PLUS, Num(1), Num(2)), Num(3))') def test_mul_left(self): # 2*3*4 → (2*3)*4 self.assertEqual(repr(p('2*3*4')), 'BinOp(STAR, BinOp(STAR, Num(2), Num(3)), Num(4))') class TestParentheses(unittest.TestCase): def test_parens_override_precedence(self): # (1+2)*3 → STAR at top, PLUS inside self.assertEqual(repr(p('(1+2)*3')), 'BinOp(STAR, BinOp(PLUS, Num(1), Num(2)), Num(3))') def test_parens_on_right(self): # 3*(1+2) self.assertEqual(repr(p('3*(1+2)')), 'BinOp(STAR, Num(3), BinOp(PLUS, Num(1), Num(2)))') def test_nested_parens(self): self.assertEqual(repr(p('((2))')), 'Num(2)') class TestUnaryMinus(unittest.TestCase): def test_leading_unary(self): self.assertEqual(repr(p('-5')), 'Unary(MINUS, Num(5))') def test_unary_paren(self): self.assertEqual(repr(p('-(1+2)')), 'Unary(MINUS, BinOp(PLUS, Num(1), Num(2)))') def test_unary_in_mul(self): self.assertEqual(repr(p('3 * -2')), 'BinOp(STAR, Num(3), Unary(MINUS, Num(2)))') def test_double_unary(self): self.assertEqual(repr(p('--5')), 'Unary(MINUS, Unary(MINUS, Num(5)))') class TestErrors(unittest.TestCase): def test_trailing_operator(self): with self.assertRaises(ParseError): p('1 +') def test_unclosed_paren(self): with self.assertRaises(ParseError): p('(1') def test_two_consecutive_numbers(self): with self.assertRaises(ParseError): p('1 2') def test_close_open_paren(self): with self.assertRaises(ParseError): p(')(') def test_empty_string(self): with self.assertRaises(ParseError): p('') if __name__ == '__main__': unittest.main()