From f6b79488ff2c3e7829234141bc3046fef095c77c Mon Sep 17 00:00:00 2001 From: Jacob Lifshay Date: Fri, 4 Sep 2020 19:26:45 -0700 Subject: [PATCH] rewrite heuristics for which fields of bugs should change when they are inconsistent --- src/budget_sync/budget_graph.py | 172 +++++++- src/budget_sync/test/test_budget_graph.py | 503 +++++++++++++++++----- 2 files changed, 560 insertions(+), 115 deletions(-) diff --git a/src/budget_sync/budget_graph.py b/src/budget_sync/budget_graph.py index cb77a22..1ebcd50 100644 --- a/src/budget_sync/budget_graph.py +++ b/src/budget_sync/budget_graph.py @@ -6,6 +6,7 @@ from budget_sync.money import Money from functools import cached_property import toml import sys +from collections import deque class BudgetGraphBaseError(Exception): @@ -55,6 +56,8 @@ class Node: immediate_children: Set["Node"] budget_excluding_subtasks: Money budget_including_subtasks: Money + fixed_budget_excluding_subtasks: Money + fixed_budget_including_subtasks: Money nlnet_milestone: Optional[str] def __init__(self, graph: "BudgetGraph", bug: Bug): @@ -63,7 +66,9 @@ class Node: self.parent_id = getattr(bug, "cf_budget_parent", None) self.immediate_children = set() self.budget_excluding_subtasks = Money.from_str(bug.cf_budget) + self.fixed_budget_excluding_subtasks = self.budget_excluding_subtasks self.budget_including_subtasks = Money.from_str(bug.cf_total_budget) + self.fixed_budget_including_subtasks = self.budget_including_subtasks self.nlnet_milestone = bug.cf_nlnet_milestone if self.nlnet_milestone == "---": self.nlnet_milestone = None @@ -133,6 +138,16 @@ class Node: yield from visitor(i) return visitor(self) + def children_breadth_first(self) -> Iterable["Node"]: + q = deque(self.immediate_children) + while True: + try: + node = q.popleft() + except IndexError: + return + q.extend(node.immediate_children) + yield node + def __eq__(self, other): return self.bug.id == other.bug.id @@ -158,6 +173,8 @@ class Node: f"parent={parent}, " f"budget_excluding_subtasks={self.budget_excluding_subtasks}, " f"budget_including_subtasks={self.budget_including_subtasks}, " + f"fixed_budget_excluding_subtasks={self.fixed_budget_excluding_subtasks}, " + f"fixed_budget_including_subtasks={self.fixed_budget_including_subtasks}, " f"nlnet_milestone={self.nlnet_milestone!r}, " f"immediate_children={immediate_children!r}, " f"payees={self.payees!r}") @@ -183,7 +200,7 @@ class BudgetGraphMilestoneMismatch(BudgetGraphError): f" #{self.root_bug_id}") -class BudgetGraphMoneyMismatch(BudgetGraphError): +class BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(BudgetGraphError): def __init__(self, bug_id: int, root_bug_id: int, expected_budget_excluding_subtasks: Money): super().__init__(bug_id, root_bug_id) @@ -197,6 +214,20 @@ class BudgetGraphMoneyMismatch(BudgetGraphError): f" {self.expected_budget_excluding_subtasks}") +class BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(BudgetGraphError): + def __init__(self, bug_id: int, root_bug_id: int, + expected_budget_including_subtasks: Money): + super().__init__(bug_id, root_bug_id) + self.expected_budget_including_subtasks = \ + expected_budget_including_subtasks + + def __str__(self): + return (f"Budget assigned to task including subtasks " + f"(cf_total_budget field) doesn't match calculated value: " + f"bug #{self.bug_id}, calculated value" + f" {self.expected_budget_including_subtasks}") + + class BudgetGraphNegativeMoney(BudgetGraphError): def __str__(self): return (f"Budget assigned to task is less than zero: " @@ -204,16 +235,17 @@ class BudgetGraphNegativeMoney(BudgetGraphError): class BudgetGraphPayeesMoneyMismatch(BudgetGraphError): - def __init__(self, bug_id: int, root_bug_id: int, payees_total: Money): + def __init__(self, bug_id: int, root_bug_id: int, payees_total: Money, + expected_payees_total: Money): super().__init__(bug_id, root_bug_id) self.payees_total = payees_total + self.expected_payees_total = expected_payees_total def __str__(self): - return (f"Budget assigned to task excluding subtasks " - f"(cf_budget field) doesn't match total value " - f"assigned to payees (cf_payees_list): " - f"bug #{self.bug_id}, calculated total" - f" {self.payees_total}") + return (f"Total budget assigned to payees (cf_payees_list) doesn't " + f"match expected value: bug #{self.bug_id}, calculated total " + f"{self.payees_total}, expected value " + f"{self.expected_payees_total}") class BudgetGraphNegativePayeeMoney(BudgetGraphError): @@ -254,29 +286,135 @@ class BudgetGraph: or node.budget_excluding_subtasks != 0: errors.append(BudgetGraphMoneyWithNoMilestone( node.bug.id, root.bug.id)) + if node.nlnet_milestone != root.nlnet_milestone: errors.append(BudgetGraphMilestoneMismatch( node.bug.id, root.bug.id)) + if node.budget_excluding_subtasks < 0 \ or node.budget_including_subtasks < 0: errors.append(BudgetGraphNegativeMoney( node.bug.id, root.bug.id)) - budget = node.budget_including_subtasks + + subtasks_total = Money(0) for child in node.immediate_children: - budget -= child.budget_including_subtasks - if node.budget_excluding_subtasks != budget: - errors.append(BudgetGraphMoneyMismatch( - node.bug.id, root.bug.id, budget)) + subtasks_total += child.fixed_budget_including_subtasks + payees_total = Money(0) for payee_key, payee_value in node.payees.items(): if payee_value < 0: errors.append(BudgetGraphNegativePayeeMoney( node.bug.id, root.bug.id, payee_key)) payees_total += payee_value - if node.budget_excluding_subtasks != payees_total \ - and len(node.payees) != 0: + + def set_including_from_excluding_and_error(): + node.fixed_budget_including_subtasks = \ + node.budget_excluding_subtasks + subtasks_total + errors.append( + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + node.bug.id, root.bug.id, + node.fixed_budget_including_subtasks)) + + def set_including_from_payees_and_error(): + node.fixed_budget_including_subtasks = \ + payees_total + subtasks_total + errors.append( + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + node.bug.id, root.bug.id, + node.fixed_budget_including_subtasks)) + + def set_excluding_from_including_and_error(): + node.fixed_budget_excluding_subtasks = \ + node.budget_including_subtasks - subtasks_total + errors.append( + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + node.bug.id, root.bug.id, + node.fixed_budget_excluding_subtasks)) + + def set_excluding_from_payees_and_error(): + node.fixed_budget_excluding_subtasks = \ + payees_total + errors.append( + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + node.bug.id, root.bug.id, + node.fixed_budget_excluding_subtasks)) + + def set_payees_from_including_and_error(): + fixed_payees_total = \ + node.budget_including_subtasks - subtasks_total errors.append(BudgetGraphPayeesMoneyMismatch( - node.bug.id, root.bug.id, payees_total)) + node.bug.id, root.bug.id, payees_total, fixed_payees_total)) + + def set_payees_from_excluding_and_error(): + fixed_payees_total = \ + node.budget_excluding_subtasks + errors.append(BudgetGraphPayeesMoneyMismatch( + node.bug.id, root.bug.id, payees_total, fixed_payees_total)) + + payees_matches_including = \ + node.budget_including_subtasks - subtasks_total == payees_total + payees_matches_excluding = \ + node.budget_excluding_subtasks == payees_total + including_matches_excluding = \ + node.budget_including_subtasks - subtasks_total \ + == node.budget_excluding_subtasks + + if payees_matches_including \ + and payees_matches_excluding \ + and including_matches_excluding: + pass # no error + elif payees_matches_including: + # can't have 2 match without all 3 matching + assert not payees_matches_excluding + assert not including_matches_excluding + if node.budget_including_subtasks == 0 and len(node.payees) == 0: + set_including_from_excluding_and_error() + else: + set_excluding_from_including_and_error() + elif payees_matches_excluding: + # can't have 2 match without all 3 matching + assert not payees_matches_including + assert not including_matches_excluding + if node.budget_excluding_subtasks == 0 and len(node.payees) == 0: + if node.budget_including_subtasks == 0: + set_including_from_excluding_and_error() + else: + set_excluding_from_including_and_error() + else: + set_including_from_excluding_and_error() + elif including_matches_excluding: + # can't have 2 match without all 3 matching + assert not payees_matches_including + assert not payees_matches_excluding + if len(node.payees) == 0: + pass # no error -- payees is just not set + elif node.budget_excluding_subtasks == 0 \ + and node.budget_including_subtasks == 0: + set_excluding_from_payees_and_error() + set_including_from_payees_and_error() + else: + set_payees_from_excluding_and_error() + else: + # nothing matches + if len(node.payees) == 0: + # payees unset -- don't need to set payees + if node.budget_including_subtasks == 0: + set_including_from_excluding_and_error() + else: + set_excluding_from_including_and_error() + elif node.budget_excluding_subtasks == 0 \ + and node.budget_including_subtasks == 0: + set_excluding_from_payees_and_error() + set_including_from_payees_and_error() + elif node.budget_excluding_subtasks == 0: + set_excluding_from_including_and_error() + set_payees_from_including_and_error() + elif node.budget_including_subtasks == 0: + set_including_from_excluding_and_error() + set_payees_from_excluding_and_error() + else: + set_including_from_excluding_and_error() + set_payees_from_excluding_and_error() def get_errors(self) -> List[BudgetGraphBaseError]: errors = [] @@ -288,12 +426,12 @@ class BudgetGraph: for root in roots: try: - self._get_node_errors(root, root, errors) - for child in root.children(): + for child in reversed(list(root.children_breadth_first())): try: self._get_node_errors(root, child, errors) except BudgetGraphBaseError as e: errors.append(e) + self._get_node_errors(root, root, errors) except BudgetGraphBaseError as e: errors.append(e) return errors diff --git a/src/budget_sync/test/test_budget_graph.py b/src/budget_sync/test/test_budget_graph.py index 932d48e..16c107d 100644 --- a/src/budget_sync/test/test_budget_graph.py +++ b/src/budget_sync/test/test_budget_graph.py @@ -2,7 +2,8 @@ from budget_sync.test.mock_bug import MockBug from budget_sync.budget_graph import (BudgetGraphLoopError, BudgetGraph, Node, BudgetGraphMoneyWithNoMilestone, BudgetGraphBaseError, - BudgetGraphMoneyMismatch, + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks, + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks, BudgetGraphNegativeMoney, BudgetGraphMilestoneMismatch, BudgetGraphNegativePayeeMoney, @@ -33,10 +34,18 @@ class TestErrorFormatting(unittest.TestCase): "bug #1, root bug #5") def test_budget_graph_money_mismatch(self): - self.assertEqual(str(BudgetGraphMoneyMismatch(1, 5, "123.4")), - "Budget assigned to task excluding subtasks " - "(cf_budget field) doesn't match calculated value:" - " bug #1, calculated value 123.4") + self.assertEqual(str( + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + 1, 5, "123.4")), + "Budget assigned to task excluding subtasks " + "(cf_budget field) doesn't match calculated value:" + " bug #1, calculated value 123.4") + self.assertEqual(str( + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 5, "123.4")), + "Budget assigned to task including subtasks " + "(cf_total_budget field) doesn't match calculated value:" + " bug #1, calculated value 123.4") def test_budget_graph_negative_money(self): self.assertEqual(str(BudgetGraphNegativeMoney(1, 5)), @@ -55,10 +64,9 @@ class TestErrorFormatting(unittest.TestCase): 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") + BudgetGraphPayeesMoneyMismatch(1, 5, Money(123), Money(456))), + "Total budget assigned to payees (cf_payees_list) doesn't match " + "expected value: bug #1, calculated total 123, expected value 456") EXAMPLE_BUG1 = MockBug(bug_id=1, @@ -157,6 +165,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(list(node1.children_breadth_first()), [node2]) self.assertEqual(node1.payees, {}) self.assertIsInstance(node2, Node) self.assertIs(node2.graph, bg) @@ -169,117 +178,429 @@ class TestBudgetGraph(unittest.TestCase): self.assertEqual(node2.nlnet_milestone, "abc") self.assertEqual(node2.payees, {}) - def test_money_with_no_milestone(self): + def test_children(self): bg = BudgetGraph([ MockBug(bug_id=1, cf_budget_parent=None, cf_budget="0", - cf_total_budget="10", + cf_total_budget="0", cf_nlnet_milestone=None, cf_payees_list=""), - ]) - errors = bg.get_errors() - self.assertErrorTypesMatches(errors, - [BudgetGraphMoneyWithNoMilestone, - BudgetGraphMoneyMismatch]) - self.assertEqual(errors[0].bug_id, 1) - self.assertEqual(errors[0].root_bug_id, 1) - bg = BudgetGraph([ - MockBug(bug_id=1, - cf_budget_parent=None, - cf_budget="10", + MockBug(bug_id=2, + cf_budget_parent=1, + cf_budget="0", cf_total_budget="0", cf_nlnet_milestone=None, cf_payees_list=""), - ]) - errors = bg.get_errors() - self.assertErrorTypesMatches(errors, - [BudgetGraphMoneyWithNoMilestone, - BudgetGraphMoneyMismatch]) - self.assertEqual(errors[0].bug_id, 1) - self.assertEqual(errors[0].root_bug_id, 1) - bg = BudgetGraph([ - MockBug(bug_id=1, - cf_budget_parent=None, - cf_budget="10", - cf_total_budget="10", + MockBug(bug_id=3, + cf_budget_parent=1, + cf_budget="0", + cf_total_budget="0", + cf_nlnet_milestone=None, + cf_payees_list=""), + MockBug(bug_id=4, + cf_budget_parent=1, + cf_budget="0", + cf_total_budget="0", + cf_nlnet_milestone=None, + cf_payees_list=""), + MockBug(bug_id=5, + cf_budget_parent=3, + cf_budget="0", + cf_total_budget="0", + cf_nlnet_milestone=None, + cf_payees_list=""), + MockBug(bug_id=6, + cf_budget_parent=3, + cf_budget="0", + cf_total_budget="0", + cf_nlnet_milestone=None, + cf_payees_list=""), + MockBug(bug_id=7, + cf_budget_parent=5, + cf_budget="0", + cf_total_budget="0", cf_nlnet_milestone=None, cf_payees_list=""), ]) - errors = bg.get_errors() - self.assertErrorTypesMatches(errors, [BudgetGraphMoneyWithNoMilestone]) - self.assertEqual(errors[0].bug_id, 1) - self.assertEqual(errors[0].root_bug_id, 1) + self.assertEqual(len(bg.nodes), 7) + node1: Node = bg.nodes[1] + node2: Node = bg.nodes[2] + node3: Node = bg.nodes[3] + node4: Node = bg.nodes[4] + node5: Node = bg.nodes[5] + node6: Node = bg.nodes[6] + node7: Node = bg.nodes[7] + self.assertEqual(bg.roots, {node1}) + self.assertEqual(list(node1.children()), + [node2, node3, node5, node7, node6, node4]) + self.assertEqual(list(node1.children_breadth_first()), + [node2, node3, node4, node5, node6, node7]) - def test_money_mismatch(self): + def test_money_with_no_milestone(self): bg = BudgetGraph([ MockBug(bug_id=1, cf_budget_parent=None, cf_budget="0", cf_total_budget="10", - cf_nlnet_milestone="abc", + cf_nlnet_milestone=None, cf_payees_list=""), ]) errors = bg.get_errors() - self.assertErrorTypesMatches(errors, - [BudgetGraphMoneyMismatch]) + self.assertErrorTypesMatches(errors, [ + BudgetGraphMoneyWithNoMilestone, + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks]) self.assertEqual(errors[0].bug_id, 1) self.assertEqual(errors[0].root_bug_id, 1) - self.assertEqual(errors[0].expected_budget_excluding_subtasks, 10) bg = BudgetGraph([ MockBug(bug_id=1, cf_budget_parent=None, cf_budget="10", cf_total_budget="0", - cf_nlnet_milestone="abc", + cf_nlnet_milestone=None, cf_payees_list=""), ]) errors = bg.get_errors() - self.assertErrorTypesMatches(errors, - [BudgetGraphMoneyMismatch]) + self.assertErrorTypesMatches(errors, [ + BudgetGraphMoneyWithNoMilestone, + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks]) self.assertEqual(errors[0].bug_id, 1) self.assertEqual(errors[0].root_bug_id, 1) - self.assertEqual(errors[0].expected_budget_excluding_subtasks, 0) bg = BudgetGraph([ MockBug(bug_id=1, cf_budget_parent=None, cf_budget="10", cf_total_budget="10", - cf_nlnet_milestone="abc", - cf_payees_list=""), - ]) - errors = bg.get_errors() - self.assertEqual(errors, []) - bg = BudgetGraph([ - MockBug(bug_id=1, - cf_budget_parent=None, - cf_budget="10", - cf_total_budget="10", - 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_payees_list=""), - MockBug(bug_id=3, - cf_budget_parent=1, - cf_budget="1", - cf_total_budget="10", - cf_nlnet_milestone="abc", + cf_nlnet_milestone=None, cf_payees_list=""), ]) errors = bg.get_errors() - self.assertErrorTypesMatches(errors, - [BudgetGraphMoneyMismatch, - BudgetGraphMoneyMismatch]) + self.assertErrorTypesMatches(errors, [BudgetGraphMoneyWithNoMilestone]) self.assertEqual(errors[0].bug_id, 1) self.assertEqual(errors[0].root_bug_id, 1) - self.assertEqual(errors[0].expected_budget_excluding_subtasks, -10) - self.assertEqual(errors[1].bug_id, 3) - self.assertEqual(errors[1].root_bug_id, 1) - self.assertEqual(errors[1].expected_budget_excluding_subtasks, 10) + + def test_money_mismatch(self): + def helper(budget, total_budget, payees_list, child_budget, + expected_errors, expected_fixed_error_types=None): + if expected_fixed_error_types is None: + expected_fixed_error_types = [] + bg = BudgetGraph([ + MockBug(bug_id=1, + cf_budget_parent=None, + cf_budget=budget, + cf_total_budget=total_budget, + cf_nlnet_milestone="abc", + cf_payees_list=payees_list), + MockBug(bug_id=2, + cf_budget_parent=1, + cf_budget=child_budget, + cf_total_budget=child_budget, + cf_nlnet_milestone="abc", + cf_payees_list=""), + ]) + node1: Node = bg.nodes[1] + errors = bg.get_errors() + self.assertErrorTypesMatches(errors, + [type(i) for i in expected_errors]) + self.assertEqual([str(i) for i in errors], + [str(i) for i in expected_errors]) + bg = BudgetGraph([ + MockBug(bug_id=1, + cf_budget_parent=None, + cf_budget=str(node1.fixed_budget_excluding_subtasks), + cf_total_budget=str( + node1.fixed_budget_including_subtasks), + cf_nlnet_milestone="abc", + cf_payees_list=payees_list), + MockBug(bug_id=2, + cf_budget_parent=1, + cf_budget=child_budget, + cf_total_budget=child_budget, + cf_nlnet_milestone="abc", + cf_payees_list=""), + ]) + errors = bg.get_errors() + self.assertErrorTypesMatches(errors, + expected_fixed_error_types) + helper(budget="0", + total_budget="0", + payees_list="", + child_budget="0", + expected_errors=[]) + helper(budget="0", + total_budget="0", + payees_list="", + child_budget="5", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 1, Money(5)), + ]) + helper(budget="0", + total_budget="0", + payees_list="a=1", + child_budget="0", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + 1, 1, Money(1)), + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 1, Money(1)), + ]) + helper(budget="0", + total_budget="0", + payees_list="a=1", + child_budget="5", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + 1, 1, Money(1)), + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 1, Money(6)), + ]) + helper(budget="0", + total_budget="0", + payees_list="a=10", + child_budget="0", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + 1, 1, Money(10)), + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 1, Money(10)), + ]) + helper(budget="0", + total_budget="0", + payees_list="a=10", + child_budget="5", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + 1, 1, Money(10)), + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 1, Money(15)), + ]) + helper(budget="0", + total_budget="100", + payees_list="", + child_budget="0", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + 1, 1, Money(100)), + ]) + helper(budget="0", + total_budget="100", + payees_list="", + child_budget="5", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + 1, 1, Money(95)), + ]) + helper(budget="0", + total_budget="100", + payees_list="a=1", + child_budget="0", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + 1, 1, Money(100)), + BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(100)), + ], + expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch]) + helper(budget="0", + total_budget="100", + payees_list="a=1", + child_budget="5", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + 1, 1, Money(95)), + BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(95)), + ], + expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch]) + helper(budget="0", + total_budget="100", + payees_list="a=10", + child_budget="0", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + 1, 1, Money(100)), + BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(100)), + ], + expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch]) + helper(budget="0", + total_budget="100", + payees_list="a=10", + child_budget="5", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + 1, 1, Money(95)), + BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(95)), + ], + expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch]) + helper(budget="0", + total_budget="5", + payees_list="", + child_budget="5", + expected_errors=[]) + helper(budget="0", + total_budget="5", + payees_list="a=1", + child_budget="5", + expected_errors=[ + BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(0)), + ], + expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch]) + helper(budget="0", + total_budget="5", + payees_list="a=10", + child_budget="5", + expected_errors=[ + BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(0)), + ], + expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch]) + helper(budget="10", + total_budget="0", + payees_list="", + child_budget="0", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 1, Money(10)), + ]) + helper(budget="10", + total_budget="0", + payees_list="", + child_budget="5", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 1, Money(15)), + ]) + helper(budget="10", + total_budget="0", + payees_list="a=1", + child_budget="0", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 1, Money(10)), + BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)), + ], + expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch]) + helper(budget="10", + total_budget="0", + payees_list="a=1", + child_budget="5", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 1, Money(15)), + BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)), + ], + expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch]) + helper(budget="10", + total_budget="0", + payees_list="a=10", + child_budget="0", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 1, Money(10)), + ]) + helper(budget="10", + total_budget="0", + payees_list="a=10", + child_budget="5", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 1, Money(15)), + ]) + helper(budget="10", + total_budget="10", + payees_list="", + child_budget="0", + expected_errors=[]) + helper(budget="10", + total_budget="10", + payees_list="a=1", + child_budget="0", + expected_errors=[ + BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)), + ], + expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch]) + helper(budget="10", + total_budget="10", + payees_list="a=10", + child_budget="0", + expected_errors=[]) + helper(budget="10", + total_budget="100", + payees_list="", + child_budget="0", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + 1, 1, Money(100)), + ]) + helper(budget="10", + total_budget="100", + payees_list="", + child_budget="5", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + 1, 1, Money(95)), + ]) + helper(budget="10", + total_budget="100", + payees_list="a=1", + child_budget="0", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 1, Money(10)), + BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)), + ], + expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch]) + helper(budget="10", + total_budget="100", + payees_list="a=1", + child_budget="5", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 1, Money(15)), + BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)), + ], + expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch]) + helper(budget="10", + total_budget="100", + payees_list="a=10", + child_budget="0", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 1, Money(10)), + ]) + helper(budget="10", + total_budget="100", + payees_list="a=10", + child_budget="5", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks( + 1, 1, Money(15)), + ]) + helper(budget="10", + total_budget="15", + payees_list="", + child_budget="5", + expected_errors=[]) + helper(budget="10", + total_budget="15", + payees_list="a=1", + child_budget="5", + expected_errors=[ + BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)), + ], + expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch]) + helper(budget="10", + total_budget="15", + payees_list="a=10", + child_budget="5", + expected_errors=[]) + + helper(budget="1", + total_budget="15", + payees_list="a=10", + child_budget="5", + expected_errors=[ + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks( + 1, 1, Money("10")) + ]) def test_negative_money(self): bg = BudgetGraph([ @@ -291,9 +612,9 @@ class TestBudgetGraph(unittest.TestCase): cf_payees_list=""), ]) errors = bg.get_errors() - self.assertErrorTypesMatches(errors, - [BudgetGraphNegativeMoney, - BudgetGraphMoneyMismatch]) + self.assertErrorTypesMatches(errors, [ + BudgetGraphNegativeMoney, + BudgetGraphMoneyMismatchForBudgetExcludingSubtasks]) self.assertEqual(errors[0].bug_id, 1) self.assertEqual(errors[0].root_bug_id, 1) self.assertEqual(errors[1].bug_id, 1) @@ -308,14 +629,14 @@ class TestBudgetGraph(unittest.TestCase): cf_payees_list=""), ]) errors = bg.get_errors() - self.assertErrorTypesMatches(errors, - [BudgetGraphNegativeMoney, - BudgetGraphMoneyMismatch]) + self.assertErrorTypesMatches(errors, [ + BudgetGraphNegativeMoney, + BudgetGraphMoneyMismatchForBudgetIncludingSubtasks]) self.assertEqual(errors[0].bug_id, 1) self.assertEqual(errors[0].root_bug_id, 1) self.assertEqual(errors[1].bug_id, 1) self.assertEqual(errors[1].root_bug_id, 1) - self.assertEqual(errors[1].expected_budget_excluding_subtasks, 0) + self.assertEqual(errors[1].expected_budget_including_subtasks, -10) bg = BudgetGraph([ MockBug(bug_id=1, cf_budget_parent=None, @@ -394,20 +715,6 @@ class TestBudgetGraph(unittest.TestCase): 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): -- 2.30.2