add status field to budget_graph.Node
authorJacob Lifshay <programmerjake@gmail.com>
Fri, 18 Sep 2020 02:18:17 +0000 (19:18 -0700)
committerJacob Lifshay <programmerjake@gmail.com>
Fri, 18 Sep 2020 02:18:17 +0000 (19:18 -0700)
src/budget_sync/budget_graph.py
src/budget_sync/test/test_budget_graph.py

index 463c3e1..322b240 100644 (file)
@@ -1,7 +1,6 @@
 from bugzilla.bug import Bug
-from bugzilla import Bugzilla
 from typing import Set, Dict, Iterable, Optional, List, Union, Any
-from budget_sync.util import all_bugs
+from budget_sync.util import BugStatus
 from budget_sync.money import Money
 from budget_sync.config import Config, Person, Milestone
 from functools import cached_property
@@ -212,6 +211,16 @@ class BudgetGraphUnknownMilestone(BudgetGraphParseError):
             f"#{self.bug_id}: unknown milestone: {self.milestone_str!r}"
 
 
+class BudgetGraphUnknownStatus(BudgetGraphParseError):
+    def __init__(self, bug_id: int, status_str: str):
+        super().__init__(bug_id)
+        self.status_str = status_str
+
+    def __str__(self):
+        return f"failed to parse status field of bug " \
+            f"#{self.bug_id}: unknown status: {self.status_str!r}"
+
+
 class Node:
     graph: "BudgetGraph"
     bug: Bug
@@ -236,6 +245,14 @@ class Node:
         if self.milestone_str == "---":
             self.milestone_str = None
 
+    @property
+    def status(self) -> BugStatus:
+        try:
+            return BugStatus.cast(self.bug.status)
+        except ValueError:
+            new_err = BudgetGraphUnknownStatus(self.bug.id, self.bug.status)
+            raise new_err.with_traceback(sys.exc_info()[2])
+
     @cached_property
     def bug_url(self) -> str:
         return f"{self.graph.config.bugzilla_url_stripped}/show_bug.cgi?" \
@@ -330,6 +347,10 @@ class Node:
             milestone = repr(self.milestone)
         except BudgetGraphBaseError:
             milestone = "<unknown milestone>"
+        try:
+            status = repr(self.status)
+        except BudgetGraphBaseError:
+            status = f"<unknown status: {self.bug.status!r}>"
         immediate_children = []
         for i in self.immediate_children:
             immediate_children.append(_NodeSimpleReprWrapper(i))
@@ -347,7 +368,8 @@ class Node:
                 f"milestone_str={self.milestone_str!r}, "
                 f"milestone={milestone}, "
                 f"immediate_children={immediate_children!r}, "
-                f"payments={payments!r}")
+                f"payments={payments!r}, "
+                f"status={status})")
 
 
 class BudgetGraphError(BudgetGraphBaseError):
@@ -497,6 +519,12 @@ class BudgetGraph:
         except BudgetGraphBaseError as e:
             errors.append(e)
 
+        try:
+            # check for status errors
+            node.status
+        except BudgetGraphBaseError as e:
+            errors.append(e)
+
         if node.milestone_str != root.milestone_str:
             errors.append(BudgetGraphMilestoneMismatch(
                 node.bug.id, root.bug.id))
@@ -672,3 +700,13 @@ class BudgetGraph:
                 for payment in node.payments.values():
                     retval[payment.payee][node.milestone].append(payment)
         return retval
+
+    def __repr__(self):
+        nodes = [*self.nodes.values()]
+        try:
+            roots = [_NodeSimpleReprWrapper(i) for i in self.roots]
+            roots.sort()
+            roots_str = repr(roots)
+        except BudgetGraphBaseError:
+            roots_str = "<failed>"
+        return f"BudgetGraph{{nodes={nodes!r}, roots={roots}}}"
index 9fe072a..60ce3d0 100644 (file)
@@ -7,8 +7,10 @@ from budget_sync.budget_graph import (
     BudgetGraphNegativeMoney, BudgetGraphMilestoneMismatch,
     BudgetGraphNegativePayeeMoney, BudgetGraphPayeesParseError,
     BudgetGraphPayeesMoneyMismatch, BudgetGraphUnknownMilestone,
-    BudgetGraphDuplicatePayeesForTask, BudgetGraphIncorrectRootForMilestone)
+    BudgetGraphDuplicatePayeesForTask, BudgetGraphIncorrectRootForMilestone,
+    BudgetGraphUnknownStatus)
 from budget_sync.money import Money
