start adding cf_payees_list handling
[utils.git] / src / budget_sync / test / test_budget_graph.py
index 7f08762f3ea797835abff61a4f418d04114ce990..932d48efd0cc5a93a138f4d6a93e3caef0bf47ef 100644 (file)
@@ -4,7 +4,10 @@ from budget_sync.budget_graph import (BudgetGraphLoopError, BudgetGraph,
                                       BudgetGraphBaseError,
                                       BudgetGraphMoneyMismatch,
                                       BudgetGraphNegativeMoney,
-                                      BudgetGraphMilestoneMismatch)
+                                      BudgetGraphMilestoneMismatch,
+                                      BudgetGraphNegativePayeeMoney,
+                                      BudgetGraphPayeesParseError,
+                                      BudgetGraphPayeesMoneyMismatch)
 from budget_sync.money import Money
 from typing import List, Type
 import unittest
@@ -39,37 +42,61 @@ class TestErrorFormatting(unittest.TestCase):
         self.assertEqual(str(BudgetGraphNegativeMoney(1, 5)),
                          "Budget assigned to task is less than zero: bug #1")
 
+    def test_budget_graph_negative_payee_money(self):
+        self.assertEqual(str(BudgetGraphNegativePayeeMoney(1, 5, "payee1")),
+                         "Budget assigned to payee for task is less than "
+                         "zero: bug #1, payee 'payee1'")
+
+    def test_budget_graph_payees_parse_error(self):
+        self.assertEqual(str(
+            BudgetGraphPayeesParseError(1, "my fake parse error")),
+            "Failed to parse cf_payees_list field of bug #1: "
+            "my fake parse error")
+
+    def test_budget_graph_payees_money_mismatch(self):
+        self.assertEqual(str(
+            BudgetGraphPayeesMoneyMismatch(1, 5, Money(123))),
+            "Budget assigned to task excluding subtasks (cf_budget field) "
+            "doesn't match total value assigned to payees (cf_payees_list):"
+            " bug #1, calculated total 123")
+
 
 EXAMPLE_BUG1 = MockBug(bug_id=1,
                        cf_budget_parent=None,
                        cf_budget="0",
                        cf_total_budget="0",
-                       cf_nlnet_milestone=None)
+                       cf_nlnet_milestone=None,
+                       cf_payees_list="")
 EXAMPLE_LOOP1_BUG1 = MockBug(bug_id=1,
                              cf_budget_parent=1,
                              cf_budget="0",
                              cf_total_budget="0",
-                             cf_nlnet_milestone=None)
+                             cf_nlnet_milestone=None,
+                             cf_payees_list="")
 EXAMPLE_LOOP2_BUG1 = MockBug(bug_id=1,
                              cf_budget_parent=2,
                              cf_budget="0",
                              cf_total_budget="0",
-                             cf_nlnet_milestone=None)
+                             cf_nlnet_milestone=None,
+                             cf_payees_list="")
 EXAMPLE_LOOP2_BUG2 = MockBug(bug_id=2,
                              cf_budget_parent=1,
                              cf_budget="0",
                              cf_total_budget="0",
-                             cf_nlnet_milestone=None)
+                             cf_nlnet_milestone=None,
+                             cf_payees_list="")
 EXAMPLE_PARENT_BUG1 = MockBug(bug_id=1,
                               cf_budget_parent=None,
                               cf_budget="10",
                               cf_total_budget="20",
-                              cf_nlnet_milestone="abc")
+                              cf_nlnet_milestone="abc",
+                              cf_payees_list="")
 EXAMPLE_CHILD_BUG2 = MockBug(bug_id=2,
                              cf_budget_parent=1,
                              cf_budget="10",
                              cf_total_budget="10",
-                             cf_nlnet_milestone="abc")
+                             cf_nlnet_milestone="abc",
+                             cf_payees_list="")
 
 
 class TestBudgetGraph(unittest.TestCase):
@@ -98,6 +125,7 @@ class TestBudgetGraph(unittest.TestCase):
         self.assertEqual(node.budget_excluding_subtasks, Money(cents=0))
         self.assertEqual(node.budget_including_subtasks, Money(cents=0))
         self.assertIsNone(node.nlnet_milestone)
