1 from budget_sync
.test
.mock_bug
import MockBug
2 from budget_sync
.budget_graph
import (BudgetGraphLoopError
, BudgetGraph
,
3 Node
, BudgetGraphMoneyWithNoMilestone
,
5 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
6 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
,
7 BudgetGraphNegativeMoney
,
8 BudgetGraphMilestoneMismatch
,
9 BudgetGraphNegativePayeeMoney
,
10 BudgetGraphPayeesParseError
,
11 BudgetGraphPayeesMoneyMismatch
)
12 from budget_sync
.money
import Money
13 from typing
import List
, Type
17 class TestErrorFormatting(unittest
.TestCase
):
18 def test_budget_graph_loop_error(self
):
19 self
.assertEqual(str(BudgetGraphLoopError([1, 2, 3, 4, 5])),
20 "Detected Loop in Budget Graph: #5 -> #1 "
21 "-> #2 -> #3 -> #4 -> #5")
22 self
.assertEqual(str(BudgetGraphLoopError([1])),
23 "Detected Loop in Budget Graph: #1 -> #1")
25 def test_budget_graph_money_with_no_milestone(self
):
26 self
.assertEqual(str(BudgetGraphMoneyWithNoMilestone(1, 5)),
27 "Bug assigned money but without any assigned "
30 def test_budget_graph_milestone_mismatch(self
):
31 self
.assertEqual(str(BudgetGraphMilestoneMismatch(1, 5)),
32 "Bug's assigned milestone doesn't match the "
33 "milestone assigned to the root bug: descendant "
34 "bug #1, root bug #5")
36 def test_budget_graph_money_mismatch(self
):
38 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
40 "Budget assigned to task excluding subtasks "
41 "(cf_budget field) doesn't match calculated value:"
42 " bug #1, calculated value 123.4")
44 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
46 "Budget assigned to task including subtasks "
47 "(cf_total_budget field) doesn't match calculated value:"
48 " bug #1, calculated value 123.4")
50 def test_budget_graph_negative_money(self
):
51 self
.assertEqual(str(BudgetGraphNegativeMoney(1, 5)),
52 "Budget assigned to task is less than zero: bug #1")
54 def test_budget_graph_negative_payee_money(self
):
55 self
.assertEqual(str(BudgetGraphNegativePayeeMoney(1, 5, "payee1")),
56 "Budget assigned to payee for task is less than "
57 "zero: bug #1, payee 'payee1'")
59 def test_budget_graph_payees_parse_error(self
):
61 BudgetGraphPayeesParseError(1, "my fake parse error")),
62 "Failed to parse cf_payees_list field of bug #1: "
63 "my fake parse error")
65 def test_budget_graph_payees_money_mismatch(self
):
67 BudgetGraphPayeesMoneyMismatch(1, 5, Money(123), Money(456))),
68 "Total budget assigned to payees (cf_payees_list) doesn't match "
69 "expected value: bug #1, calculated total 123, expected value 456")
72 EXAMPLE_BUG1
= MockBug(bug_id
=1,
73 cf_budget_parent
=None,
76 cf_nlnet_milestone
=None,
78 EXAMPLE_LOOP1_BUG1
= MockBug(bug_id
=1,
82 cf_nlnet_milestone
=None,
84 EXAMPLE_LOOP2_BUG1
= MockBug(bug_id
=1,
88 cf_nlnet_milestone
=None,
90 EXAMPLE_LOOP2_BUG2
= MockBug(bug_id
=2,
94 cf_nlnet_milestone
=None,
96 EXAMPLE_PARENT_BUG1
= MockBug(bug_id
=1,
97 cf_budget_parent
=None,
100 cf_nlnet_milestone
="abc",
102 EXAMPLE_CHILD_BUG2
= MockBug(bug_id
=2,
105 cf_total_budget
="10",
106 cf_nlnet_milestone
="abc",
110 class TestBudgetGraph(unittest
.TestCase
):
111 def assertErrorTypesMatches(self
, errors
: List
[BudgetGraphBaseError
], template
: List
[Type
]):
114 error_types
.append(type(error
))
115 self
.assertEqual(error_types
, template
)
117 def test_empty(self
):
119 self
.assertEqual(len(bg
.nodes
), 0)
120 self
.assertEqual(len(bg
.roots
), 0)
122 def test_single(self
):
123 bg
= BudgetGraph([EXAMPLE_BUG1
])
124 self
.assertEqual(len(bg
.nodes
), 1)
125 node
: Node
= bg
.nodes
[1]
126 self
.assertEqual(bg
.roots
, {node}
)
127 self
.assertIsInstance(node
, Node
)
128 self
.assertIs(node
.graph
, bg
)
129 self
.assertIs(node
.bug
, EXAMPLE_BUG1
)
130 self
.assertIs(node
.root
, node
)
131 self
.assertIsNone(node
.parent_id
)
132 self
.assertEqual(node
.immediate_children
, set())
133 self
.assertEqual(node
.budget_excluding_subtasks
, Money(cents
=0))
134 self
.assertEqual(node
.budget_including_subtasks
, Money(cents
=0))
135 self
.assertIsNone(node
.nlnet_milestone
)
136 self
.assertEqual(node
.payees
, {})
138 def test_loop1(self
):
139 with self
.assertRaises(BudgetGraphLoopError
) as cm
:
140 BudgetGraph([EXAMPLE_LOOP1_BUG1
]).roots
141 self
.assertEqual(cm
.exception
.bug_ids
, [1])
143 def test_loop2(self
):
144 with self
.assertRaises(BudgetGraphLoopError
) as cm
:
145 BudgetGraph([EXAMPLE_LOOP2_BUG1
, EXAMPLE_LOOP2_BUG2
]).roots
146 self
.assertEqual(cm
.exception
.bug_ids
, [2, 1])
148 def test_parent_child(self
):
149 bg
= BudgetGraph([EXAMPLE_PARENT_BUG1
, EXAMPLE_CHILD_BUG2
])
150 self
.assertEqual(len(bg
.nodes
), 2)
151 node1
: Node
= bg
.nodes
[1]
152 node2
: Node
= bg
.nodes
[2]
153 self
.assertEqual(bg
.roots
, {node1}
)
154 self
.assertEqual(node1
, node1
)
155 self
.assertEqual(node2
, node2
)
156 self
.assertNotEqual(node1
, node2
)
157 self
.assertNotEqual(node2
, node1
)
158 self
.assertIsInstance(node1
, Node
)
159 self
.assertIs(node1
.graph
, bg
)
160 self
.assertIs(node1
.bug
, EXAMPLE_PARENT_BUG1
)
161 self
.assertIsNone(node1
.parent_id
)
162 self
.assertEqual(node1
.root
, node1
)
163 self
.assertEqual(node1
.immediate_children
, {node2}
)
164 self
.assertEqual(node1
.budget_excluding_subtasks
, Money(cents
=1000))
165 self
.assertEqual(node1
.budget_including_subtasks
, Money(cents
=2000))
166 self
.assertEqual(node1
.nlnet_milestone
, "abc")
167 self
.assertEqual(list(node1
.children()), [node2
])
168 self
.assertEqual(list(node1
.children_breadth_first()), [node2
])
169 self
.assertEqual(node1
.payees
, {})
170 self
.assertIsInstance(node2
, Node
)
171 self
.assertIs(node2
.graph
, bg
)
172 self
.assertIs(node2
.bug
, EXAMPLE_CHILD_BUG2
)
173 self
.assertEqual(node2
.parent_id
, 1)
174 self
.assertEqual(node2
.root
, node1
)
175 self
.assertEqual(node2
.immediate_children
, set())
176 self
.assertEqual(node2
.budget_excluding_subtasks
, Money(cents
=1000))
177 self
.assertEqual(node2
.budget_including_subtasks
, Money(cents
=1000))
178 self
.assertEqual(node2
.nlnet_milestone
, "abc")
179 self
.assertEqual(node2
.payees
, {})
181 def test_children(self
):
184 cf_budget_parent
=None,
187 cf_nlnet_milestone
=None,
193 cf_nlnet_milestone
=None,
199 cf_nlnet_milestone
=None,
205 cf_nlnet_milestone
=None,
211 cf_nlnet_milestone
=None,
217 cf_nlnet_milestone
=None,
223 cf_nlnet_milestone
=None,
226 self
.assertEqual(len(bg
.nodes
), 7)
227 node1
: Node
= bg
.nodes
[1]
228 node2
: Node
= bg
.nodes
[2]
229 node3
: Node
= bg
.nodes
[3]
230 node4
: Node
= bg
.nodes
[4]
231 node5
: Node
= bg
.nodes
[5]
232 node6
: Node
= bg
.nodes
[6]
233 node7
: Node
= bg
.nodes
[7]
234 self
.assertEqual(bg
.roots
, {node1}
)
235 self
.assertEqual(list(node1
.children()),
236 [node2
, node3
, node5
, node7
, node6
, node4
])
237 self
.assertEqual(list(node1
.children_breadth_first()),
238 [node2
, node3
, node4
, node5
, node6
, node7
])
240 def test_money_with_no_milestone(self
):
243 cf_budget_parent
=None,
245 cf_total_budget
="10",
246 cf_nlnet_milestone
=None,
249 errors
= bg
.get_errors()
250 self
.assertErrorTypesMatches(errors
, [
251 BudgetGraphMoneyWithNoMilestone
,
252 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
])
253 self
.assertEqual(errors
[0].bug_id
, 1)
254 self
.assertEqual(errors
[0].root_bug_id
, 1)
257 cf_budget_parent
=None,
260 cf_nlnet_milestone
=None,
263 errors
= bg
.get_errors()
264 self
.assertErrorTypesMatches(errors
, [
265 BudgetGraphMoneyWithNoMilestone
,
266 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
])
267 self
.assertEqual(errors
[0].bug_id
, 1)
268 self
.assertEqual(errors
[0].root_bug_id
, 1)
271 cf_budget_parent
=None,
273 cf_total_budget
="10",
274 cf_nlnet_milestone
=None,
277 errors
= bg
.get_errors()
278 self
.assertErrorTypesMatches(errors
, [BudgetGraphMoneyWithNoMilestone
])
279 self
.assertEqual(errors
[0].bug_id
, 1)
280 self
.assertEqual(errors
[0].root_bug_id
, 1)
282 def test_money_mismatch(self
):
283 def helper(budget
, total_budget
, payees_list
, child_budget
,
284 expected_errors
, expected_fixed_error_types
=None):
285 if expected_fixed_error_types
is None:
286 expected_fixed_error_types
= []
289 cf_budget_parent
=None,
291 cf_total_budget
=total_budget
,
292 cf_nlnet_milestone
="abc",
293 cf_payees_list
=payees_list
),
296 cf_budget
=child_budget
,
297 cf_total_budget
=child_budget
,
298 cf_nlnet_milestone
="abc",
301 node1
: Node
= bg
.nodes
[1]
302 errors
= bg
.get_errors()
303 self
.assertErrorTypesMatches(errors
,
304 [type(i
) for i
in expected_errors
])
305 self
.assertEqual([str(i
) for i
in errors
],
306 [str(i
) for i
in expected_errors
])
309 cf_budget_parent
=None,
310 cf_budget
=str(node1
.fixed_budget_excluding_subtasks
),
312 node1
.fixed_budget_including_subtasks
),
313 cf_nlnet_milestone
="abc",
314 cf_payees_list
=payees_list
),
317 cf_budget
=child_budget
,
318 cf_total_budget
=child_budget
,
319 cf_nlnet_milestone
="abc",
322 errors
= bg
.get_errors()
323 self
.assertErrorTypesMatches(errors
,
324 expected_fixed_error_types
)
335 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
343 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
345 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
353 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
355 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
363 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
365 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
373 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
375 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
383 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
391 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
399 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
401 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(100)),
403 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
409 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
411 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(95)),
413 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
419 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
421 BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(100)),
423 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
429 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
431 BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(95)),
433 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
444 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(0)),
446 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
452 BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(0)),
454 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
460 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
468 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
476 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
478 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
480 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
486 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
488 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
490 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
496 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
504 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
517 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
519 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
530 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
538 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
546 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
548 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
550 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
556 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
558 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
560 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
566 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
574 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
587 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
589 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
601 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
605 def test_negative_money(self
):
608 cf_budget_parent
=None,
610 cf_total_budget
="-10",
611 cf_nlnet_milestone
="abc",
614 errors
= bg
.get_errors()
615 self
.assertErrorTypesMatches(errors
, [
616 BudgetGraphNegativeMoney
,
617 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
])
618 self
.assertEqual(errors
[0].bug_id
, 1)
619 self
.assertEqual(errors
[0].root_bug_id
, 1)
620 self
.assertEqual(errors
[1].bug_id
, 1)
621 self
.assertEqual(errors
[1].root_bug_id
, 1)
622 self
.assertEqual(errors
[1].expected_budget_excluding_subtasks
, -10)
625 cf_budget_parent
=None,
628 cf_nlnet_milestone
="abc",
631 errors
= bg
.get_errors()
632 self
.assertErrorTypesMatches(errors
, [
633 BudgetGraphNegativeMoney
,
634 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
])
635 self
.assertEqual(errors
[0].bug_id
, 1)
636 self
.assertEqual(errors
[0].root_bug_id
, 1)
637 self
.assertEqual(errors
[1].bug_id
, 1)
638 self
.assertEqual(errors
[1].root_bug_id
, 1)
639 self
.assertEqual(errors
[1].expected_budget_including_subtasks
, -10)
642 cf_budget_parent
=None,
644 cf_total_budget
="-10",
645 cf_nlnet_milestone
="abc",
648 errors
= bg
.get_errors()
649 self
.assertErrorTypesMatches(errors
,
650 [BudgetGraphNegativeMoney
])
651 self
.assertEqual(errors
[0].bug_id
, 1)
652 self
.assertEqual(errors
[0].root_bug_id
, 1)
654 def test_payees_parse(self
):
655 def check(cf_payees_list
, expected_payees
):
656 bg
= BudgetGraph([MockBug(bug_id
=1,
657 cf_budget_parent
=None,
660 cf_nlnet_milestone
="abc",
661 cf_payees_list
=cf_payees_list
),
663 self
.assertEqual(len(bg
.nodes
), 1)
664 node
: Node
= bg
.nodes
[1]
665 self
.assertEqual(node
.payees
, expected_payees
)
678 {"abc": Money("123.45")})
684 "abc": Money("123.45"),
685 "d e f": Money("21.35"),
693 "abc": Money("123.45"),
694 "AAA": Money("-21.35"),
697 "not-an-email@example.com" = "-2345"
700 "not-an-email@example.com": Money(-2345),
703 def test_payees_money_mismatch(self
):
706 cf_budget_parent
=None,
708 cf_total_budget
="10",
709 cf_nlnet_milestone
="abc",
710 cf_payees_list
="payee = 5\npayee2 = 10"),
712 errors
= bg
.get_errors()
713 self
.assertErrorTypesMatches(errors
,
714 [BudgetGraphPayeesMoneyMismatch
])
715 self
.assertEqual(errors
[0].bug_id
, 1)
716 self
.assertEqual(errors
[0].root_bug_id
, 1)
717 self
.assertEqual(errors
[0].payees_total
, 15)
719 def test_payees_parse_error(self
):
720 def check_parse_error(cf_payees_list
, expected_msg
):
721 errors
= BudgetGraph([
723 cf_budget_parent
=None,
726 cf_nlnet_milestone
="abc",
727 cf_payees_list
=cf_payees_list
),
729 self
.assertErrorTypesMatches(errors
,
730 [BudgetGraphPayeesParseError
])
731 self
.assertEqual(errors
[0].bug_id
, 1)
732 self
.assertEqual(errors
[0].msg
, expected_msg
)
734 check_parse_error("""
737 "value for key 'payee 1' is not a string or integer "
738 "(to use fractional values such as 123.45, write "
741 check_parse_error("""
744 "failed to parse Money value for key 'payee': "
745 "invalid Money string: characters after sign and "
746 "before first `.` must be ascii digits")
748 check_parse_error("""
752 "TOML parse error: Duplicate keys! (line 3"
753 " column 1 char 39)")
755 check_parse_error("""
758 "value for key 'payee' is not a string or "
759 "integer (to use fractional values such as "
760 "123.45, write \"123.45\"): 123.45")
762 def test_negative_payee_money(self
):
765 cf_budget_parent
=None,
767 cf_total_budget
="10",
768 cf_nlnet_milestone
="abc",
769 cf_payees_list
="""payee1 = -10"""),
771 errors
= bg
.get_errors()
772 self
.assertErrorTypesMatches(errors
,
773 [BudgetGraphNegativePayeeMoney
,
774 BudgetGraphPayeesMoneyMismatch
])
775 self
.assertEqual(errors
[0].bug_id
, 1)
776 self
.assertEqual(errors
[0].root_bug_id
, 1)
777 self
.assertEqual(errors
[0].payee_key
, "payee1")
778 self
.assertEqual(errors
[1].bug_id
, 1)
779 self
.assertEqual(errors
[1].root_bug_id
, 1)
780 self
.assertEqual(errors
[1].payees_total
, -10)
782 def test_payee_keys(self
):
785 cf_budget_parent
=None,
787 cf_total_budget
="10",
788 cf_nlnet_milestone
="abc",
789 cf_payees_list
="payee2 = 3\npayee1 = 7"),
791 cf_budget_parent
=None,
793 cf_total_budget
="10",
794 cf_nlnet_milestone
="def",
795 cf_payees_list
="""payee3 = 5\npayee2 = 5"""),
797 self
.assertErrorTypesMatches(bg
.get_errors(), [])
798 self
.assertEqual(bg
.payee_keys
, {"payee1", "payee2", "payee3"})
801 if __name__
== "__main__":