+from budget_sync.util import BugStatus
 from typing import List, Type
 import unittest
 
@@ -52,6 +54,12 @@ class TestErrorFormatting(unittest.TestCase):
             "failed to parse cf_nlnet_milestone field of bug "
             "#123: unknown milestone: 'fake milestone'")
 
+    def test_budget_graph_unknown_status(self):
+        self.assertEqual(str(BudgetGraphUnknownStatus(
+            123, "fake status")),
+            "failed to parse status field of bug "
+            "#123: unknown status: 'fake status'")
+
     def test_budget_graph_money_mismatch(self):
         self.assertEqual(str(
             BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
@@ -168,6 +176,38 @@ class TestBudgetGraph(unittest.TestCase):
             error_types.append(type(error))
         self.assertEqual(wrap_type_list(error_types), wrap_type_list(template))
 
+    def test_repr(self):
+        bg = BudgetGraph([EXAMPLE_PARENT_BUG1, EXAMPLE_CHILD_BUG2],
+                         EXAMPLE_CONFIG)
+        self.assertEqual(
+            repr(bg),
+            "BudgetGraph{nodes=[Node(graph=..., id=#1, root=#1, parent=None, "
+            "budget_excluding_subtasks=10, budget_including_subtasks=20, "
+            "fixed_budget_excluding_subtasks=10, "
+            "fixed_budget_including_subtasks=20, "
+            "milestone_str='milestone 1', milestone=Milestone(config=..., "
+            "identifier='milestone 1', canonical_bug_id=1), "
+            "immediate_children=[#2], payments=[], "
+            "status=BugStatus.CONFIRMED), Node(graph=..., id=#2, root=#1, "
+            "parent=#1, budget_excluding_subtasks=10, "
+            "budget_including_subtasks=10, "
+            "fixed_budget_excluding_subtasks=10, "
+            "fixed_budget_including_subtasks=10, "
+            "milestone_str='milestone 1', milestone=Milestone(config=..., "
+            "identifier='milestone 1', canonical_bug_id=1), "
+            "immediate_children=[], payments=[], "
+            "status=BugStatus.CONFIRMED)], roots=[#1]}")
+        bg = BudgetGraph([MockBug(bug_id=1, status="blah")],
+                         EXAMPLE_CONFIG)
+        self.assertEqual(
+            repr(bg),
+            "BudgetGraph{nodes=[Node(graph=..., id=#1, root=#1, parent=None, "
+            "budget_excluding_subtasks=0, budget_including_subtasks=0, "
+            "fixed_budget_excluding_subtasks=0, "
+            "fixed_budget_including_subtasks=0, milestone_str=None, "
+            "milestone=None, immediate_children=[], payments=[], "
+            "status=<unknown status: 'blah'>)], roots=[#1]}")
+
     def test_empty(self):
         bg = BudgetGraph([], EXAMPLE_CONFIG)
         self.assertEqual(len(bg.nodes), 0)
@@ -1189,6 +1229,20 @@ class TestBudgetGraph(unittest.TestCase):
                              },
                          })
 
+    def test_status(self):
+        bg = BudgetGraph([MockBug(bug_id=1, status="blah")],
+                         EXAMPLE_CONFIG)
+        errors = bg.get_errors()
+        self.assertErrorTypesMatches(errors,
+                                     [BudgetGraphUnknownStatus])
+        self.assertEqual(errors[0].bug_id, 1)
+        self.assertEqual(errors[0].status_str, "blah")
+        for status in BugStatus:
+            bg = BudgetGraph([MockBug(bug_id=1, status=status)],
+                             EXAMPLE_CONFIG)
+            self.assertErrorTypesMatches(bg.get_errors(), [])
+            self.assertEqual(bg.nodes[1].status, status)
+
 
 if __name__ == "__main__":
     unittest.main()