+        self.assertEqual(node.payees, {})
 
     def test_loop1(self):
         with self.assertRaises(BudgetGraphLoopError) as cm:
@@ -129,6 +157,7 @@ class TestBudgetGraph(unittest.TestCase):
         self.assertEqual(node1.budget_including_subtasks, Money(cents=2000))
         self.assertEqual(node1.nlnet_milestone, "abc")
         self.assertEqual(list(node1.children()), [node2])
+        self.assertEqual(node1.payees, {})
         self.assertIsInstance(node2, Node)
         self.assertIs(node2.graph, bg)
         self.assertIs(node2.bug, EXAMPLE_CHILD_BUG2)
@@ -138,6 +167,7 @@ class TestBudgetGraph(unittest.TestCase):
         self.assertEqual(node2.budget_excluding_subtasks, Money(cents=1000))
         self.assertEqual(node2.budget_including_subtasks, Money(cents=1000))
         self.assertEqual(node2.nlnet_milestone, "abc")
+        self.assertEqual(node2.payees, {})
 
     def test_money_with_no_milestone(self):
         bg = BudgetGraph([
@@ -145,7 +175,8 @@ class TestBudgetGraph(unittest.TestCase):
                     cf_budget_parent=None,
                     cf_budget="0",
                     cf_total_budget="10",
-                    cf_nlnet_milestone=None),
+                    cf_nlnet_milestone=None,
+                    cf_payees_list=""),
         ])
         errors = bg.get_errors()
         self.assertErrorTypesMatches(errors,
@@ -158,7 +189,8 @@ class TestBudgetGraph(unittest.TestCase):
                     cf_budget_parent=None,
                     cf_budget="10",
                     cf_total_budget="0",
-                    cf_nlnet_milestone=None),
+                    cf_nlnet_milestone=None,
+                    cf_payees_list=""),
         ])
         errors = bg.get_errors()
         self.assertErrorTypesMatches(errors,
@@ -171,7 +203,8 @@ class TestBudgetGraph(unittest.TestCase):
                     cf_budget_parent=None,
                     cf_budget="10",
                     cf_total_budget="10",
-                    cf_nlnet_milestone=None),
+                    cf_nlnet_milestone=None,
+                    cf_payees_list=""),
         ])
         errors = bg.get_errors()
         self.assertErrorTypesMatches(errors, [BudgetGraphMoneyWithNoMilestone])
@@ -184,7 +217,8 @@ class TestBudgetGraph(unittest.TestCase):
                     cf_budget_parent=None,
                     cf_budget="0",
                     cf_total_budget="10",
-                    cf_nlnet_milestone="abc"),
+                    cf_nlnet_milestone="abc",
+                    cf_payees_list=""),
         ])
         errors = bg.get_errors()
         self.assertErrorTypesMatches(errors,
@@ -197,7 +231,8 @@ class TestBudgetGraph(unittest.TestCase):
                     cf_budget_parent=None,
                     cf_budget="10",
                     cf_total_budget="0",
-                    cf_nlnet_milestone="abc"),
+                    cf_nlnet_milestone="abc",
+                    cf_payees_list=""),
         ])
         errors = bg.get_errors()
         self.assertErrorTypesMatches(errors,
@@ -210,7 +245,8 @@ class TestBudgetGraph(unittest.TestCase):
                     cf_budget_parent=None,
                     cf_budget="10",
                     cf_total_budget="10",
-                    cf_nlnet_milestone="abc"),
+                    cf_nlnet_milestone="abc",
+                    cf_payees_list=""),
         ])
         errors = bg.get_errors()
         self.assertEqual(errors, [])
@@ -219,17 +255,20 @@ class TestBudgetGraph(unittest.TestCase):
                     cf_budget_parent=None,
                     cf_budget="10",
                     cf_total_budget="10",
