1 from budget_sync
.test
.mock_bug
import MockBug
2 from budget_sync
.config
import Config
3 from budget_sync
.budget_graph
import (
4 BudgetGraphLoopError
, BudgetGraph
, Node
, BudgetGraphMoneyWithNoMilestone
,
5 BudgetGraphBaseError
, BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
6 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
,
7 BudgetGraphNegativeMoney
, BudgetGraphMilestoneMismatch
,
8 BudgetGraphNegativePayeeMoney
, BudgetGraphPayeesParseError
,
9 BudgetGraphPayeesMoneyMismatch
, BudgetGraphUnknownMilestone
,
10 BudgetGraphDuplicatePayeesForTask
, BudgetGraphIncorrectRootForMilestone
,
11 BudgetGraphUnknownStatus
, BudgetGraphUnknownAssignee
)
12 from budget_sync
.money
import Money
13 from budget_sync
.util
import BugStatus
14 from typing
import List
, Type
18 class TestErrorFormatting(unittest
.TestCase
):
19 def test_budget_graph_incorrect_root_for_milestone(self
):
20 self
.assertEqual(str(BudgetGraphIncorrectRootForMilestone(
21 2, "milestone 1", 1)),
22 "Bug #2 is not the canonical root bug for assigned milestone "
23 "'milestone 1' but has no parent bug set: the milestone's "
24 "canonical root bug is #1")
26 def test_budget_graph_duplicate_payees_for_task(self
):
27 self
.assertEqual(str(BudgetGraphDuplicatePayeesForTask(
28 2, 1, "alias1", "alias2")),
29 "Budget assigned to multiple aliases of the same person in a "
30 "single task: bug #2, budget assigned to both 'alias1' "
33 def test_budget_graph_loop_error(self
):
34 self
.assertEqual(str(BudgetGraphLoopError([1, 2, 3, 4, 5])),
35 "Detected Loop in Budget Graph: #5 -> #1 "
36 "-> #2 -> #3 -> #4 -> #5")
37 self
.assertEqual(str(BudgetGraphLoopError([1])),
38 "Detected Loop in Budget Graph: #1 -> #1")
40 def test_budget_graph_money_with_no_milestone(self
):
41 self
.assertEqual(str(BudgetGraphMoneyWithNoMilestone(1, 5)),
42 "Bug assigned money but without any assigned "
45 def test_budget_graph_milestone_mismatch(self
):
46 self
.assertEqual(str(BudgetGraphMilestoneMismatch(1, 5)),
47 "Bug's assigned milestone doesn't match the "
48 "milestone assigned to the root bug: descendant "
49 "bug #1, root bug #5")
51 def test_budget_graph_unknown_milestone(self
):
52 self
.assertEqual(str(BudgetGraphUnknownMilestone(
53 123, "fake milestone")),
54 "failed to parse cf_nlnet_milestone field of bug "
55 "#123: unknown milestone: 'fake milestone'")
57 def test_budget_graph_unknown_status(self
):
58 self
.assertEqual(str(BudgetGraphUnknownStatus(
60 "failed to parse status field of bug "
61 "#123: unknown status: 'fake status'")
63 def test_budget_graph_unknown_assignee(self
):
64 self
.assertEqual(str(BudgetGraphUnknownAssignee(
65 123, "unknown@example.com")),
66 "Bug #123 is assigned to an unknown person:"
67 " 'unknown@example.com'")
69 def test_budget_graph_money_mismatch(self
):
71 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
73 "Budget assigned to task excluding subtasks "
74 "(cf_budget field) doesn't match calculated value:"
75 " bug #1, calculated value 123.4")
77 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
79 "Budget assigned to task including subtasks "
80 "(cf_total_budget field) doesn't match calculated value:"
81 " bug #1, calculated value 123.4")
83 def test_budget_graph_negative_money(self
):
84 self
.assertEqual(str(BudgetGraphNegativeMoney(1, 5)),
85 "Budget assigned to task is less than zero: bug #1")
87 def test_budget_graph_negative_payee_money(self
):
88 self
.assertEqual(str(BudgetGraphNegativePayeeMoney(1, 5, "payee1")),
89 "Budget assigned to payee for task is less than "
90 "zero: bug #1, payee 'payee1'")
92 def test_budget_graph_payees_parse_error(self
):
94 BudgetGraphPayeesParseError(1, "my fake parse error")),
95 "Failed to parse cf_payees_list field of bug #1: "
96 "my fake parse error")
98 def test_budget_graph_payees_money_mismatch(self
):
100 BudgetGraphPayeesMoneyMismatch(1, 5, Money(123), Money(456))),
101 "Total budget assigned to payees (cf_payees_list) doesn't match "
102 "expected value: bug #1, calculated total 123, expected value 456")
105 EXAMPLE_BUG1
= MockBug(bug_id
=1,
106 cf_budget_parent
=None,
109 cf_nlnet_milestone
=None,
112 EXAMPLE_LOOP1_BUG1
= MockBug(bug_id
=1,
116 cf_nlnet_milestone
=None,
119 EXAMPLE_LOOP2_BUG1
= MockBug(bug_id
=1,
123 cf_nlnet_milestone
=None,
126 EXAMPLE_LOOP2_BUG2
= MockBug(bug_id
=2,
130 cf_nlnet_milestone
=None,
133 EXAMPLE_PARENT_BUG1
= MockBug(bug_id
=1,
134 cf_budget_parent
=None,
136 cf_total_budget
="20",
137 cf_nlnet_milestone
="milestone 1",
140 EXAMPLE_CHILD_BUG2
= MockBug(bug_id
=2,
143 cf_total_budget
="10",
144 cf_nlnet_milestone
="milestone 1",
148 EXAMPLE_CONFIG
= Config
.from_str(
150 bugzilla_url = "https://bugzilla.example.com/"
152 aliases = ["person1_alias1", "alias1"]
153 output_markdown_file = "person1.mdwn"
155 email = "person2@example.com"
156 aliases = ["person1_alias2", "alias2", "person 2"]
157 output_markdown_file = "person2.mdwn"
159 email = "user@example.com"
160 output_markdown_file = "person3.mdwn"
162 "milestone 1" = { canonical_bug_id = 1 }
163 "milestone 2" = { canonical_bug_id = 2 }
167 class TestBudgetGraph(unittest
.TestCase
):
170 def assertErrorTypesMatches(self
, errors
: List
[BudgetGraphBaseError
], template
: List
[Type
]):
171 def wrap_type_list(type_list
: List
[Type
]):
173 def __init__(self
, t
):
177 return self
.t
.__name
__
179 def __eq__(self
, other
):
180 return self
.t
== other
.t
181 return [TypeWrapper(i
) for i
in type_list
]
184 error_types
.append(type(error
))
185 self
.assertEqual(wrap_type_list(error_types
), wrap_type_list(template
))
188 bg
= BudgetGraph([EXAMPLE_PARENT_BUG1
, EXAMPLE_CHILD_BUG2
],
192 "BudgetGraph{nodes=[Node(graph=..., id=#1, root=#1, parent=None, "
193 "budget_excluding_subtasks=10, budget_including_subtasks=20, "
194 "fixed_budget_excluding_subtasks=10, "
195 "fixed_budget_including_subtasks=20, "
196 "milestone_str='milestone 1', milestone=Milestone(config=..., "
197 "identifier='milestone 1', canonical_bug_id=1), "
198 "immediate_children=[#2], payments=[], "
199 "status=BugStatus.CONFIRMED, assignee=Person<'person3'>), "
200 "Node(graph=..., id=#2, root=#1, "
201 "parent=#1, budget_excluding_subtasks=10, "
202 "budget_including_subtasks=10, "
203 "fixed_budget_excluding_subtasks=10, "
204 "fixed_budget_including_subtasks=10, "
205 "milestone_str='milestone 1', milestone=Milestone(config=..., "
206 "identifier='milestone 1', canonical_bug_id=1), "
207 "immediate_children=[], payments=[], "
208 "status=BugStatus.CONFIRMED, assignee=Person<'person3'>)], "
210 bg
= BudgetGraph([MockBug(bug_id
=1, status
="blah",
211 assigned_to
="unknown@example.com")],
215 "BudgetGraph{nodes=[Node(graph=..., id=#1, root=#1, parent=None, "
216 "budget_excluding_subtasks=0, budget_including_subtasks=0, "
217 "fixed_budget_excluding_subtasks=0, "
218 "fixed_budget_including_subtasks=0, milestone_str=None, "
219 "milestone=None, immediate_children=[], payments=[], "
220 "status=<unknown status: 'blah'>, "
221 "assignee=<unknown assignee: 'unknown@example.com'>)], "
224 def test_empty(self
):
225 bg
= BudgetGraph([], EXAMPLE_CONFIG
)
226 self
.assertEqual(len(bg
.nodes
), 0)
227 self
.assertEqual(len(bg
.roots
), 0)
228 self
.assertIs(bg
.config
, EXAMPLE_CONFIG
)
230 def test_single(self
):
231 bg
= BudgetGraph([EXAMPLE_BUG1
], EXAMPLE_CONFIG
)
232 self
.assertEqual(len(bg
.nodes
), 1)
233 node
: Node
= bg
.nodes
[1]
234 self
.assertEqual(bg
.roots
, {node}
)
235 self
.assertIsInstance(node
, Node
)
236 self
.assertIs(node
.graph
, bg
)
237 self
.assertIs(node
.bug
, EXAMPLE_BUG1
)
238 self
.assertIs(node
.root
, node
)
239 self
.assertIsNone(node
.parent_id
)
240 self
.assertEqual(node
.immediate_children
, set())
241 self
.assertEqual(node
.bug_url
,
242 "https://bugzilla.example.com/show_bug.cgi?id=1")
243 self
.assertEqual(node
.budget_excluding_subtasks
, Money(cents
=0))
244 self
.assertEqual(node
.budget_including_subtasks
, Money(cents
=0))
245 self
.assertIsNone(node
.milestone
)
246 self
.assertEqual(node
.payments
, {})
248 def test_loop1(self
):
249 with self
.assertRaises(BudgetGraphLoopError
) as cm
:
250 BudgetGraph([EXAMPLE_LOOP1_BUG1
], EXAMPLE_CONFIG
).roots
251 self
.assertEqual(cm
.exception
.bug_ids
, [1])
253 def test_loop2(self
):
254 with self
.assertRaises(BudgetGraphLoopError
) as cm
:
255 BudgetGraph([EXAMPLE_LOOP2_BUG1
, EXAMPLE_LOOP2_BUG2
],
256 EXAMPLE_CONFIG
).roots
257 self
.assertEqual(cm
.exception
.bug_ids
, [2, 1])
259 def test_parent_child(self
):
260 bg
= BudgetGraph([EXAMPLE_PARENT_BUG1
, EXAMPLE_CHILD_BUG2
],
262 self
.assertEqual(len(bg
.nodes
), 2)
263 node1
: Node
= bg
.nodes
[1]
264 node2
: Node
= bg
.nodes
[2]
265 self
.assertEqual(bg
.roots
, {node1}
)
266 self
.assertEqual(node1
, node1
)
267 self
.assertEqual(node2
, node2
)
268 self
.assertNotEqual(node1
, node2
)
269 self
.assertNotEqual(node2
, node1
)
270 self
.assertIsInstance(node1
, Node
)
271 self
.assertIs(node1
.graph
, bg
)
272 self
.assertIs(node1
.bug
, EXAMPLE_PARENT_BUG1
)
273 self
.assertIsNone(node1
.parent_id
)
274 self
.assertEqual(node1
.root
, node1
)
275 self
.assertEqual(node1
.immediate_children
, {node2}
)
276 self
.assertEqual(node1
.budget_excluding_subtasks
, Money(cents
=1000))
277 self
.assertEqual(node1
.budget_including_subtasks
, Money(cents
=2000))
278 self
.assertEqual(node1
.milestone_str
, "milestone 1")
279 self
.assertEqual(node1
.bug_url
,
280 "https://bugzilla.example.com/show_bug.cgi?id=1")
281 self
.assertEqual(list(node1
.children()), [node2
])
282 self
.assertEqual(list(node1
.children_breadth_first()), [node2
])
283 self
.assertEqual(node1
.payments
, {})
284 self
.assertIsInstance(node2
, Node
)
285 self
.assertIs(node2
.graph
, bg
)
286 self
.assertIs(node2
.bug
, EXAMPLE_CHILD_BUG2
)
287 self
.assertEqual(node2
.parent_id
, 1)
288 self
.assertEqual(node2
.root
, node1
)
289 self
.assertEqual(node2
.immediate_children
, set())
290 self
.assertEqual(node2
.budget_excluding_subtasks
, Money(cents
=1000))
291 self
.assertEqual(node2
.budget_including_subtasks
, Money(cents
=1000))
292 self
.assertEqual(node2
.milestone_str
, "milestone 1")
293 self
.assertEqual(node2
.payments
, {})
294 self
.assertEqual(node2
.bug_url
,
295 "https://bugzilla.example.com/show_bug.cgi?id=2")
297 def test_children(self
):
300 cf_budget_parent
=None,
303 cf_nlnet_milestone
=None,
310 cf_nlnet_milestone
=None,
317 cf_nlnet_milestone
=None,
324 cf_nlnet_milestone
=None,
331 cf_nlnet_milestone
=None,
338 cf_nlnet_milestone
=None,
345 cf_nlnet_milestone
=None,
349 self
.assertEqual(len(bg
.nodes
), 7)
350 node1
: Node
= bg
.nodes
[1]
351 node2
: Node
= bg
.nodes
[2]
352 node3
: Node
= bg
.nodes
[3]
353 node4
: Node
= bg
.nodes
[4]
354 node5
: Node
= bg
.nodes
[5]
355 node6
: Node
= bg
.nodes
[6]
356 node7
: Node
= bg
.nodes
[7]
357 self
.assertEqual(bg
.roots
, {node1}
)
358 self
.assertEqual(list(node1
.children()),
359 [node2
, node3
, node5
, node7
, node6
, node4
])
360 self
.assertEqual(list(node1
.children_breadth_first()),
361 [node2
, node3
, node4
, node5
, node6
, node7
])
363 def test_money_with_no_milestone(self
):
366 cf_budget_parent
=None,
368 cf_total_budget
="10",
369 cf_nlnet_milestone
=None,
373 errors
= bg
.get_errors()
374 self
.assertErrorTypesMatches(errors
, [
375 BudgetGraphMoneyWithNoMilestone
,
376 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
])
377 self
.assertEqual(errors
[0].bug_id
, 1)
378 self
.assertEqual(errors
[0].root_bug_id
, 1)
381 cf_budget_parent
=None,
384 cf_nlnet_milestone
=None,
388 errors
= bg
.get_errors()
389 self
.assertErrorTypesMatches(errors
, [
390 BudgetGraphMoneyWithNoMilestone
,
391 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
])
392 self
.assertEqual(errors
[0].bug_id
, 1)
393 self
.assertEqual(errors
[0].root_bug_id
, 1)
396 cf_budget_parent
=None,
398 cf_total_budget
="10",
399 cf_nlnet_milestone
=None,
403 errors
= bg
.get_errors()
404 self
.assertErrorTypesMatches(errors
, [BudgetGraphMoneyWithNoMilestone
])
405 self
.assertEqual(errors
[0].bug_id
, 1)
406 self
.assertEqual(errors
[0].root_bug_id
, 1)
408 def test_money_mismatch(self
):
409 def helper(budget
, total_budget
, payees_list
, child_budget
,
410 expected_errors
, expected_fixed_error_types
=None):
411 if expected_fixed_error_types
is None:
412 expected_fixed_error_types
= []
415 cf_budget_parent
=None,
417 cf_total_budget
=total_budget
,
418 cf_nlnet_milestone
="milestone 1",
419 cf_payees_list
=payees_list
,
423 cf_budget
=child_budget
,
424 cf_total_budget
=child_budget
,
425 cf_nlnet_milestone
="milestone 1",
429 node1
: Node
= bg
.nodes
[1]
430 errors
= bg
.get_errors()
431 self
.assertErrorTypesMatches(errors
,
432 [type(i
) for i
in expected_errors
])
433 self
.assertEqual([str(i
) for i
in errors
],
434 [str(i
) for i
in expected_errors
])
437 cf_budget_parent
=None,
438 cf_budget
=str(node1
.fixed_budget_excluding_subtasks
),
440 node1
.fixed_budget_including_subtasks
),
441 cf_nlnet_milestone
="milestone 1",
442 cf_payees_list
=payees_list
,
446 cf_budget
=child_budget
,
447 cf_total_budget
=child_budget
,
448 cf_nlnet_milestone
="milestone 1",
452 errors
= bg
.get_errors()
453 self
.assertErrorTypesMatches(errors
,
454 expected_fixed_error_types
)
465 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
470 payees_list
="person1=1",
473 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
475 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
480 payees_list
="person1=1",
483 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
485 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
490 payees_list
="person1=10",
493 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
495 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
500 payees_list
="person1=10",
503 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
505 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
513 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
521 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
526 payees_list
="person1=1",
529 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
531 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(100)),
533 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
536 payees_list
="person1=1",
539 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
541 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(95)),
543 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
546 payees_list
="person1=10",
549 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
551 BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(100)),
553 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
556 payees_list
="person1=10",
559 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
561 BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(95)),
563 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
571 payees_list
="person1=1",
574 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(0)),
576 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
579 payees_list
="person1=10",
582 BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(0)),
584 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
590 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
598 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
603 payees_list
="person1=1",
606 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
608 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
610 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
613 payees_list
="person1=1",
616 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
618 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
620 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
623 payees_list
="person1=10",
626 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
631 payees_list
="person1=10",
634 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
644 payees_list
="person1=1",
647 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
649 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
652 payees_list
="person1=10",
660 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
668 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
673 payees_list
="person1=1",
676 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
678 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
680 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
683 payees_list
="person1=1",
686 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
688 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
690 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
693 payees_list
="person1=10",
696 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
701 payees_list
="person1=10",
704 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
714 payees_list
="person1=1",
717 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
719 expected_fixed_error_types
=[BudgetGraphPayeesMoneyMismatch
])
722 payees_list
="person1=10",
728 payees_list
="person1=10",
731 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
735 def test_negative_money(self
):
738 cf_budget_parent
=None,
740 cf_total_budget
="-10",
741 cf_nlnet_milestone
="milestone 1",
745 errors
= bg
.get_errors()
746 self
.assertErrorTypesMatches(errors
, [
747 BudgetGraphNegativeMoney
,
748 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
])
749 self
.assertEqual(errors
[0].bug_id
, 1)
750 self
.assertEqual(errors
[0].root_bug_id
, 1)
751 self
.assertEqual(errors
[1].bug_id
, 1)
752 self
.assertEqual(errors
[1].root_bug_id
, 1)
753 self
.assertEqual(errors
[1].expected_budget_excluding_subtasks
, -10)
756 cf_budget_parent
=None,
759 cf_nlnet_milestone
="milestone 1",
763 errors
= bg
.get_errors()
764 self
.assertErrorTypesMatches(errors
, [
765 BudgetGraphNegativeMoney
,
766 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
])
767 self
.assertEqual(errors
[0].bug_id
, 1)
768 self
.assertEqual(errors
[0].root_bug_id
, 1)
769 self
.assertEqual(errors
[1].bug_id
, 1)
770 self
.assertEqual(errors
[1].root_bug_id
, 1)
771 self
.assertEqual(errors
[1].expected_budget_including_subtasks
, -10)
774 cf_budget_parent
=None,
776 cf_total_budget
="-10",
777 cf_nlnet_milestone
="milestone 1",
781 errors
= bg
.get_errors()
782 self
.assertErrorTypesMatches(errors
,
783 [BudgetGraphNegativeMoney
])
784 self
.assertEqual(errors
[0].bug_id
, 1)
785 self
.assertEqual(errors
[0].root_bug_id
, 1)
787 def test_payees_parse(self
):
788 def check(cf_payees_list
, error_types
, expected_payments
):
789 bg
= BudgetGraph([MockBug(bug_id
=1,
790 cf_budget_parent
=None,
793 cf_nlnet_milestone
="milestone 1",
794 cf_payees_list
=cf_payees_list
,
797 self
.assertErrorTypesMatches(bg
.get_errors(), error_types
)
798 self
.assertEqual(len(bg
.nodes
), 1)
799 node
: Node
= bg
.nodes
[1]
800 self
.assertEqual([str(i
) for i
in node
.payments
.values()],
807 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
808 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
809 ["Payment(node=#1, payee=Person<'person1'>, "
810 "payee_key='person1', amount=123, "
811 "state=NotYetSubmitted, paid=None, submitted=None)"])
816 [BudgetGraphPayeesParseError
,
817 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
818 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
819 ["Payment(node=#1, payee=<unknown person>, payee_key='abc', "
820 "amount=123, state=NotYetSubmitted, paid=None, "
826 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
827 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
828 ["Payment(node=#1, payee=Person<'person1'>, "
829 "payee_key='person1', amount=123.45, "
830 "state=NotYetSubmitted, paid=None, submitted=None)"])
836 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
837 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
838 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
839 'amount=123.45, state=NotYetSubmitted, paid=None, '
841 "Payment(node=#1, payee=Person<'person2'>, payee_key='person 2', "
842 'amount=21.35, state=NotYetSubmitted, paid=None, '
849 [BudgetGraphPayeesParseError
,
850 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
851 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
852 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
853 'amount=123.45, state=NotYetSubmitted, paid=None, '
855 "Payment(node=#1, payee=<unknown person>, payee_key='d e f', "
856 'amount=21.35, state=NotYetSubmitted, paid=None, '
864 [BudgetGraphPayeesParseError
,
865 BudgetGraphNegativePayeeMoney
,
866 BudgetGraphPayeesParseError
,
867 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
868 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
869 ["Payment(node=#1, payee=<unknown person>, payee_key='abc', "
870 'amount=123.45, state=NotYetSubmitted, paid=None, '
872 "Payment(node=#1, payee=<unknown person>, payee_key='AAA', "
873 'amount=-21.35, state=NotYetSubmitted, paid=None, '
877 "not-an-email@example.com" = "-2345"
879 [BudgetGraphNegativePayeeMoney
,
880 BudgetGraphPayeesParseError
,
881 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
882 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
883 ['Payment(node=#1, payee=<unknown person>, '
884 "payee_key='not-an-email@example.com', amount=-2345, "
885 "state=NotYetSubmitted, paid=None, submitted=None)"])
888 person1 = { amount = 123 }
890 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
891 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
892 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
893 "amount=123, state=NotYetSubmitted, paid=None, submitted=None)"])
896 person1 = { amount = 123, submitted = 2020-05-01 }
898 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
899 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
900 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
901 + "amount=123, state=Submitted, paid=None, "
902 + "submitted=2020-05-01)"])
905 person1 = { amount = 123, submitted = 2020-05-01T00:00:00 }
907 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
908 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
909 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
910 + "amount=123, state=Submitted, paid=None, "
911 + "submitted=2020-05-01 00:00:00)"])
914 person1 = { amount = 123, submitted = 2020-05-01T00:00:00Z }
916 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
917 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
918 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
919 + "amount=123, state=Submitted, paid=None, "
920 + "submitted=2020-05-01 00:00:00+00:00)"])
923 person1 = { amount = 123, submitted = 2020-05-01T00:00:00-07:23 }
925 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
926 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
927 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
928 + "amount=123, state=Submitted, paid=None, "
929 + "submitted=2020-05-01 00:00:00-07:23)"])
932 person1 = { amount = 123, paid = 2020-05-01 }
934 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
935 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
936 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
937 + "amount=123, state=Paid, paid=2020-05-01, "
938 + "submitted=None)"])
941 person1 = { amount = 123, paid = 2020-05-01T00:00:00 }
943 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
944 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
945 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
946 + "amount=123, state=Paid, paid=2020-05-01 00:00:00, "
947 + "submitted=None)"])
950 person1 = { amount = 123, paid = 2020-05-01T00:00:00Z }
952 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
953 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
954 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
955 + "amount=123, state=Paid, paid=2020-05-01 00:00:00+00:00, "
956 + "submitted=None)"])
959 person1 = { amount = 123, paid = 2020-05-01T00:00:00-07:23 }
961 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
962 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
963 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
964 + "amount=123, state=Paid, paid=2020-05-01 00:00:00-07:23, "
965 + "submitted=None)"])
970 submitted = 2020-05-23
973 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
974 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
975 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
976 + "amount=123, state=Paid, paid=2020-05-01, "
977 + "submitted=2020-05-23)"])
982 submitted = 2020-05-23
983 paid = 2020-05-01T00:00:00
985 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
986 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
987 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
988 + "amount=123, state=Paid, paid=2020-05-01 00:00:00, "
989 + "submitted=2020-05-23)"])
994 submitted = 2020-05-23
995 paid = 2020-05-01T00:00:00Z
997 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
998 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
999 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1000 + "amount=123, state=Paid, paid=2020-05-01 00:00:00+00:00, "
1001 + "submitted=2020-05-23)"])
1006 submitted = 2020-05-23
1007 paid = 2020-05-01T00:00:00-07:23
1009 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks
,
1010 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks
],
1011 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1012 + "amount=123, state=Paid, paid=2020-05-01 00:00:00-07:23, "
1013 + "submitted=2020-05-23)"])
1015 def test_payees_money_mismatch(self
):
1018 cf_budget_parent
=None,
1020 cf_total_budget
="10",
1021 cf_nlnet_milestone
="milestone 1",
1022 cf_payees_list
="person1 = 5\nperson2 = 10",
1025 errors
= bg
.get_errors()
1026 self
.assertErrorTypesMatches(errors
,
1027 [BudgetGraphPayeesMoneyMismatch
])
1028 self
.assertEqual(errors
[0].bug_id
, 1)
1029 self
.assertEqual(errors
[0].root_bug_id
, 1)
1030 self
.assertEqual(errors
[0].payees_total
, 15)
1032 def test_payees_parse_error(self
):
1033 def check_parse_error(cf_payees_list
, expected_msg
):
1034 errors
= BudgetGraph([
1036 cf_budget_parent
=None,
1038 cf_total_budget
="0",
1039 cf_nlnet_milestone
="milestone 1",
1040 cf_payees_list
=cf_payees_list
,
1042 ], EXAMPLE_CONFIG
).get_errors()
1043 self
.assertErrorTypesMatches(errors
,
1044 [BudgetGraphPayeesParseError
])
1045 self
.assertEqual(errors
[0].bug_id
, 1)
1046 self
.assertEqual(errors
[0].msg
, expected_msg
)
1048 check_parse_error("""
1051 "value for key 'payee 1' is invalid -- it should "
1052 "either be a monetary value or a table")
1054 check_parse_error("""
1057 "failed to parse monetary amount for key 'payee': "
1058 "invalid Money string: characters after sign and "
1059 "before first `.` must be ascii digits")
1061 check_parse_error("""
1065 "TOML parse error: Duplicate keys! (line 3"
1066 " column 1 char 39)")
1068 check_parse_error("""
1071 "failed to parse monetary amount for key 'payee': "
1072 "monetary amount is not a string or integer (to "
1073 "use fractional amounts such as 123.45, write "
1074 "\"123.45\"): 123.45")
1076 check_parse_error("""
1079 "value for key 'payee' is missing the `amount` "
1080 "field which is required")
1082 check_parse_error("""
1083 payee = { amount = 123.45 }
1085 "failed to parse monetary amount for key 'payee': "
1086 "monetary amount is not a string or integer (to "
1087 "use fractional amounts such as 123.45, write "
1088 "\"123.45\"): 123.45")
1090 check_parse_error("""
1091 payee = { amount = 123, blah = false }
1093 "value for key 'payee' has an unknown field: `blah`")
1095 check_parse_error("""
1096 payee = { amount = 123, submitted = false }
1098 "failed to parse `submitted` field for key "
1099 "'payee': invalid date: false")
1101 check_parse_error("""
1102 payee = { amount = 123, submitted = 123 }
1104 "failed to parse `submitted` field for key 'payee':"
1105 " invalid date: 123")
1109 payee = { amount = 123, paid = 2020-01-01, submitted = "abc" }
1111 "failed to parse `submitted` field for key 'payee': "
1112 "invalid date: 'abc'")
1116 payee = { amount = 123, paid = 12:34:56 }
1118 "failed to parse `paid` field for key 'payee': just a time of "
1119 "day by itself is not enough, a date must be included: 12:34:56")
1123 payee = { amount = 123, submitted = 12:34:56.123456 }
1125 "failed to parse `submitted` field for key 'payee': just a time "
1126 "of day by itself is not enough, a date must be included: "
1129 def test_negative_payee_money(self
):
1132 cf_budget_parent
=None,
1134 cf_total_budget
="10",
1135 cf_nlnet_milestone
="milestone 1",
1136 cf_payees_list
="""person1 = -10""",
1139 errors
= bg
.get_errors()
1140 self
.assertErrorTypesMatches(errors
,
1141 [BudgetGraphNegativePayeeMoney
,
1142 BudgetGraphPayeesMoneyMismatch
])
1143 self
.assertEqual(errors
[0].bug_id
, 1)
1144 self
.assertEqual(errors
[0].root_bug_id
, 1)
1145 self
.assertEqual(errors
[0].payee_key
, "person1")
1146 self
.assertEqual(errors
[1].bug_id
, 1)
1147 self
.assertEqual(errors
[1].root_bug_id
, 1)
1148 self
.assertEqual(errors
[1].payees_total
, -10)
1150 def test_duplicate_payments(self
):
1153 cf_budget_parent
=None,
1155 cf_total_budget
="10",
1156 cf_nlnet_milestone
="milestone 1",
1163 errors
= bg
.get_errors()
1164 self
.assertErrorTypesMatches(errors
,
1165 [BudgetGraphDuplicatePayeesForTask
])
1166 self
.assertEqual(errors
[0].bug_id
, 1)
1167 self
.assertEqual(errors
[0].root_bug_id
, 1)
1168 self
.assertEqual(errors
[0].payee1_key
, "person1")
1169 self
.assertEqual(errors
[0].payee2_key
, "alias1")
1171 def test_incorrect_root_for_milestone(self
):
1174 cf_budget_parent
=None,
1176 cf_total_budget
="10",
1177 cf_nlnet_milestone
="milestone 2",
1181 errors
= bg
.get_errors()
1182 self
.assertErrorTypesMatches(errors
,
1183 [BudgetGraphIncorrectRootForMilestone
])
1184 self
.assertEqual(errors
[0].bug_id
, 1)
1185 self
.assertEqual(errors
[0].root_bug_id
, 1)
1186 self
.assertEqual(errors
[0].milestone
, "milestone 2")
1187 self
.assertEqual(errors
[0].milestone_canonical_bug_id
, 2)
1190 cf_budget_parent
=None,
1192 cf_total_budget
="0",
1193 cf_nlnet_milestone
="milestone 2",
1197 errors
= bg
.get_errors()
1198 self
.assertErrorTypesMatches(errors
, [])
1200 def test_payments(self
):
1203 cf_budget_parent
=None,
1205 cf_total_budget
="10",
1206 cf_nlnet_milestone
="milestone 1",
1207 cf_payees_list
="person1 = 3\nperson2 = 7",
1210 cf_budget_parent
=None,
1212 cf_total_budget
="10",
1213 cf_nlnet_milestone
="milestone 2",
1214 cf_payees_list
="person3 = 5\nperson2 = 5",
1217 self
.assertErrorTypesMatches(bg
.get_errors(), [])
1218 person1
= EXAMPLE_CONFIG
.people
["person1"]
1219 person2
= EXAMPLE_CONFIG
.people
["person2"]
1220 person3
= EXAMPLE_CONFIG
.people
["person3"]
1221 milestone1
= EXAMPLE_CONFIG
.milestones
["milestone 1"]
1222 milestone2
= EXAMPLE_CONFIG
.milestones
["milestone 2"]
1223 node1
: Node
= bg
.nodes
[1]
1224 node2
: Node
= bg
.nodes
[2]
1225 node1_payment_person1
= node1
.payments
["person1"]
1226 node1_payment_person2
= node1
.payments
["person2"]
1227 node2_payment_person2
= node2
.payments
["person2"]
1228 node2_payment_person3
= node2
.payments
["person3"]
1229 self
.assertEqual(bg
.payments
,
1232 milestone1
: [node1_payment_person1
],
1236 milestone1
: [node1_payment_person2
],
1237 milestone2
: [node2_payment_person2
],
1241 milestone2
: [node2_payment_person3
],
1245 def test_status(self
):
1246 bg
= BudgetGraph([MockBug(bug_id
=1, status
="blah")],
1248 errors
= bg
.get_errors()
1249 self
.assertErrorTypesMatches(errors
,
1250 [BudgetGraphUnknownStatus
])
1251 self
.assertEqual(errors
[0].bug_id
, 1)
1252 self
.assertEqual(errors
[0].status_str
, "blah")
1253 for status
in BugStatus
:
1254 bg
= BudgetGraph([MockBug(bug_id
=1, status
=status
)],
1256 self
.assertErrorTypesMatches(bg
.get_errors(), [])
1257 self
.assertEqual(bg
.nodes
[1].status
, status
)
1259 def test_assignee(self
):
1260 bg
= BudgetGraph([MockBug(bug_id
=1, assigned_to
="blah")],
1262 errors
= bg
.get_errors()
1263 self
.assertErrorTypesMatches(errors
,
1264 [BudgetGraphUnknownAssignee
])
1265 self
.assertEqual(errors
[0].bug_id
, 1)
1266 self
.assertEqual(errors
[0].assignee
, "blah")
1267 bg
= BudgetGraph([MockBug(bug_id
=1,
1268 assigned_to
="person2@example.com")],
1270 self
.assertErrorTypesMatches(bg
.get_errors(), [])
1271 self
.assertEqual(bg
.nodes
[1].assignee
,
1272 EXAMPLE_CONFIG
.people
["person2"])
1275 if __name__
== "__main__":