from typing import Set, Dict, Iterable, Optional, List, Union, Any
from budget_sync.util import all_bugs
from budget_sync.money import Money
+from budget_sync.config import Config, Person, Milestone
from functools import cached_property
import toml
import sys
self.msg = msg
def __str__(self):
- return f"Failed to parse cf_payees_list field of bug #{self.bug_id}: {self.msg}"
+ return f"Failed to parse cf_payees_list field of " \
+ f"bug #{self.bug_id}: {self.msg}"
class BudgetGraphLoopError(BudgetGraphBaseError):
self.paid = paid
self.submitted = submitted
+ @cached_property
+ def payee(self) -> Person:
+ try:
+ return self.node.graph.config.all_names[self.payee_key]
+ except KeyError:
+ msg = f"unknown payee name: {self.payee_key!r} is not the name " \
+ f"or an alias of any known person"
+ raise BudgetGraphPayeesParseError(self.node.bug.id, msg) \
+ .with_traceback(sys.exc_info()[2])
+
@property
def state(self):
if self.paid is not None:
paid=paid, submitted=submitted)
def __repr__(self):
+ try:
+ payee = f"Person<{self.payee.identifier!r}>"
+ except BudgetGraphBaseError:
+ payee = "<unknown person>"
return (f"Payment(node={_NodeSimpleReprWrapper(self.node)}, "
+ f"payee={payee}, "
f"payee_key={self.payee_key!r}, "
f"amount={self.amount}, "
f"state={self.state.name}, "
f"submitted={str(self.submitted)})")
+class BudgetGraphUnknownMilestone(BudgetGraphParseError):
+ def __init__(self, bug_id: int, milestone_str: str):
+ super().__init__(bug_id)
+ self.milestone_str = milestone_str
+
+ def __str__(self):
+ return f"failed to parse cf_nlnet_milestone field of bug " \
+ f"#{self.bug_id}: unknown milestone: {self.milestone_str!r}"
+
+
class Node:
graph: "BudgetGraph"
bug: Bug
budget_including_subtasks: Money
fixed_budget_excluding_subtasks: Money
fixed_budget_including_subtasks: Money
- nlnet_milestone: Optional[str]
+ milestone_str: Optional[str]
def __init__(self, graph: "BudgetGraph", bug: Bug):
self.graph = graph
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
+ self.milestone_str = bug.cf_nlnet_milestone
+ if self.milestone_str == "---":
+ self.milestone_str = None
+
+ @cached_property
+ def milestone(self) -> Optional[Milestone]:
+ try:
+ if self.milestone_str is not None:
+ return self.graph.config.milestones[self.milestone_str]
+ return None
+ except KeyError:
+ new_err = BudgetGraphUnknownMilestone(
+ self.bug.id, self.milestone_str)
+ raise new_err.with_traceback(sys.exc_info()[2])
@cached_property
def payments(self) -> Dict[str, Payment]:
root = _NodeSimpleReprWrapper(self.root)
except BudgetGraphLoopError:
root = "<loop error>"
+ try:
+ milestone = repr(self.milestone)
+ except BudgetGraphBaseError:
+ milestone = "<unknown milestone>"
immediate_children = []
for i in self.immediate_children:
immediate_children.append(_NodeSimpleReprWrapper(i))
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"milestone_str={self.milestone_str!r}, "
+ f"milestone={milestone}, "
f"immediate_children={immediate_children!r}, "
f"payments={payments!r}")
f"bug #{self.bug_id}, payee {self.payee_key!r}")
+class BudgetGraphDuplicatePayeesForTask(BudgetGraphError):
+ def __init__(self, bug_id: int, root_bug_id: int, payee1_key: str, payee2_key: str):
+ super().__init__(bug_id, root_bug_id)
+ self.payee1_key = payee1_key
+ self.payee2_key = payee2_key
+
+ def __str__(self):
+ return (f"Budget assigned to multiple aliases of the same person in "
+ f"a single task: bug #{self.bug_id}, budget assigned to both "
+ f"{self.payee1_key!r} and {self.payee2_key!r}")
+
+
+class BudgetGraphIncorrectRootForMilestone(BudgetGraphError):
+ def __init__(self, bug_id: int, milestone: str, milestone_canonical_bug_id: int):
+ super().__init__(bug_id, bug_id)
+ self.milestone = milestone
+ self.milestone_canonical_bug_id = milestone_canonical_bug_id
+
+ def __str__(self):
+ return (f"Bug #{self.bug_id} is not the canonical root bug for "
+ f"assigned milestone {self.milestone!r} but has no parent "
+ f"bug set: the milestone's canonical root bug is "
+ f"#{self.milestone_canonical_bug_id}")
+
+
class BudgetGraph:
nodes: Dict[int, Node]
- def __init__(self, bugs: Iterable[Bug]):
+ def __init__(self, bugs: Iterable[Bug], config: Config):
self.nodes = {}
+ self.config = config
for bug in bugs:
self.nodes[bug.id] = Node(self, bug)
for node in self.nodes.values():
def _get_node_errors(self, root: Node, node: Node,
errors: List[BudgetGraphBaseError]):
- if node.nlnet_milestone is None:
+ if node.milestone_str is None:
if node.budget_including_subtasks != 0 \
or node.budget_excluding_subtasks != 0:
errors.append(BudgetGraphMoneyWithNoMilestone(
node.bug.id, root.bug.id))
- if node.nlnet_milestone != root.nlnet_milestone:
+ try:
+ # check for milestone errors
+ node.milestone
+ if root == node and node.milestone is not None \
+ and node.milestone.canonical_bug_id != node.bug.id:
+ if node.budget_including_subtasks != 0 \
+ or node.budget_excluding_subtasks != 0:
+ errors.append(BudgetGraphIncorrectRootForMilestone(
+ node.bug.id, node.milestone.identifier,
+ node.milestone.canonical_bug_id
+ ))
+ except BudgetGraphBaseError as e:
+ errors.append(e)
+
+ if node.milestone_str != root.milestone_str:
errors.append(BudgetGraphMilestoneMismatch(
node.bug.id, root.bug.id))
subtasks_total += child.fixed_budget_including_subtasks
payees_total = Money(0)
+ payee_payments = {}
for payment in node.payments.values():
if payment.amount < 0:
errors.append(BudgetGraphNegativePayeeMoney(
node.bug.id, root.bug.id, payment.payee_key))
payees_total += payment.amount
+ try:
+ # check for payee errors
+ payment.payee
+ previous_payment = payee_payments.get(payment.payee)
+ if previous_payment is not None:
+ errors.append(BudgetGraphDuplicatePayeesForTask(
+ node.bug.id, root.bug.id,
+ previous_payment.payee_key, payment.payee_key
+ ))
+ payee_payments[payment.payee] = payment
+ except BudgetGraphBaseError as e:
+ errors.append(e)
def set_including_from_excluding_and_error():
node.fixed_budget_including_subtasks = \
return errors
@cached_property
- def payee_keys(self) -> Set[str]:
- retval = set()
+ def payments(self) -> Dict[Person, Dict[Milestone, List[Payment]]]:
+ retval = {}
+ for person in self.config.people.values():
+ milestone_payments = {}
+ for milestone in self.config.milestones.values():
+ milestone_payments[milestone] = []
+ retval[person] = milestone_payments
for node in self.nodes.values():
- for payee_key in node.payments.keys():
- retval.add(payee_key)
+ if node.milestone is not None:
+ for payment in node.payments.values():
+ retval[payment.payee][node.milestone].append(payment)
return retval
from budget_sync.test.mock_bug import MockBug
-from budget_sync.budget_graph import (BudgetGraphLoopError, BudgetGraph,
- Node, BudgetGraphMoneyWithNoMilestone,
- BudgetGraphBaseError,
- BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
- BudgetGraphMoneyMismatchForBudgetIncludingSubtasks,
- BudgetGraphNegativeMoney,
- BudgetGraphMilestoneMismatch,
- BudgetGraphNegativePayeeMoney,
- BudgetGraphPayeesParseError,
- BudgetGraphPayeesMoneyMismatch)
+from budget_sync.config import Config
+from budget_sync.budget_graph import (
+ BudgetGraphLoopError, BudgetGraph, Node, BudgetGraphMoneyWithNoMilestone,
+ BudgetGraphBaseError, BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks,
+ BudgetGraphNegativeMoney, BudgetGraphMilestoneMismatch,
+ BudgetGraphNegativePayeeMoney, BudgetGraphPayeesParseError,
+ BudgetGraphPayeesMoneyMismatch, BudgetGraphUnknownMilestone,
+ BudgetGraphDuplicatePayeesForTask, BudgetGraphIncorrectRootForMilestone)
from budget_sync.money import Money
from typing import List, Type
import unittest
class TestErrorFormatting(unittest.TestCase):
+ def test_budget_graph_incorrect_root_for_milestone(self):
+ self.assertEqual(str(BudgetGraphIncorrectRootForMilestone(
+ 2, "milestone 1", 1)),
+ "Bug #2 is not the canonical root bug for assigned milestone "
+ "'milestone 1' but has no parent bug set: the milestone's "
+ "canonical root bug is #1")
+
+ def test_budget_graph_duplicate_payees_for_task(self):
+ self.assertEqual(str(BudgetGraphDuplicatePayeesForTask(
+ 2, 1, "alias1", "alias2")),
+ "Budget assigned to multiple aliases of the same person in a "
+ "single task: bug #2, budget assigned to both 'alias1' "
+ "and 'alias2'")
+
def test_budget_graph_loop_error(self):
self.assertEqual(str(BudgetGraphLoopError([1, 2, 3, 4, 5])),
"Detected Loop in Budget Graph: #5 -> #1 "
"milestone assigned to the root bug: descendant "
"bug #1, root bug #5")
+ def test_budget_graph_unknown_milestone(self):
+ self.assertEqual(str(BudgetGraphUnknownMilestone(
+ 123, "fake milestone")),
+ "failed to parse cf_nlnet_milestone field of bug "
+ "#123: unknown milestone: 'fake milestone'")
+
def test_budget_graph_money_mismatch(self):
self.assertEqual(str(
BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
cf_budget_parent=None,
cf_budget="10",
cf_total_budget="20",
- cf_nlnet_milestone="abc",
+ cf_nlnet_milestone="milestone 1",
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="milestone 1",
cf_payees_list="")
+EXAMPLE_CONFIG = Config.from_str(
+ """
+ bugzilla_url = ""
+ [people."person1"]
+ aliases = ["person1_alias1", "alias1"]
+ [people."person2"]
+ aliases = ["person1_alias2", "alias2", "person 2"]
+ [people."person3"]
+ [milestones]
+ "milestone 1" = { canonical_bug_id = 1 }
+ "milestone 2" = { canonical_bug_id = 2 }
+ """)
+
class TestBudgetGraph(unittest.TestCase):
+ maxDiff = None
+
def assertErrorTypesMatches(self, errors: List[BudgetGraphBaseError], template: List[Type]):
+ def wrap_type_list(type_list: List[Type]):
+ class TypeWrapper:
+ def __init__(self, t):
+ self.t = t
+
+ def __repr__(self):
+ return self.t.__name__
+
+ def __eq__(self, other):
+ return self.t == other.t
+ return [TypeWrapper(i) for i in type_list]
error_types = []
for error in errors:
error_types.append(type(error))
- self.assertEqual(error_types, template)
+ self.assertEqual(wrap_type_list(error_types), wrap_type_list(template))
def test_empty(self):
- bg = BudgetGraph([])
+ bg = BudgetGraph([], EXAMPLE_CONFIG)
self.assertEqual(len(bg.nodes), 0)
self.assertEqual(len(bg.roots), 0)
+ self.assertIs(bg.config, EXAMPLE_CONFIG)
def test_single(self):
- bg = BudgetGraph([EXAMPLE_BUG1])
+ bg = BudgetGraph([EXAMPLE_BUG1], EXAMPLE_CONFIG)
self.assertEqual(len(bg.nodes), 1)
node: Node = bg.nodes[1]
self.assertEqual(bg.roots, {node})
self.assertEqual(node.immediate_children, set())
self.assertEqual(node.budget_excluding_subtasks, Money(cents=0))
self.assertEqual(node.budget_including_subtasks, Money(cents=0))
- self.assertIsNone(node.nlnet_milestone)
+ self.assertIsNone(node.milestone)
self.assertEqual(node.payments, {})
def test_loop1(self):
with self.assertRaises(BudgetGraphLoopError) as cm:
- BudgetGraph([EXAMPLE_LOOP1_BUG1]).roots
+ BudgetGraph([EXAMPLE_LOOP1_BUG1], EXAMPLE_CONFIG).roots
self.assertEqual(cm.exception.bug_ids, [1])
def test_loop2(self):
with self.assertRaises(BudgetGraphLoopError) as cm:
- BudgetGraph([EXAMPLE_LOOP2_BUG1, EXAMPLE_LOOP2_BUG2]).roots
+ BudgetGraph([EXAMPLE_LOOP2_BUG1, EXAMPLE_LOOP2_BUG2],
+ EXAMPLE_CONFIG).roots
self.assertEqual(cm.exception.bug_ids, [2, 1])
def test_parent_child(self):
- bg = BudgetGraph([EXAMPLE_PARENT_BUG1, EXAMPLE_CHILD_BUG2])
+ bg = BudgetGraph([EXAMPLE_PARENT_BUG1, EXAMPLE_CHILD_BUG2],
+ EXAMPLE_CONFIG)
self.assertEqual(len(bg.nodes), 2)
node1: Node = bg.nodes[1]
node2: Node = bg.nodes[2]
self.assertEqual(node1.immediate_children, {node2})
self.assertEqual(node1.budget_excluding_subtasks, Money(cents=1000))
self.assertEqual(node1.budget_including_subtasks, Money(cents=2000))
- self.assertEqual(node1.nlnet_milestone, "abc")
+ self.assertEqual(node1.milestone_str, "milestone 1")
self.assertEqual(list(node1.children()), [node2])
self.assertEqual(list(node1.children_breadth_first()), [node2])
self.assertEqual(node1.payments, {})
self.assertEqual(node2.immediate_children, set())
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.milestone_str, "milestone 1")
self.assertEqual(node2.payments, {})
def test_children(self):
cf_total_budget="0",
cf_nlnet_milestone=None,
cf_payees_list=""),
- ])
+ ], EXAMPLE_CONFIG)
self.assertEqual(len(bg.nodes), 7)
node1: Node = bg.nodes[1]
node2: Node = bg.nodes[2]
cf_total_budget="10",
cf_nlnet_milestone=None,
cf_payees_list=""),
- ])
+ ], EXAMPLE_CONFIG)
errors = bg.get_errors()
self.assertErrorTypesMatches(errors, [
BudgetGraphMoneyWithNoMilestone,
cf_total_budget="0",
cf_nlnet_milestone=None,
cf_payees_list=""),
- ])
+ ], EXAMPLE_CONFIG)
errors = bg.get_errors()
self.assertErrorTypesMatches(errors, [
BudgetGraphMoneyWithNoMilestone,
cf_total_budget="10",
cf_nlnet_milestone=None,
cf_payees_list=""),
- ])
+ ], EXAMPLE_CONFIG)
errors = bg.get_errors()
self.assertErrorTypesMatches(errors, [BudgetGraphMoneyWithNoMilestone])
self.assertEqual(errors[0].bug_id, 1)
cf_budget_parent=None,
cf_budget=budget,
cf_total_budget=total_budget,
- cf_nlnet_milestone="abc",
+ cf_nlnet_milestone="milestone 1",
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_nlnet_milestone="milestone 1",
cf_payees_list=""),
- ])
+ ], EXAMPLE_CONFIG)
node1: Node = bg.nodes[1]
errors = bg.get_errors()
self.assertErrorTypesMatches(errors,
cf_budget=str(node1.fixed_budget_excluding_subtasks),
cf_total_budget=str(
node1.fixed_budget_including_subtasks),
- cf_nlnet_milestone="abc",
+ cf_nlnet_milestone="milestone 1",
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_nlnet_milestone="milestone 1",
cf_payees_list=""),
- ])
+ ], EXAMPLE_CONFIG)
errors = bg.get_errors()
self.assertErrorTypesMatches(errors,
expected_fixed_error_types)
])
helper(budget="0",
total_budget="0",
- payees_list="a=1",
+ payees_list="person1=1",
child_budget="0",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
])
helper(budget="0",
total_budget="0",
- payees_list="a=1",
+ payees_list="person1=1",
child_budget="5",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
])
helper(budget="0",
total_budget="0",
- payees_list="a=10",
+ payees_list="person1=10",
child_budget="0",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
])
helper(budget="0",
total_budget="0",
- payees_list="a=10",
+ payees_list="person1=10",
child_budget="5",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
])
helper(budget="0",
total_budget="100",
- payees_list="a=1",
+ payees_list="person1=1",
child_budget="0",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
helper(budget="0",
total_budget="100",
- payees_list="a=1",
+ payees_list="person1=1",
child_budget="5",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
helper(budget="0",
total_budget="100",
- payees_list="a=10",
+ payees_list="person1=10",
child_budget="0",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
helper(budget="0",
total_budget="100",
- payees_list="a=10",
+ payees_list="person1=10",
child_budget="5",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
expected_errors=[])
helper(budget="0",
total_budget="5",
- payees_list="a=1",
+ payees_list="person1=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",
+ payees_list="person1=10",
child_budget="5",
expected_errors=[
BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(0)),
])
helper(budget="10",
total_budget="0",
- payees_list="a=1",
+ payees_list="person1=1",
child_budget="0",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
helper(budget="10",
total_budget="0",
- payees_list="a=1",
+ payees_list="person1=1",
child_budget="5",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
helper(budget="10",
total_budget="0",
- payees_list="a=10",
+ payees_list="person1=10",
child_budget="0",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
])
helper(budget="10",
total_budget="0",
- payees_list="a=10",
+ payees_list="person1=10",
child_budget="5",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
expected_errors=[])
helper(budget="10",
total_budget="10",
- payees_list="a=1",
+ payees_list="person1=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",
+ payees_list="person1=10",
child_budget="0",
expected_errors=[])
helper(budget="10",
])
helper(budget="10",
total_budget="100",
- payees_list="a=1",
+ payees_list="person1=1",
child_budget="0",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
helper(budget="10",
total_budget="100",
- payees_list="a=1",
+ payees_list="person1=1",
child_budget="5",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
helper(budget="10",
total_budget="100",
- payees_list="a=10",
+ payees_list="person1=10",
child_budget="0",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
])
helper(budget="10",
total_budget="100",
- payees_list="a=10",
+ payees_list="person1=10",
child_budget="5",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
expected_errors=[])
helper(budget="10",
total_budget="15",
- payees_list="a=1",
+ payees_list="person1=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",
+ payees_list="person1=10",
child_budget="5",
expected_errors=[])
helper(budget="1",
total_budget="15",
- payees_list="a=10",
+ payees_list="person1=10",
child_budget="5",
expected_errors=[
BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
cf_budget_parent=None,
cf_budget="0",
cf_total_budget="-10",
- cf_nlnet_milestone="abc",
+ cf_nlnet_milestone="milestone 1",
cf_payees_list=""),
- ])
+ ], EXAMPLE_CONFIG)
errors = bg.get_errors()
self.assertErrorTypesMatches(errors, [
BudgetGraphNegativeMoney,
cf_budget_parent=None,
cf_budget="-10",
cf_total_budget="0",
- cf_nlnet_milestone="abc",
+ cf_nlnet_milestone="milestone 1",
cf_payees_list=""),
- ])
+ ], EXAMPLE_CONFIG)
errors = bg.get_errors()
self.assertErrorTypesMatches(errors, [
BudgetGraphNegativeMoney,
cf_budget_parent=None,
cf_budget="-10",
cf_total_budget="-10",
- cf_nlnet_milestone="abc",
+ cf_nlnet_milestone="milestone 1",
cf_payees_list=""),
- ])
+ ], EXAMPLE_CONFIG)
errors = bg.get_errors()
self.assertErrorTypesMatches(errors,
[BudgetGraphNegativeMoney])
self.assertEqual(errors[0].root_bug_id, 1)
def test_payees_parse(self):
- def check(cf_payees_list, expected_payments):
+ def check(cf_payees_list, error_types, expected_payments):
bg = BudgetGraph([MockBug(bug_id=1,
cf_budget_parent=None,
cf_budget="0",
cf_total_budget="0",
- cf_nlnet_milestone="abc",
+ cf_nlnet_milestone="milestone 1",
cf_payees_list=cf_payees_list),
- ])
+ ], EXAMPLE_CONFIG)
+ self.assertErrorTypesMatches(bg.get_errors(), error_types)
self.assertEqual(len(bg.nodes), 1)
node: Node = bg.nodes[1]
self.assertEqual([str(i) for i in node.payments.values()],
expected_payments)
- check("""
- abc = 123
- """,
- ["Payment(node=#1, payee_key='abc', amount=123, "
- + "state=NotYetSubmitted, paid=None, submitted=None)"])
- check("""
- abc = "123"
- """,
- ["Payment(node=#1, payee_key='abc', amount=123, "
- + "state=NotYetSubmitted, paid=None, submitted=None)"])
- check("""
- abc = "123.45"
- """,
- ["Payment(node=#1, payee_key='abc', amount=123.45, "
- + "state=NotYetSubmitted, paid=None, submitted=None)"])
- check("""
- abc = "123.45"
- "d e f" = "21.35"
- """,
- ["Payment(node=#1, payee_key='abc', amount=123.45, "
- + "state=NotYetSubmitted, paid=None, submitted=None)",
- "Payment(node=#1, payee_key='d e f', amount=21.35, "
- + "state=NotYetSubmitted, paid=None, submitted=None)"])
- check("""
- abc = "123.45"
- # my comments
- "AAA" = "-21.35"
- """,
- ["Payment(node=#1, payee_key='abc', amount=123.45, "
- + "state=NotYetSubmitted, paid=None, submitted=None)",
- "Payment(node=#1, payee_key='AAA', amount=-21.35, "
- + "state=NotYetSubmitted, paid=None, submitted=None)"])
- check("""
- "not-an-email@example.com" = "-2345"
- """,
- ["Payment(node=#1, payee_key='not-an-email@example.com', "
- + "amount=-2345, state=NotYetSubmitted, paid=None, "
- + "submitted=None)"])
- check("""
- payee = { amount = 123 }
- """,
- ["Payment(node=#1, payee_key='payee', "
- + "amount=123, state=NotYetSubmitted, paid=None, "
- + "submitted=None)"])
- check("""
- payee = { amount = 123, submitted = 2020-05-01 }
- """,
- ["Payment(node=#1, payee_key='payee', "
- + "amount=123, state=Submitted, paid=None, "
- + "submitted=2020-05-01)"])
- check("""
- payee = { amount = 123, submitted = 2020-05-01T00:00:00 }
- """,
- ["Payment(node=#1, payee_key='payee', "
- + "amount=123, state=Submitted, paid=None, "
- + "submitted=2020-05-01 00:00:00)"])
- check("""
- payee = { amount = 123, submitted = 2020-05-01T00:00:00Z }
- """,
- ["Payment(node=#1, payee_key='payee', "
- + "amount=123, state=Submitted, paid=None, "
- + "submitted=2020-05-01 00:00:00+00:00)"])
- check("""
- payee = { amount = 123, submitted = 2020-05-01T00:00:00-07:23 }
- """,
- ["Payment(node=#1, payee_key='payee', "
- + "amount=123, state=Submitted, paid=None, "
- + "submitted=2020-05-01 00:00:00-07:23)"])
- check("""
- payee = { amount = 123, paid = 2020-05-01 }
- """,
- ["Payment(node=#1, payee_key='payee', "
- + "amount=123, state=Paid, paid=2020-05-01, "
- + "submitted=None)"])
- check("""
- payee = { amount = 123, paid = 2020-05-01T00:00:00 }
- """,
- ["Payment(node=#1, payee_key='payee', "
- + "amount=123, state=Paid, paid=2020-05-01 00:00:00, "
- + "submitted=None)"])
- check("""
- payee = { amount = 123, paid = 2020-05-01T00:00:00Z }
- """,
- ["Payment(node=#1, payee_key='payee', "
- + "amount=123, state=Paid, paid=2020-05-01 00:00:00+00:00, "
- + "submitted=None)"])
- check("""
- payee = { amount = 123, paid = 2020-05-01T00:00:00-07:23 }
- """,
- ["Payment(node=#1, payee_key='payee', "
- + "amount=123, state=Paid, paid=2020-05-01 00:00:00-07:23, "
- + "submitted=None)"])
- check("""
- [payee]
- amount = 123
- submitted = 2020-05-23
- paid = 2020-05-01
- """,
- ["Payment(node=#1, payee_key='payee', "
- + "amount=123, state=Paid, paid=2020-05-01, "
- + "submitted=2020-05-23)"])
- check("""
- [payee]
- amount = 123
- submitted = 2020-05-23
- paid = 2020-05-01T00:00:00
- """,
- ["Payment(node=#1, payee_key='payee', "
- + "amount=123, state=Paid, paid=2020-05-01 00:00:00, "
- + "submitted=2020-05-23)"])
- check("""
- [payee]
- amount = 123
- submitted = 2020-05-23
- paid = 2020-05-01T00:00:00Z
- """,
- ["Payment(node=#1, payee_key='payee', "
- + "amount=123, state=Paid, paid=2020-05-01 00:00:00+00:00, "
- + "submitted=2020-05-23)"])
- check("""
- [payee]
- amount = 123
- submitted = 2020-05-23
- paid = 2020-05-01T00:00:00-07:23
- """,
- ["Payment(node=#1, payee_key='payee', "
- + "amount=123, state=Paid, paid=2020-05-01 00:00:00-07:23, "
- + "submitted=2020-05-23)"])
+ check(
+ """
+ person1 = 123
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, "
+ "payee_key='person1', amount=123, "
+ "state=NotYetSubmitted, paid=None, submitted=None)"])
+ check(
+ """
+ abc = "123"
+ """,
+ [BudgetGraphPayeesParseError,
+ BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=<unknown person>, payee_key='abc', "
+ "amount=123, state=NotYetSubmitted, paid=None, "
+ "submitted=None)"])
+ check(
+ """
+ person1 = "123.45"
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, "
+ "payee_key='person1', amount=123.45, "
+ "state=NotYetSubmitted, paid=None, submitted=None)"])
+ check(
+ """
+ person1 = "123.45"
+ "person 2" = "21.35"
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
+ 'amount=123.45, state=NotYetSubmitted, paid=None, '
+ 'submitted=None)',
+ "Payment(node=#1, payee=Person<'person2'>, payee_key='person 2', "
+ 'amount=21.35, state=NotYetSubmitted, paid=None, '
+ 'submitted=None)'])
+ check(
+ """
+ person1 = "123.45"
+ "d e f" = "21.35"
+ """,
+ [BudgetGraphPayeesParseError,
+ BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
+ 'amount=123.45, state=NotYetSubmitted, paid=None, '
+ 'submitted=None)',
+ "Payment(node=#1, payee=<unknown person>, payee_key='d e f', "
+ 'amount=21.35, state=NotYetSubmitted, paid=None, '
+ 'submitted=None)'])
+ check(
+ """
+ abc = "123.45"
+ # my comments
+ "AAA" = "-21.35"
+ """,
+ [BudgetGraphPayeesParseError,
+ BudgetGraphNegativePayeeMoney,
+ BudgetGraphPayeesParseError,
+ BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=<unknown person>, payee_key='abc', "
+ 'amount=123.45, state=NotYetSubmitted, paid=None, '
+ 'submitted=None)',
+ "Payment(node=#1, payee=<unknown person>, payee_key='AAA', "
+ 'amount=-21.35, state=NotYetSubmitted, paid=None, '
+ 'submitted=None)'])
+ check(
+ """
+ "not-an-email@example.com" = "-2345"
+ """,
+ [BudgetGraphNegativePayeeMoney,
+ BudgetGraphPayeesParseError,
+ BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ['Payment(node=#1, payee=<unknown person>, '
+ "payee_key='not-an-email@example.com', amount=-2345, "
+ "state=NotYetSubmitted, paid=None, submitted=None)"])
+ check(
+ """
+ person1 = { amount = 123 }
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
+ "amount=123, state=NotYetSubmitted, paid=None, submitted=None)"])
+ check(
+ """
+ person1 = { amount = 123, submitted = 2020-05-01 }
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
+ + "amount=123, state=Submitted, paid=None, "
+ + "submitted=2020-05-01)"])
+ check(
+ """
+ person1 = { amount = 123, submitted = 2020-05-01T00:00:00 }
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
+ + "amount=123, state=Submitted, paid=None, "
+ + "submitted=2020-05-01 00:00:00)"])
+ check(
+ """
+ person1 = { amount = 123, submitted = 2020-05-01T00:00:00Z }
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
+ + "amount=123, state=Submitted, paid=None, "
+ + "submitted=2020-05-01 00:00:00+00:00)"])
+ check(
+ """
+ person1 = { amount = 123, submitted = 2020-05-01T00:00:00-07:23 }
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
+ + "amount=123, state=Submitted, paid=None, "
+ + "submitted=2020-05-01 00:00:00-07:23)"])
+ check(
+ """
+ person1 = { amount = 123, paid = 2020-05-01 }
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
+ + "amount=123, state=Paid, paid=2020-05-01, "
+ + "submitted=None)"])
+ check(
+ """
+ person1 = { amount = 123, paid = 2020-05-01T00:00:00 }
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
+ + "amount=123, state=Paid, paid=2020-05-01 00:00:00, "
+ + "submitted=None)"])
+ check(
+ """
+ person1 = { amount = 123, paid = 2020-05-01T00:00:00Z }
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
+ + "amount=123, state=Paid, paid=2020-05-01 00:00:00+00:00, "
+ + "submitted=None)"])
+ check(
+ """
+ person1 = { amount = 123, paid = 2020-05-01T00:00:00-07:23 }
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
+ + "amount=123, state=Paid, paid=2020-05-01 00:00:00-07:23, "
+ + "submitted=None)"])
+ check(
+ """
+ [person1]
+ amount = 123
+ submitted = 2020-05-23
+ paid = 2020-05-01
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
+ + "amount=123, state=Paid, paid=2020-05-01, "
+ + "submitted=2020-05-23)"])
+ check(
+ """
+ [person1]
+ amount = 123
+ submitted = 2020-05-23
+ paid = 2020-05-01T00:00:00
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
+ + "amount=123, state=Paid, paid=2020-05-01 00:00:00, "
+ + "submitted=2020-05-23)"])
+ check(
+ """
+ [person1]
+ amount = 123
+ submitted = 2020-05-23
+ paid = 2020-05-01T00:00:00Z
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
+ + "amount=123, state=Paid, paid=2020-05-01 00:00:00+00:00, "
+ + "submitted=2020-05-23)"])
+ check(
+ """
+ [person1]
+ amount = 123
+ submitted = 2020-05-23
+ paid = 2020-05-01T00:00:00-07:23
+ """,
+ [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+ BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
+ ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
+ + "amount=123, state=Paid, paid=2020-05-01 00:00:00-07:23, "
+ + "submitted=2020-05-23)"])
def test_payees_money_mismatch(self):
bg = BudgetGraph([
cf_budget_parent=None,
cf_budget="10",
cf_total_budget="10",
- cf_nlnet_milestone="abc",
- cf_payees_list="payee = 5\npayee2 = 10"),
- ])
+ cf_nlnet_milestone="milestone 1",
+ cf_payees_list="person1 = 5\nperson2 = 10"),
+ ], EXAMPLE_CONFIG)
errors = bg.get_errors()
self.assertErrorTypesMatches(errors,
[BudgetGraphPayeesMoneyMismatch])
cf_budget_parent=None,
cf_budget="0",
cf_total_budget="0",
- cf_nlnet_milestone="abc",
+ cf_nlnet_milestone="milestone 1",
cf_payees_list=cf_payees_list),
- ]).get_errors()
+ ], EXAMPLE_CONFIG).get_errors()
self.assertErrorTypesMatches(errors,
[BudgetGraphPayeesParseError])
self.assertEqual(errors[0].bug_id, 1)
cf_budget_parent=None,
cf_budget="10",
cf_total_budget="10",
- cf_nlnet_milestone="abc",
- cf_payees_list="""payee1 = -10"""),
- ])
+ cf_nlnet_milestone="milestone 1",
+ cf_payees_list="""person1 = -10"""),
+ ], EXAMPLE_CONFIG)
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[0].payee_key, "person1")
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):
+ def test_duplicate_payments(self):
+ bg = BudgetGraph([
+ MockBug(bug_id=1,
+ cf_budget_parent=None,
+ cf_budget="10",
+ cf_total_budget="10",
+ cf_nlnet_milestone="milestone 1",
+ cf_payees_list="""
+ person1 = 5
+ alias1 = 5
+ """),
+ ], EXAMPLE_CONFIG)
+ errors = bg.get_errors()
+ self.assertErrorTypesMatches(errors,
+ [BudgetGraphDuplicatePayeesForTask])
+ self.assertEqual(errors[0].bug_id, 1)
+ self.assertEqual(errors[0].root_bug_id, 1)
+ self.assertEqual(errors[0].payee1_key, "person1")
+ self.assertEqual(errors[0].payee2_key, "alias1")
+
+ def test_incorrect_root_for_milestone(self):
+ bg = BudgetGraph([
+ MockBug(bug_id=1,
+ cf_budget_parent=None,
+ cf_budget="10",
+ cf_total_budget="10",
+ cf_nlnet_milestone="milestone 2",
+ cf_payees_list=""),
+ ], EXAMPLE_CONFIG)
+ errors = bg.get_errors()
+ self.assertErrorTypesMatches(errors,
+ [BudgetGraphIncorrectRootForMilestone])
+ self.assertEqual(errors[0].bug_id, 1)
+ self.assertEqual(errors[0].root_bug_id, 1)
+ self.assertEqual(errors[0].milestone, "milestone 2")
+ self.assertEqual(errors[0].milestone_canonical_bug_id, 2)
+ bg = BudgetGraph([
+ MockBug(bug_id=1,
+ cf_budget_parent=None,
+ cf_budget="0",
+ cf_total_budget="0",
+ cf_nlnet_milestone="milestone 2",
+ cf_payees_list=""),
+ ], EXAMPLE_CONFIG)
+ errors = bg.get_errors()
+ self.assertErrorTypesMatches(errors, [])
+
+ def test_payments(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"),
+ cf_nlnet_milestone="milestone 1",
+ cf_payees_list="person1 = 3\nperson2 = 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"""),
- ])
+ cf_nlnet_milestone="milestone 2",
+ cf_payees_list="person3 = 5\nperson2 = 5"),
+ ], EXAMPLE_CONFIG)
self.assertErrorTypesMatches(bg.get_errors(), [])
- self.assertEqual(bg.payee_keys, {"payee1", "payee2", "payee3"})
+ person1 = EXAMPLE_CONFIG.people["person1"]
+ person2 = EXAMPLE_CONFIG.people["person2"]
+ person3 = EXAMPLE_CONFIG.people["person3"]
+ milestone1 = EXAMPLE_CONFIG.milestones["milestone 1"]
+ milestone2 = EXAMPLE_CONFIG.milestones["milestone 2"]
+ node1: Node = bg.nodes[1]
+ node2: Node = bg.nodes[2]
+ node1_payment_person1 = node1.payments["person1"]
+ node1_payment_person2 = node1.payments["person2"]
+ node2_payment_person2 = node2.payments["person2"]
+ node2_payment_person3 = node2.payments["person3"]
+ self.assertEqual(bg.payments,
+ {
+ person1: {
+ milestone1: [node1_payment_person1],
+ milestone2: [],
+ },
+ person2: {
+ milestone1: [node1_payment_person2],
+ milestone2: [node2_payment_person2],
+ },
+ person3: {
+ milestone1: [],
+ milestone2: [node2_payment_person3],
+ },
+ })
if __name__ == "__main__":