-                    cf_nlnet_milestone="abc"),
+                    cf_nlnet_milestone="abc",
+                    cf_payees_list=""),
             MockBug(bug_id=2,
                     cf_budget_parent=1,
                     cf_budget="10",
                     cf_total_budget="10",
-                    cf_nlnet_milestone="abc"),
+                    cf_nlnet_milestone="abc",
+                    cf_payees_list=""),
             MockBug(bug_id=3,
                     cf_budget_parent=1,
                     cf_budget="1",
                     cf_total_budget="10",
-                    cf_nlnet_milestone="abc"),
+                    cf_nlnet_milestone="abc",
+                    cf_payees_list=""),
         ])
         errors = bg.get_errors()
         self.assertErrorTypesMatches(errors,
@@ -248,7 +287,8 @@ class TestBudgetGraph(unittest.TestCase):
                     cf_budget_parent=None,
                     cf_budget="0",
                     cf_total_budget="-10",
-                    cf_nlnet_milestone="abc"),
+                    cf_nlnet_milestone="abc",
+                    cf_payees_list=""),
         ])
         errors = bg.get_errors()
         self.assertErrorTypesMatches(errors,
@@ -264,7 +304,8 @@ class TestBudgetGraph(unittest.TestCase):
                     cf_budget_parent=None,
                     cf_budget="-10",
                     cf_total_budget="0",
-                    cf_nlnet_milestone="abc"),
+                    cf_nlnet_milestone="abc",
+                    cf_payees_list=""),
         ])
         errors = bg.get_errors()
         self.assertErrorTypesMatches(errors,
@@ -280,7 +321,8 @@ class TestBudgetGraph(unittest.TestCase):
                     cf_budget_parent=None,
                     cf_budget="-10",
                     cf_total_budget="-10",
-                    cf_nlnet_milestone="abc"),
+                    cf_nlnet_milestone="abc",
+                    cf_payees_list=""),
         ])
         errors = bg.get_errors()
         self.assertErrorTypesMatches(errors,
@@ -288,6 +330,166 @@ class TestBudgetGraph(unittest.TestCase):
         self.assertEqual(errors[0].bug_id, 1)
         self.assertEqual(errors[0].root_bug_id, 1)
 
+    def test_payees_parse(self):
+        def check(cf_payees_list, expected_payees):
+            bg = BudgetGraph([MockBug(bug_id=1,
+                                      cf_budget_parent=None,
+                                      cf_budget="0",
+                                      cf_total_budget="0",
+                                      cf_nlnet_milestone="abc",
+                                      cf_payees_list=cf_payees_list),
+                              ])
+            self.assertEqual(len(bg.nodes), 1)
+            node: Node = bg.nodes[1]
+            self.assertEqual(node.payees, expected_payees)
+
+        check("""
+              abc = 123
+              """,
+              {"abc": Money(123)})
+        check("""
+              abc = "123"
+              """,
+              {"abc": Money(123)})
+        check("""
+              abc = "123.45"
+              """,
+              {"abc": Money("123.45")})
+        check("""
+              abc = "123.45"
+              "d e f" = "21.35"
+              """,
+              {
+                  "abc": Money("123.45"),
+                  "d e f": Money("21.35"),
+              })
+        check("""
+              abc = "123.45"
+              # my comments
+              "AAA" = "-21.35"
+              """,
+              {
+                  "abc": Money("123.45"),
+                  "AAA": Money("-21.35"),
+              })
+        check("""
+              "not-an-email@example.com" = "-2345"
+              """,
+              {
+                  "not-an-email@example.com": Money(-2345),
+              })
+
+    def test_payees_money_mismatch(self):
+        bg = BudgetGraph([
+            MockBug(bug_id=1,
+                    cf_budget_parent=None,
+                    cf_budget="10",
+                    cf_total_budget="10",
+                    cf_nlnet_milestone="abc",
+                    cf_payees_list="payee = 5\npayee2 = 10"),
+        ])
+        errors = bg.get_errors()
+        self.assertErrorTypesMatches(errors,
+                                     [BudgetGraphPayeesMoneyMismatch])
+        self.assertEqual(errors[0].bug_id, 1)
+        self.assertEqual(errors[0].root_bug_id, 1)
+        self.assertEqual(errors[0].payees_total, 15)
+        bg = BudgetGraph([
+            MockBug(bug_id=1,
+                    cf_budget_parent=None,
+                    cf_budget="0",
+                    cf_total_budget="0",
+                    cf_nlnet_milestone=None,
+                    cf_payees_list="payee = 5\npayee2 = 10"),
+        ])
+        errors = bg.get_errors()
+        self.assertErrorTypesMatches(errors,
+                                     [BudgetGraphPayeesMoneyMismatch])
+        self.assertEqual(errors[0].bug_id, 1)
+        self.assertEqual(errors[0].root_bug_id, 1)
+        self.assertEqual(errors[0].payees_total, 15)
+
+    def test_payees_parse_error(self):
+        def check_parse_error(cf_payees_list, expected_msg):
+            errors = BudgetGraph([
+                MockBug(bug_id=1,
+                        cf_budget_parent=None,
+                        cf_budget="0",
+                        cf_total_budget="0",
+                        cf_nlnet_milestone="abc",
+                        cf_payees_list=cf_payees_list),
+            ]).get_errors()
+            self.assertErrorTypesMatches(errors,
+                                         [BudgetGraphPayeesParseError])
+            self.assertEqual(errors[0].bug_id, 1)
+            self.assertEqual(errors[0].msg, expected_msg)
+
+        check_parse_error("""
+                          "payee 1" = {}
+                          """,
+                          "value for key 'payee 1' is not a string or integer "
+                          "(to use fractional values such as 123.45, write "
+                          "\"123.45\"): {}")
+
+        check_parse_error("""
+                          payee = "ashjkf"
+                          """,
+                          "failed to parse Money value for key 'payee': "
+                          "invalid Money string: characters after sign and "
+                          "before first `.` must be ascii digits")
+
+        check_parse_error("""
+                          payee = "1"
+                          payee = "1"
+                          """,
+                          "TOML parse error: Duplicate keys! (line 3"
+                          " column 1 char 39)")
+
+        check_parse_error("""
+                          payee = 123.45
+                          """,
+                          "value for key 'payee' is not a string or "
+                          "integer (to use fractional values such as "
+                          "123.45, write \"123.45\"): 123.45")
+
+    def test_negative_payee_money(self):
+        bg = BudgetGraph([
+            MockBug(bug_id=1,
+                    cf_budget_parent=None,
+                    cf_budget="10",
+                    cf_total_budget="10",
+                    cf_nlnet_milestone="abc",
+                    cf_payees_list="""payee1 = -10"""),
+        ])
+        errors = bg.get_errors()
+        self.assertErrorTypesMatches(errors,
+                                     [BudgetGraphNegativePayeeMoney,
+                                      BudgetGraphPayeesMoneyMismatch])
+        self.assertEqual(errors[0].bug_id, 1)
+        self.assertEqual(errors[0].root_bug_id, 1)
+        self.assertEqual(errors[0].payee_key, "payee1")
+        self.assertEqual(errors[1].bug_id, 1)
+        self.assertEqual(errors[1].root_bug_id, 1)
+        self.assertEqual(errors[1].payees_total, -10)
+
+    def test_payee_keys(self):
+        bg = BudgetGraph([
+            MockBug(bug_id=1,
+                    cf_budget_parent=None,
+                    cf_budget="10",
+                    cf_total_budget="10",
+                    cf_nlnet_milestone="abc",
+                    cf_payees_list="payee2 = 3\npayee1 = 7"),
+            MockBug(bug_id=2,
+                    cf_budget_parent=None,
+                    cf_budget="10",
+                    cf_total_budget="10",
+                    cf_nlnet_milestone="def",
+                    cf_payees_list="""payee3 = 5\npayee2 = 5"""),
+        ])
+        self.assertErrorTypesMatches(bg.get_errors(), [])
+        self.assertEqual(bg.payee_keys, {"payee1", "payee2", "payee3"})
+
 
 if __name__ == "__main__":
     unittest.main()