add support for reporting the closest task that is in a signed MoU
[utils.git] / src / budget_sync / test / test_budget_graph.py
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 BudgetGraphIncorrectRootForMilestone,
11 BudgetGraphUnknownStatus, BudgetGraphUnknownAssignee,
12 BudgetGraphRootWithMilestoneNotInMoU, BudgetGraphInMoUButParentNotInMoU,
13 BudgetGraphInMoUWithoutMilestone)
14 from budget_sync.money import Money
15 from budget_sync.util import BugStatus
16 from typing import List, Type
17 import unittest
18
19
20 class TestErrorFormatting(unittest.TestCase):
21 def test_budget_graph_incorrect_root_for_milestone(self):
22 self.assertEqual(str(BudgetGraphIncorrectRootForMilestone(
23 2, "milestone 1", 1)),
24 "Bug #2 is not the canonical root bug for assigned milestone "
25 "'milestone 1' but has no parent bug set: the milestone's "
26 "canonical root bug is #1")
27
28 def test_budget_graph_loop_error(self):
29 self.assertEqual(str(BudgetGraphLoopError([1, 2, 3, 4, 5])),
30 "Detected Loop in Budget Graph: #5 -> #1 "
31 "-> #2 -> #3 -> #4 -> #5")
32 self.assertEqual(str(BudgetGraphLoopError([1])),
33 "Detected Loop in Budget Graph: #1 -> #1")
34
35 def test_budget_graph_money_with_no_milestone(self):
36 self.assertEqual(str(BudgetGraphMoneyWithNoMilestone(1, 5)),
37 "Bug assigned money but without any assigned "
38 "milestone: #1")
39
40 def test_budget_graph_milestone_mismatch(self):
41 self.assertEqual(str(BudgetGraphMilestoneMismatch(1, 5)),
42 "Bug's assigned milestone doesn't match the "
43 "milestone assigned to the root bug: descendant "
44 "bug #1, root bug #5")
45
46 def test_budget_graph_unknown_milestone(self):
47 self.assertEqual(str(BudgetGraphUnknownMilestone(
48 123, "fake milestone")),
49 "failed to parse cf_nlnet_milestone field of bug "
50 "#123: unknown milestone: 'fake milestone'")
51
52 def test_budget_graph_unknown_status(self):
53 self.assertEqual(str(BudgetGraphUnknownStatus(
54 123, "fake status")),
55 "failed to parse status field of bug "
56 "#123: unknown status: 'fake status'")
57
58 def test_budget_graph_unknown_assignee(self):
59 self.assertEqual(str(BudgetGraphUnknownAssignee(
60 123, "unknown@example.com")),
61 "Bug #123 is assigned to an unknown person:"
62 " 'unknown@example.com'")
63
64 def test_budget_graph_money_mismatch(self):
65 self.assertEqual(str(
66 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
67 1, 5, "123.4")),
68 "Budget assigned to task excluding subtasks "
69 "(cf_budget field) doesn't match calculated value:"
70 " bug #1, calculated value 123.4")
71 self.assertEqual(str(
72 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
73 1, 5, "123.4")),
74 "Budget assigned to task including subtasks "
75 "(cf_total_budget field) doesn't match calculated value:"
76 " bug #1, calculated value 123.4")
77
78 def test_budget_graph_negative_money(self):
79 self.assertEqual(str(BudgetGraphNegativeMoney(1, 5)),
80 "Budget assigned to task is less than zero: bug #1")
81
82 def test_budget_graph_negative_payee_money(self):
83 self.assertEqual(str(BudgetGraphNegativePayeeMoney(1, 5, "payee1")),
84 "Budget assigned to payee for task is less than "
85 "zero: bug #1, payee 'payee1'")
86
87 def test_budget_graph_payees_parse_error(self):
88 self.assertEqual(str(
89 BudgetGraphPayeesParseError(1, "my fake parse error")),
90 "Failed to parse cf_payees_list field of bug #1: "
91 "my fake parse error")
92
93 def test_budget_graph_payees_money_mismatch(self):
94 self.assertEqual(str(
95 BudgetGraphPayeesMoneyMismatch(1, 5, Money(123), Money(456))),
96 "Total budget assigned to payees (cf_payees_list) doesn't match "
97 "expected value: bug #1, calculated total 123, expected value 456")
98
99 def test_budget_graph_root_with_milestone_not_in_mou(self):
100 self.assertEqual(str(
101 BudgetGraphRootWithMilestoneNotInMoU(1, "milestone 1")),
102 "Bug #1 has no parent bug set and has an assigned milestone "
103 "'milestone 1' but isn't set to be part of the signed MoU")
104
105 def test_budget_graph_in_mou_but_parent_not_in_mou(self):
106 self.assertEqual(str(
107 BudgetGraphInMoUButParentNotInMoU(5, 3, 1, "milestone 1")),
108 "Bug #5 is set to be part of the signed MoU for milestone "
109 "'milestone 1', but its parent bug isn't set to be part of "
110 "the signed MoU")
111
112 def test_budget_graph_in_mou_without_milestone(self):
113 self.assertEqual(str(
114 BudgetGraphInMoUWithoutMilestone(1, 5)),
115 "Bug #1 is set to be part of a signed MoU but has no "
116 "milestone set")
117
118
119 EXAMPLE_BUG1 = MockBug(bug_id=1,
120 cf_budget_parent=None,
121 cf_budget="0",
122 cf_total_budget="0",
123 cf_nlnet_milestone=None,
124 cf_payees_list="",
125 summary="")
126 EXAMPLE_LOOP1_BUG1 = MockBug(bug_id=1,
127 cf_budget_parent=1,
128 cf_budget="0",
129 cf_total_budget="0",
130 cf_nlnet_milestone=None,
131 cf_payees_list="",
132 summary="")
133 EXAMPLE_LOOP2_BUG1 = MockBug(bug_id=1,
134 cf_budget_parent=2,
135 cf_budget="0",
136 cf_total_budget="0",
137 cf_nlnet_milestone=None,
138 cf_payees_list="",
139 summary="")
140 EXAMPLE_LOOP2_BUG2 = MockBug(bug_id=2,
141 cf_budget_parent=1,
142 cf_budget="0",
143 cf_total_budget="0",
144 cf_nlnet_milestone=None,
145 cf_payees_list="",
146 summary="")
147 EXAMPLE_PARENT_BUG1 = MockBug(bug_id=1,
148 cf_budget_parent=None,
149 cf_budget="10",
150 cf_total_budget="20",
151 cf_nlnet_milestone="milestone 1",
152 cf_payees_list="",
153 summary="",
154 cf_is_in_nlnet_mou2="Yes")
155 EXAMPLE_CHILD_BUG2 = MockBug(bug_id=2,
156 cf_budget_parent=1,
157 cf_budget="10",
158 cf_total_budget="10",
159 cf_nlnet_milestone="milestone 1",
160 cf_payees_list="",
161 summary="")
162
163 EXAMPLE_CONFIG = Config.from_str(
164 """
165 bugzilla_url = "https://bugzilla.example.com/"
166 [people."person1"]
167 aliases = ["person1_alias1", "alias1"]
168 full_name = "Person One"
169 [people."person2"]
170 email = "person2@example.com"
171 aliases = ["person1_alias2", "alias2", "person 2"]
172 full_name = "Person Two"
173 [people."person3"]
174 email = "user@example.com"
175 full_name = "Person Three"
176 [milestones]
177 "milestone 1" = { canonical_bug_id = 1 }
178 "milestone 2" = { canonical_bug_id = 2 }
179 """)
180
181
182 class TestBudgetGraph(unittest.TestCase):
183 maxDiff = None
184
185 def assertErrorTypesMatches(self, errors: List[BudgetGraphBaseError], template: List[Type]):
186 def wrap_type_list(type_list: List[Type]):
187 class TypeWrapper:
188 def __init__(self, t):
189 self.t = t
190
191 def __repr__(self):
192 return self.t.__name__
193
194 def __eq__(self, other):
195 return self.t == other.t
196 return [TypeWrapper(i) for i in type_list]
197 error_types = []
198 for error in errors:
199 error_types.append(type(error))
200 self.assertEqual(wrap_type_list(error_types), wrap_type_list(template))
201
202 def test_repr(self):
203 bg = BudgetGraph([EXAMPLE_PARENT_BUG1, EXAMPLE_CHILD_BUG2],
204 EXAMPLE_CONFIG)
205 self.assertEqual(
206 repr(bg),
207 "BudgetGraph{nodes=[Node(graph=..., id=#1, root=#1, parent=None, "
208 "budget_excluding_subtasks=10, budget_including_subtasks=20, "
209 "fixed_budget_excluding_subtasks=10, "
210 "fixed_budget_including_subtasks=20, milestone_str='milestone "
211 "1', is_in_nlnet_mou=True, "
212 "milestone=Milestone(config=..., identifier='milestone 1', "
213 "canonical_bug_id=1), immediate_children=[#2], payments=[], "
214 "status=BugStatus.CONFIRMED, assignee=Person<'person3'>, "
215 "resolved_payments={}, payment_summaries={}), Node(graph=..., "
216 "id=#2, root=#1, parent=#1, budget_excluding_subtasks=10, "
217 "budget_including_subtasks=10, "
218 "fixed_budget_excluding_subtasks=10, "
219 "fixed_budget_including_subtasks=10, milestone_str='milestone "
220 "1', is_in_nlnet_mou=False, "
221 "milestone=Milestone(config=..., identifier='milestone 1', "
222 "canonical_bug_id=1), immediate_children=[], payments=[], "
223 "status=BugStatus.CONFIRMED, assignee=Person<'person3'>, "
224 "resolved_payments={}, payment_summaries={})], roots=[#1], "
225 "assigned_nodes={Person(config=..., identifier='person1', "
226 "full_name='Person One', "
227 "aliases=OrderedSet(['person1_alias1', 'alias1']), email=None): "
228 "[], Person(config=..., identifier='person2', "
229 "full_name='Person Two', "
230 "aliases=OrderedSet(['person1_alias2', 'alias2', 'person 2']), "
231 "email='person2@example.com'): [], Person(config=..., "
232 "identifier='person3', full_name='Person Three', "
233 "aliases=OrderedSet(), email='user@example.com'): [#1, #2]}, "
234 "assigned_nodes_for_milestones={Milestone(config=..., "
235 "identifier='milestone 1', canonical_bug_id=1): [#1, #2], "
236 "Milestone(config=..., identifier='milestone 2', "
237 "canonical_bug_id=2): []}, "
238 "milestone_payments={Milestone(config=..., identifier='milestone "
239 "1', canonical_bug_id=1): [], Milestone(config=..., "
240 "identifier='milestone 2', canonical_bug_id=2): []}, "
241 "payments={Person(config=..., identifier='person1', "
242 "full_name='Person One', "
243 "aliases=OrderedSet(['person1_alias1', 'alias1']), email=None): "
244 "{Milestone(config=..., identifier='milestone 1', "
245 "canonical_bug_id=1): [], Milestone(config=..., "
246 "identifier='milestone 2', canonical_bug_id=2): []}, "
247 "Person(config=..., identifier='person2', "
248 "full_name='Person Two', "
249 "aliases=OrderedSet(['person1_alias2', 'alias2', 'person 2']), "
250 "email='person2@example.com'): {Milestone(config=..., "
251 "identifier='milestone 1', canonical_bug_id=1): [], "
252 "Milestone(config=..., identifier='milestone 2', "
253 "canonical_bug_id=2): []}, Person(config=..., "
254 "identifier='person3', full_name='Person Three', "
255 "aliases=OrderedSet(), email='user@example.com'): "
256 "{Milestone(config=..., identifier='milestone 1', "
257 "canonical_bug_id=1): [], Milestone(config=..., "
258 "identifier='milestone 2', canonical_bug_id=2): []}}, "
259 "milestone_people={Milestone(config=..., identifier='milestone "
260 "1', canonical_bug_id=1): OrderedSet(), Milestone(config=..., "
261 "identifier='milestone 2', canonical_bug_id=2): OrderedSet()}}")
262 bg = BudgetGraph([MockBug(bug_id=1, status="blah",
263 assigned_to="unknown@example.com")],
264 EXAMPLE_CONFIG)
265 self.assertEqual(
266 repr(bg),
267 "BudgetGraph{nodes=[Node(graph=..., id=#1, root=#1, parent=None, "
268 "budget_excluding_subtasks=0, budget_including_subtasks=0, "
269 "fixed_budget_excluding_subtasks=0, "
270 "fixed_budget_including_subtasks=0, milestone_str=None, "
271 "is_in_nlnet_mou=False, "
272 "milestone=None, immediate_children=[], payments=[], "
273 "status=<unknown status: 'blah'>, assignee=<unknown assignee: "
274 "'unknown@example.com'>, resolved_payments={}, "
275 "payment_summaries={})], roots=[#1], assigned_nodes=<failed>, "
276 "assigned_nodes_for_milestones={Milestone(config=..., "
277 "identifier='milestone 1', canonical_bug_id=1): [], "
278 "Milestone(config=..., identifier='milestone 2', "
279 "canonical_bug_id=2): []}, "
280 "milestone_payments={Milestone(config=..., "
281 "identifier='milestone 1', canonical_bug_id=1): [], "
282 "Milestone(config=..., identifier='milestone 2', "
283 "canonical_bug_id=2): []}, payments={Person(config=..., "
284 "identifier='person1', full_name='Person One', "
285 "aliases=OrderedSet(['person1_alias1', 'alias1']), email=None): "
286 "{Milestone(config=..., identifier='milestone 1', "
287 "canonical_bug_id=1): [], Milestone(config=..., "
288 "identifier='milestone 2', canonical_bug_id=2): []}, "
289 "Person(config=..., identifier='person2', "
290 "full_name='Person Two', "
291 "aliases=OrderedSet(['person1_alias2', 'alias2', "
292 "'person 2']), email='person2@example.com'): "
293 "{Milestone(config=..., identifier='milestone 1', "
294 "canonical_bug_id=1): [], Milestone(config=..., "
295 "identifier='milestone 2', canonical_bug_id=2): []}, "
296 "Person(config=..., identifier='person3', "
297 "full_name='Person Three', aliases=OrderedSet(), "
298 "email='user@example.com'): {Milestone(config=..., "
299 "identifier='milestone 1', canonical_bug_id=1): [], "
300 "Milestone(config=..., identifier='milestone 2', "
301 "canonical_bug_id=2): []}}, "
302 "milestone_people={Milestone(config=..., identifier='milestone "
303 "1', canonical_bug_id=1): OrderedSet(), Milestone(config=..., "
304 "identifier='milestone 2', canonical_bug_id=2): OrderedSet()}}")
305 bg = BudgetGraph([MockBug(bug_id=1, status="blah",
306 assigned_to="unknown@example.com",
307 cf_payees_list="""\
308 person1 = {paid=2020-03-15,amount=5}
309 alias1 = {paid=2020-03-15,amount=10}
310 person2 = {submitted=2020-03-15,amount=15}
311 alias2 = {paid=2020-03-16,amount=23}
312 """)],
313 EXAMPLE_CONFIG)
314 self.assertEqual(
315 repr(bg),
316 "BudgetGraph{nodes=[Node(graph=..., id=#1, root=#1, parent=None, "
317 "budget_excluding_subtasks=0, budget_including_subtasks=0, "
318 "fixed_budget_excluding_subtasks=0, "
319 "fixed_budget_including_subtasks=0, milestone_str=None, "
320 "is_in_nlnet_mou=False, "
321 "milestone=None, immediate_children=[], "
322 "payments=[Payment(node=#1, payee=Person<'person1'>, "
323 "payee_key='person1', amount=5, state=Paid, paid=2020-03-15, "
324 "submitted=None), Payment(node=#1, payee=Person<'person1'>, "
325 "payee_key='alias1', amount=10, state=Paid, paid=2020-03-15, "
326 "submitted=None), Payment(node=#1, payee=Person<'person2'>, "
327 "payee_key='person2', amount=15, state=Submitted, paid=None, "
328 "submitted=2020-03-15), Payment(node=#1, "
329 "payee=Person<'person2'>, payee_key='alias2', amount=23, "
330 "state=Paid, paid=2020-03-16, submitted=None)], status=<unknown "
331 "status: 'blah'>, assignee=<unknown assignee: "
332 "'unknown@example.com'>, resolved_payments={Person(config=..., "
333 "identifier='person1', full_name='Person One', "
334 "aliases=OrderedSet(['person1_alias1', 'alias1']), email=None): "
335 "[Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
336 "amount=5, state=Paid, paid=2020-03-15, submitted=None), "
337 "Payment(node=#1, payee=Person<'person1'>, payee_key='alias1', "
338 "amount=10, state=Paid, paid=2020-03-15, submitted=None)], "
339 "Person(config=..., identifier='person2', "
340 "full_name='Person Two', "
341 "aliases=OrderedSet(['person1_alias2', 'alias2', 'person 2']), "
342 "email='person2@example.com'): [Payment(node=#1, "
343 "payee=Person<'person2'>, payee_key='person2', amount=15, "
344 "state=Submitted, paid=None, submitted=2020-03-15), "
345 "Payment(node=#1, payee=Person<'person2'>, payee_key='alias2', "
346 "amount=23, state=Paid, paid=2020-03-16, submitted=None)]}, "
347 "payment_summaries={Person(config=..., identifier='person1', "
348 "full_name='Person One', "
349 "aliases=OrderedSet(['person1_alias1', 'alias1']), email=None): "
350 "PaymentSummary(total=15, total_paid=15, total_submitted=15, "
351 "submitted_date=None, paid_date=2020-03-15, "
352 "state=PaymentSummaryState.Paid, payments=(Payment(node=#1, "
353 "payee=Person<'person1'>, payee_key='person1', amount=5, "
354 "state=Paid, paid=2020-03-15, submitted=None), Payment(node=#1, "
355 "payee=Person<'person1'>, payee_key='alias1', amount=10, "
356 "state=Paid, paid=2020-03-15, submitted=None))), "
357 "Person(config=..., identifier='person2', "
358 "full_name='Person Two', "
359 "aliases=OrderedSet(['person1_alias2', 'alias2', 'person 2']), "
360 "email='person2@example.com'): PaymentSummary(total=38, "
361 "total_paid=23, total_submitted=38, submitted_date=None, "
362 "paid_date=None, state=PaymentSummaryState.Inconsistent, "
363 "payments=(Payment(node=#1, payee=Person<'person2'>, "
364 "payee_key='person2', amount=15, state=Submitted, paid=None, "
365 "submitted=2020-03-15), Payment(node=#1, "
366 "payee=Person<'person2'>, payee_key='alias2', amount=23, "
367 "state=Paid, paid=2020-03-16, submitted=None)))})], roots=[#1], "
368 "assigned_nodes=<failed>, "
369 "assigned_nodes_for_milestones={Milestone(config=..., "
370 "identifier='milestone 1', canonical_bug_id=1): [], "
371 "Milestone(config=..., identifier='milestone 2', "
372 "canonical_bug_id=2): []}, "
373 "milestone_payments={Milestone(config=..., identifier='milestone "
374 "1', canonical_bug_id=1): [], Milestone(config=..., "
375 "identifier='milestone 2', canonical_bug_id=2): []}, "
376 "payments={Person(config=..., identifier='person1', "
377 "full_name='Person One', "
378 "aliases=OrderedSet(['person1_alias1', 'alias1']), email=None): "
379 "{Milestone(config=..., identifier='milestone 1', "
380 "canonical_bug_id=1): [], Milestone(config=..., "
381 "identifier='milestone 2', canonical_bug_id=2): []}, "
382 "Person(config=..., identifier='person2', "
383 "full_name='Person Two', "
384 "aliases=OrderedSet(['person1_alias2', 'alias2', 'person 2']), "
385 "email='person2@example.com'): {Milestone(config=..., "
386 "identifier='milestone 1', canonical_bug_id=1): [], "
387 "Milestone(config=..., identifier='milestone 2', "
388 "canonical_bug_id=2): []}, Person(config=..., "
389 "identifier='person3', full_name='Person Three', "
390 "aliases=OrderedSet(), email='user@example.com'): "
391 "{Milestone(config=..., identifier='milestone 1', "
392 "canonical_bug_id=1): [], Milestone(config=..., "
393 "identifier='milestone 2', canonical_bug_id=2): []}}, "
394 "milestone_people={Milestone(config=..., identifier='milestone "
395 "1', canonical_bug_id=1): OrderedSet(), Milestone(config=..., "
396 "identifier='milestone 2', canonical_bug_id=2): OrderedSet()}}")
397
398 def test_empty(self):
399 bg = BudgetGraph([], EXAMPLE_CONFIG)
400 self.assertEqual(len(bg.nodes), 0)
401 self.assertEqual(len(bg.roots), 0)
402 self.assertIs(bg.config, EXAMPLE_CONFIG)
403
404 def test_single(self):
405 bg = BudgetGraph([EXAMPLE_BUG1], EXAMPLE_CONFIG)
406 self.assertEqual(len(bg.nodes), 1)
407 node: Node = bg.nodes[1]
408 self.assertEqual(bg.roots, {node})
409 self.assertIsInstance(node, Node)
410 self.assertIs(node.graph, bg)
411 self.assertIs(node.bug, EXAMPLE_BUG1)
412 self.assertIs(node.root, node)
413 self.assertIsNone(node.parent_id)
414 self.assertEqual(node.immediate_children, set())
415 self.assertEqual(node.bug_url,
416 "https://bugzilla.example.com/show_bug.cgi?id=1")
417 self.assertEqual(node.budget_excluding_subtasks, Money(cents=0))
418 self.assertEqual(node.budget_including_subtasks, Money(cents=0))
419 self.assertIsNone(node.milestone)
420 self.assertEqual(node.payments, {})
421
422 def test_loop1(self):
423 with self.assertRaises(BudgetGraphLoopError) as cm:
424 BudgetGraph([EXAMPLE_LOOP1_BUG1], EXAMPLE_CONFIG).roots
425 self.assertEqual(cm.exception.bug_ids, [1])
426
427 def test_loop2(self):
428 with self.assertRaises(BudgetGraphLoopError) as cm:
429 BudgetGraph([EXAMPLE_LOOP2_BUG1, EXAMPLE_LOOP2_BUG2],
430 EXAMPLE_CONFIG).roots
431 self.assertEqual(cm.exception.bug_ids, [2, 1])
432
433 def test_parent_child(self):
434 bg = BudgetGraph([EXAMPLE_PARENT_BUG1, EXAMPLE_CHILD_BUG2],
435 EXAMPLE_CONFIG)
436 self.assertEqual(len(bg.nodes), 2)
437 node1: Node = bg.nodes[1]
438 node2: Node = bg.nodes[2]
439 self.assertEqual(bg.roots, {node1})
440 self.assertEqual(node1, node1)
441 self.assertEqual(node2, node2)
442 self.assertNotEqual(node1, node2)
443 self.assertNotEqual(node2, node1)
444 self.assertIsInstance(node1, Node)
445 self.assertIs(node1.graph, bg)
446 self.assertIs(node1.bug, EXAMPLE_PARENT_BUG1)
447 self.assertIsNone(node1.parent_id)
448 self.assertEqual(node1.root, node1)
449 self.assertEqual(node1.immediate_children, {node2})
450 self.assertEqual(node1.budget_excluding_subtasks, Money(cents=1000))
451 self.assertEqual(node1.budget_including_subtasks, Money(cents=2000))
452 self.assertEqual(node1.milestone_str, "milestone 1")
453 self.assertEqual(node1.bug_url,
454 "https://bugzilla.example.com/show_bug.cgi?id=1")
455 self.assertEqual(list(node1.children()), [node2])
456 self.assertEqual(list(node1.children_breadth_first()), [node2])
457 self.assertEqual(node1.payments, {})
458 self.assertIsInstance(node2, Node)
459 self.assertIs(node2.graph, bg)
460 self.assertIs(node2.bug, EXAMPLE_CHILD_BUG2)
461 self.assertEqual(node2.parent_id, 1)
462 self.assertEqual(node2.root, node1)
463 self.assertEqual(node2.immediate_children, set())
464 self.assertEqual(node2.budget_excluding_subtasks, Money(cents=1000))
465 self.assertEqual(node2.budget_including_subtasks, Money(cents=1000))
466 self.assertEqual(node2.milestone_str, "milestone 1")
467 self.assertEqual(node2.payments, {})
468 self.assertEqual(node2.bug_url,
469 "https://bugzilla.example.com/show_bug.cgi?id=2")
470
471 def test_children(self):
472 bg = BudgetGraph([
473 MockBug(bug_id=1,
474 cf_budget_parent=None,
475 cf_budget="0",
476 cf_total_budget="0",
477 cf_nlnet_milestone=None,
478 cf_payees_list="",
479 summary=""),
480 MockBug(bug_id=2,
481 cf_budget_parent=1,
482 cf_budget="0",
483 cf_total_budget="0",
484 cf_nlnet_milestone=None,
485 cf_payees_list="",
486 summary=""),
487 MockBug(bug_id=3,
488 cf_budget_parent=1,
489 cf_budget="0",
490 cf_total_budget="0",
491 cf_nlnet_milestone=None,
492 cf_payees_list="",
493 summary=""),
494 MockBug(bug_id=4,
495 cf_budget_parent=1,
496 cf_budget="0",
497 cf_total_budget="0",
498 cf_nlnet_milestone=None,
499 cf_payees_list="",
500 summary=""),
501 MockBug(bug_id=5,
502 cf_budget_parent=3,
503 cf_budget="0",
504 cf_total_budget="0",
505 cf_nlnet_milestone=None,
506 cf_payees_list="",
507 summary=""),
508 MockBug(bug_id=6,
509 cf_budget_parent=3,
510 cf_budget="0",
511 cf_total_budget="0",
512 cf_nlnet_milestone=None,
513 cf_payees_list="",
514 summary=""),
515 MockBug(bug_id=7,
516 cf_budget_parent=5,
517 cf_budget="0",
518 cf_total_budget="0",
519 cf_nlnet_milestone=None,
520 cf_payees_list="",
521 summary=""),
522 ], EXAMPLE_CONFIG)
523 self.assertEqual(len(bg.nodes), 7)
524 node1: Node = bg.nodes[1]
525 node2: Node = bg.nodes[2]
526 node3: Node = bg.nodes[3]
527 node4: Node = bg.nodes[4]
528 node5: Node = bg.nodes[5]
529 node6: Node = bg.nodes[6]
530 node7: Node = bg.nodes[7]
531 self.assertEqual(bg.roots, {node1})
532 self.assertEqual(list(node1.children()),
533 [node2, node3, node5, node7, node6, node4])
534 self.assertEqual(list(node1.children_breadth_first()),
535 [node2, node3, node4, node5, node6, node7])
536
537 def test_money_with_no_milestone(self):
538 bg = BudgetGraph([
539 MockBug(bug_id=1,
540 cf_budget_parent=None,
541 cf_budget="0",
542 cf_total_budget="10",
543 cf_nlnet_milestone=None,
544 cf_payees_list="",
545 summary=""),
546 ], EXAMPLE_CONFIG)
547 errors = bg.get_errors()
548 self.assertErrorTypesMatches(errors, [
549 BudgetGraphMoneyWithNoMilestone,
550 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks])
551 self.assertEqual(errors[0].bug_id, 1)
552 self.assertEqual(errors[0].root_bug_id, 1)
553 bg = BudgetGraph([
554 MockBug(bug_id=1,
555 cf_budget_parent=None,
556 cf_budget="10",
557 cf_total_budget="0",
558 cf_nlnet_milestone=None,
559 cf_payees_list="",
560 summary=""),
561 ], EXAMPLE_CONFIG)
562 errors = bg.get_errors()
563 self.assertErrorTypesMatches(errors, [
564 BudgetGraphMoneyWithNoMilestone,
565 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks])
566 self.assertEqual(errors[0].bug_id, 1)
567 self.assertEqual(errors[0].root_bug_id, 1)
568 bg = BudgetGraph([
569 MockBug(bug_id=1,
570 cf_budget_parent=None,
571 cf_budget="10",
572 cf_total_budget="10",
573 cf_nlnet_milestone=None,
574 cf_payees_list="",
575 summary=""),
576 ], EXAMPLE_CONFIG)
577 errors = bg.get_errors()
578 self.assertErrorTypesMatches(errors, [BudgetGraphMoneyWithNoMilestone])
579 self.assertEqual(errors[0].bug_id, 1)
580 self.assertEqual(errors[0].root_bug_id, 1)
581
582 def test_money_mismatch(self):
583 def helper(budget, total_budget, payees_list, child_budget,
584 expected_errors, expected_fixed_error_types=None):
585 if expected_fixed_error_types is None:
586 expected_fixed_error_types = []
587 bg = BudgetGraph([
588 MockBug(bug_id=1,
589 cf_budget_parent=None,
590 cf_budget=budget,
591 cf_total_budget=total_budget,
592 cf_nlnet_milestone="milestone 1",
593 cf_payees_list=payees_list,
594 summary="",
595 cf_is_in_nlnet_mou2="Yes"),
596 MockBug(bug_id=2,
597 cf_budget_parent=1,
598 cf_budget=child_budget,
599 cf_total_budget=child_budget,
600 cf_nlnet_milestone="milestone 1",
601 summary=""),
602 ], EXAMPLE_CONFIG)
603 node1: Node = bg.nodes[1]
604 errors = bg.get_errors()
605 self.assertErrorTypesMatches(errors,
606 [type(i) for i in expected_errors])
607 self.assertEqual([str(i) for i in errors],
608 [str(i) for i in expected_errors])
609 bg = BudgetGraph([
610 MockBug(bug_id=1,
611 cf_budget_parent=None,
612 cf_budget=str(node1.fixed_budget_excluding_subtasks),
613 cf_total_budget=str(
614 node1.fixed_budget_including_subtasks),
615 cf_nlnet_milestone="milestone 1",
616 cf_payees_list=payees_list,
617 summary="",
618 cf_is_in_nlnet_mou2="Yes"),
619 MockBug(bug_id=2,
620 cf_budget_parent=1,
621 cf_budget=child_budget,
622 cf_total_budget=child_budget,
623 cf_nlnet_milestone="milestone 1",
624 cf_payees_list="",
625 summary=""),
626 ], EXAMPLE_CONFIG)
627 errors = bg.get_errors()
628 self.assertErrorTypesMatches(errors,
629 expected_fixed_error_types)
630 helper(budget="0",
631 total_budget="0",
632 payees_list="",
633 child_budget="0",
634 expected_errors=[])
635 helper(budget="0",
636 total_budget="0",
637 payees_list="",
638 child_budget="5",
639 expected_errors=[
640 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
641 1, 1, Money(5)),
642 ])
643 helper(budget="0",
644 total_budget="0",
645 payees_list="person1=1",
646 child_budget="0",
647 expected_errors=[
648 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
649 1, 1, Money(1)),
650 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
651 1, 1, Money(1)),
652 ])
653 helper(budget="0",
654 total_budget="0",
655 payees_list="person1=1",
656 child_budget="5",
657 expected_errors=[
658 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
659 1, 1, Money(1)),
660 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
661 1, 1, Money(6)),
662 ])
663 helper(budget="0",
664 total_budget="0",
665 payees_list="person1=10",
666 child_budget="0",
667 expected_errors=[
668 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
669 1, 1, Money(10)),
670 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
671 1, 1, Money(10)),
672 ])
673 helper(budget="0",
674 total_budget="0",
675 payees_list="person1=10",
676 child_budget="5",
677 expected_errors=[
678 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
679 1, 1, Money(10)),
680 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
681 1, 1, Money(15)),
682 ])
683 helper(budget="0",
684 total_budget="100",
685 payees_list="",
686 child_budget="0",
687 expected_errors=[
688 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
689 1, 1, Money(100)),
690 ])
691 helper(budget="0",
692 total_budget="100",
693 payees_list="",
694 child_budget="5",
695 expected_errors=[
696 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
697 1, 1, Money(95)),
698 ])
699 helper(budget="0",
700 total_budget="100",
701 payees_list="person1=1",
702 child_budget="0",
703 expected_errors=[
704 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
705 1, 1, Money(100)),
706 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(100)),
707 ],
708 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
709 helper(budget="0",
710 total_budget="100",
711 payees_list="person1=1",
712 child_budget="5",
713 expected_errors=[
714 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
715 1, 1, Money(95)),
716 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(95)),
717 ],
718 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
719 helper(budget="0",
720 total_budget="100",
721 payees_list="person1=10",
722 child_budget="0",
723 expected_errors=[
724 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
725 1, 1, Money(100)),
726 BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(100)),
727 ],
728 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
729 helper(budget="0",
730 total_budget="100",
731 payees_list="person1=10",
732 child_budget="5",
733 expected_errors=[
734 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
735 1, 1, Money(95)),
736 BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(95)),
737 ],
738 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
739 helper(budget="0",
740 total_budget="5",
741 payees_list="",
742 child_budget="5",
743 expected_errors=[])
744 helper(budget="0",
745 total_budget="5",
746 payees_list="person1=1",
747 child_budget="5",
748 expected_errors=[
749 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(0)),
750 ],
751 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
752 helper(budget="0",
753 total_budget="5",
754 payees_list="person1=10",
755 child_budget="5",
756 expected_errors=[
757 BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(0)),
758 ],
759 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
760 helper(budget="10",
761 total_budget="0",
762 payees_list="",
763 child_budget="0",
764 expected_errors=[
765 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
766 1, 1, Money(10)),
767 ])
768 helper(budget="10",
769 total_budget="0",
770 payees_list="",
771 child_budget="5",
772 expected_errors=[
773 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
774 1, 1, Money(15)),
775 ])
776 helper(budget="10",
777 total_budget="0",
778 payees_list="person1=1",
779 child_budget="0",
780 expected_errors=[
781 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
782 1, 1, Money(10)),
783 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
784 ],
785 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
786 helper(budget="10",
787 total_budget="0",
788 payees_list="person1=1",
789 child_budget="5",
790 expected_errors=[
791 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
792 1, 1, Money(15)),
793 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
794 ],
795 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
796 helper(budget="10",
797 total_budget="0",
798 payees_list="person1=10",
799 child_budget="0",
800 expected_errors=[
801 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
802 1, 1, Money(10)),
803 ])
804 helper(budget="10",
805 total_budget="0",
806 payees_list="person1=10",
807 child_budget="5",
808 expected_errors=[
809 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
810 1, 1, Money(15)),
811 ])
812 helper(budget="10",
813 total_budget="10",
814 payees_list="",
815 child_budget="0",
816 expected_errors=[])
817 helper(budget="10",
818 total_budget="10",
819 payees_list="person1=1",
820 child_budget="0",
821 expected_errors=[
822 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
823 ],
824 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
825 helper(budget="10",
826 total_budget="10",
827 payees_list="person1=10",
828 child_budget="0",
829 expected_errors=[])
830 helper(budget="10",
831 total_budget="100",
832 payees_list="",
833 child_budget="0",
834 expected_errors=[
835 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
836 1, 1, Money(100)),
837 ])
838 helper(budget="10",
839 total_budget="100",
840 payees_list="",
841 child_budget="5",
842 expected_errors=[
843 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
844 1, 1, Money(95)),
845 ])
846 helper(budget="10",
847 total_budget="100",
848 payees_list="person1=1",
849 child_budget="0",
850 expected_errors=[
851 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
852 1, 1, Money(10)),
853 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
854 ],
855 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
856 helper(budget="10",
857 total_budget="100",
858 payees_list="person1=1",
859 child_budget="5",
860 expected_errors=[
861 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
862 1, 1, Money(15)),
863 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
864 ],
865 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
866 helper(budget="10",
867 total_budget="100",
868 payees_list="person1=10",
869 child_budget="0",
870 expected_errors=[
871 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
872 1, 1, Money(10)),
873 ])
874 helper(budget="10",
875 total_budget="100",
876 payees_list="person1=10",
877 child_budget="5",
878 expected_errors=[
879 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
880 1, 1, Money(15)),
881 ])
882 helper(budget="10",
883 total_budget="15",
884 payees_list="",
885 child_budget="5",
886 expected_errors=[])
887 helper(budget="10",
888 total_budget="15",
889 payees_list="person1=1",
890 child_budget="5",
891 expected_errors=[
892 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
893 ],
894 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
895 helper(budget="10",
896 total_budget="15",
897 payees_list="person1=10",
898 child_budget="5",
899 expected_errors=[])
900
901 helper(budget="1",
902 total_budget="15",
903 payees_list="person1=10",
904 child_budget="5",
905 expected_errors=[
906 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
907 1, 1, Money("10"))
908 ])
909
910 def test_negative_money(self):
911 bg = BudgetGraph([
912 MockBug(bug_id=1,
913 cf_budget_parent=None,
914 cf_budget="0",
915 cf_total_budget="-10",
916 cf_nlnet_milestone="milestone 1",
917 cf_payees_list="",
918 summary="",
919 cf_is_in_nlnet_mou2="Yes"),
920 ], EXAMPLE_CONFIG)
921 errors = bg.get_errors()
922 self.assertErrorTypesMatches(errors, [
923 BudgetGraphNegativeMoney,
924 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks])
925 self.assertEqual(errors[0].bug_id, 1)
926 self.assertEqual(errors[0].root_bug_id, 1)
927 self.assertEqual(errors[1].bug_id, 1)
928 self.assertEqual(errors[1].root_bug_id, 1)
929 self.assertEqual(errors[1].expected_budget_including_subtasks, 0)
930 bg = BudgetGraph([
931 MockBug(bug_id=1,
932 cf_budget_parent=None,
933 cf_budget="-10",
934 cf_total_budget="0",
935 cf_nlnet_milestone="milestone 1",
936 cf_payees_list="",
937 summary="",
938 cf_is_in_nlnet_mou2="Yes"),
939 ], EXAMPLE_CONFIG)
940 errors = bg.get_errors()
941 self.assertErrorTypesMatches(errors, [
942 BudgetGraphNegativeMoney,
943 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks])
944 self.assertEqual(errors[0].bug_id, 1)
945 self.assertEqual(errors[0].root_bug_id, 1)
946 self.assertEqual(errors[1].bug_id, 1)
947 self.assertEqual(errors[1].root_bug_id, 1)
948 self.assertEqual(errors[1].expected_budget_including_subtasks, -10)
949 bg = BudgetGraph([
950 MockBug(bug_id=1,
951 cf_budget_parent=None,
952 cf_budget="-10",
953 cf_total_budget="-10",
954 cf_nlnet_milestone="milestone 1",
955 cf_payees_list="",
956 summary="",
957 cf_is_in_nlnet_mou2="Yes"),
958 ], EXAMPLE_CONFIG)
959 errors = bg.get_errors()
960 self.assertErrorTypesMatches(errors,
961 [BudgetGraphNegativeMoney])
962 self.assertEqual(errors[0].bug_id, 1)
963 self.assertEqual(errors[0].root_bug_id, 1)
964
965 def test_payees_parse(self):
966 def check(cf_payees_list, error_types, expected_payments):
967 bg = BudgetGraph([MockBug(bug_id=1,
968 cf_budget_parent=None,
969 cf_budget="0",
970 cf_total_budget="0",
971 cf_nlnet_milestone="milestone 1",
972 cf_payees_list=cf_payees_list,
973 summary="",
974 cf_is_in_nlnet_mou2="Yes"),
975 ], EXAMPLE_CONFIG)
976 self.assertErrorTypesMatches(bg.get_errors(), error_types)
977 self.assertEqual(len(bg.nodes), 1)
978 node: Node = bg.nodes[1]
979 self.assertEqual([str(i) for i in node.payments.values()],
980 expected_payments)
981
982 check(
983 """
984 person1 = 123
985 """,
986 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
987 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
988 ["Payment(node=#1, payee=Person<'person1'>, "
989 "payee_key='person1', amount=123, "
990 "state=NotYetSubmitted, paid=None, submitted=None)"])
991 check(
992 """
993 abc = "123"
994 """,
995 [BudgetGraphPayeesParseError,
996 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
997 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
998 ["Payment(node=#1, payee=<unknown person>, payee_key='abc', "
999 "amount=123, state=NotYetSubmitted, paid=None, "
1000 "submitted=None)"])
1001 check(
1002 """
1003 person1 = "123.45"
1004 """,
1005 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1006 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1007 ["Payment(node=#1, payee=Person<'person1'>, "
1008 "payee_key='person1', amount=123.45, "
1009 "state=NotYetSubmitted, paid=None, submitted=None)"])
1010 check(
1011 """
1012 person1 = "123.45"
1013 "person 2" = "21.35"
1014 """,
1015 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1016 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1017 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1018 'amount=123.45, state=NotYetSubmitted, paid=None, '
1019 'submitted=None)',
1020 "Payment(node=#1, payee=Person<'person2'>, payee_key='person 2', "
1021 'amount=21.35, state=NotYetSubmitted, paid=None, '
1022 'submitted=None)'])
1023 check(
1024 """
1025 person1 = "123.45"
1026 "d e f" = "21.35"
1027 """,
1028 [BudgetGraphPayeesParseError,
1029 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1030 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1031 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1032 'amount=123.45, state=NotYetSubmitted, paid=None, '
1033 'submitted=None)',
1034 "Payment(node=#1, payee=<unknown person>, payee_key='d e f', "
1035 'amount=21.35, state=NotYetSubmitted, paid=None, '
1036 'submitted=None)'])
1037 check(
1038 """
1039 abc = "123.45"
1040 # my comments
1041 "AAA" = "-21.35"
1042 """,
1043 [BudgetGraphPayeesParseError,
1044 BudgetGraphNegativePayeeMoney,
1045 BudgetGraphPayeesParseError,
1046 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1047 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1048 ["Payment(node=#1, payee=<unknown person>, payee_key='abc', "
1049 'amount=123.45, state=NotYetSubmitted, paid=None, '
1050 'submitted=None)',
1051 "Payment(node=#1, payee=<unknown person>, payee_key='AAA', "
1052 'amount=-21.35, state=NotYetSubmitted, paid=None, '
1053 'submitted=None)'])
1054 check(
1055 """
1056 "not-an-email@example.com" = "-2345"
1057 """,
1058 [BudgetGraphNegativePayeeMoney,
1059 BudgetGraphPayeesParseError,
1060 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1061 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1062 ['Payment(node=#1, payee=<unknown person>, '
1063 "payee_key='not-an-email@example.com', amount=-2345, "
1064 "state=NotYetSubmitted, paid=None, submitted=None)"])
1065 check(
1066 """
1067 person1 = { amount = 123 }
1068 """,
1069 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1070 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1071 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1072 "amount=123, state=NotYetSubmitted, paid=None, submitted=None)"])
1073 check(
1074 """
1075 person1 = { amount = 123, submitted = 2020-05-01 }
1076 """,
1077 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1078 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1079 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1080 + "amount=123, state=Submitted, paid=None, "
1081 + "submitted=2020-05-01)"])
1082 check(
1083 """
1084 person1 = { amount = 123, submitted = 2020-05-01T00:00:00 }
1085 """,
1086 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1087 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1088 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1089 + "amount=123, state=Submitted, paid=None, "
1090 + "submitted=2020-05-01 00:00:00)"])
1091 check(
1092 """
1093 person1 = { amount = 123, submitted = 2020-05-01T00:00:00Z }
1094 """,
1095 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1096 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1097 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1098 + "amount=123, state=Submitted, paid=None, "
1099 + "submitted=2020-05-01 00:00:00+00:00)"])
1100 check(
1101 """
1102 person1 = { amount = 123, submitted = 2020-05-01T00:00:00-07:23 }
1103 """,
1104 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1105 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1106 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1107 + "amount=123, state=Submitted, paid=None, "
1108 + "submitted=2020-05-01 00:00:00-07:23)"])
1109 check(
1110 """
1111 person1 = { amount = 123, paid = 2020-05-01 }
1112 """,
1113 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1114 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1115 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1116 + "amount=123, state=Paid, paid=2020-05-01, "
1117 + "submitted=None)"])
1118 check(
1119 """
1120 person1 = { amount = 123, paid = 2020-05-01T00:00:00 }
1121 """,
1122 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1123 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1124 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1125 + "amount=123, state=Paid, paid=2020-05-01 00:00:00, "
1126 + "submitted=None)"])
1127 check(
1128 """
1129 person1 = { amount = 123, paid = 2020-05-01T00:00:00Z }
1130 """,
1131 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1132 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1133 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1134 + "amount=123, state=Paid, paid=2020-05-01 00:00:00+00:00, "
1135 + "submitted=None)"])
1136 check(
1137 """
1138 person1 = { amount = 123, paid = 2020-05-01T00:00:00-07:23 }
1139 """,
1140 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1141 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1142 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1143 + "amount=123, state=Paid, paid=2020-05-01 00:00:00-07:23, "
1144 + "submitted=None)"])
1145 check(
1146 """
1147 [person1]
1148 amount = 123
1149 submitted = 2020-05-23
1150 paid = 2020-05-01
1151 """,
1152 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1153 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1154 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1155 + "amount=123, state=Paid, paid=2020-05-01, "
1156 + "submitted=2020-05-23)"])
1157 check(
1158 """
1159 [person1]
1160 amount = 123
1161 submitted = 2020-05-23
1162 paid = 2020-05-01T00:00:00
1163 """,
1164 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1165 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1166 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1167 + "amount=123, state=Paid, paid=2020-05-01 00:00:00, "
1168 + "submitted=2020-05-23)"])
1169 check(
1170 """
1171 [person1]
1172 amount = 123
1173 submitted = 2020-05-23
1174 paid = 2020-05-01T00:00:00Z
1175 """,
1176 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1177 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1178 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1179 + "amount=123, state=Paid, paid=2020-05-01 00:00:00+00:00, "
1180 + "submitted=2020-05-23)"])
1181 check(
1182 """
1183 [person1]
1184 amount = 123
1185 submitted = 2020-05-23
1186 paid = 2020-05-01T00:00:00-07:23
1187 """,
1188 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
1189 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
1190 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
1191 + "amount=123, state=Paid, paid=2020-05-01 00:00:00-07:23, "
1192 + "submitted=2020-05-23)"])
1193
1194 def test_payees_money_mismatch(self):
1195 bg = BudgetGraph([
1196 MockBug(bug_id=1,
1197 cf_budget_parent=None,
1198 cf_budget="10",
1199 cf_total_budget="10",
1200 cf_nlnet_milestone="milestone 1",
1201 cf_payees_list="person1 = 5\nperson2 = 10",
1202 summary="",
1203 cf_is_in_nlnet_mou2="Yes"),
1204 ], EXAMPLE_CONFIG)
1205 errors = bg.get_errors()
1206 self.assertErrorTypesMatches(errors,
1207 [BudgetGraphPayeesMoneyMismatch])
1208 self.assertEqual(errors[0].bug_id, 1)
1209 self.assertEqual(errors[0].root_bug_id, 1)
1210 self.assertEqual(errors[0].payees_total, 15)
1211
1212 def test_payees_parse_error(self):
1213 def check_parse_error(cf_payees_list, expected_msg):
1214 errors = BudgetGraph([
1215 MockBug(bug_id=1,
1216 cf_budget_parent=None,
1217 cf_budget="0",
1218 cf_total_budget="0",
1219 cf_nlnet_milestone="milestone 1",
1220 cf_payees_list=cf_payees_list,
1221 summary="",
1222 cf_is_in_nlnet_mou2="Yes"),
1223 ], EXAMPLE_CONFIG).get_errors()
1224 self.assertErrorTypesMatches(errors,
1225 [BudgetGraphPayeesParseError])
1226 self.assertEqual(errors[0].bug_id, 1)
1227 self.assertEqual(errors[0].msg, expected_msg)
1228
1229 check_parse_error("""
1230 "payee 1" = []
1231 """,
1232 "value for key 'payee 1' is invalid -- it should "
1233 "either be a monetary value or a table")
1234
1235 check_parse_error("""
1236 payee = "ashjkf"
1237 """,
1238 "failed to parse monetary amount for key 'payee': "
1239 "invalid Money string: characters after sign and "
1240 "before first `.` must be ascii digits")
1241
1242 check_parse_error("""
1243 payee = "1"
1244 payee = "1"
1245 """,
1246 "TOML parse error: Duplicate keys! (line 3"
1247 " column 1 char 39)")
1248
1249 check_parse_error("""
1250 payee = 123.45
1251 """,
1252 "failed to parse monetary amount for key 'payee': "
1253 "monetary amount is not a string or integer (to "
1254 "use fractional amounts such as 123.45, write "
1255 "\"123.45\"): 123.45")
1256
1257 check_parse_error("""
1258 payee = {}
1259 """,
1260 "value for key 'payee' is missing the `amount` "
1261 "field which is required")
1262
1263 check_parse_error("""
1264 payee = { amount = 123.45 }
1265 """,
1266 "failed to parse monetary amount for key 'payee': "
1267 "monetary amount is not a string or integer (to "
1268 "use fractional amounts such as 123.45, write "
1269 "\"123.45\"): 123.45")
1270
1271 check_parse_error("""
1272 payee = { amount = 123, blah = false }
1273 """,
1274 "value for key 'payee' has an unknown field: `blah`")
1275
1276 check_parse_error("""
1277 payee = { amount = 123, submitted = false }
1278 """,
1279 "failed to parse `submitted` field for key "
1280 "'payee': invalid date: false")
1281
1282 check_parse_error("""
1283 payee = { amount = 123, submitted = 123 }
1284 """,
1285 "failed to parse `submitted` field for key 'payee':"
1286 " invalid date: 123")
1287
1288 check_parse_error(
1289 """
1290 payee = { amount = 123, paid = 2020-01-01, submitted = "abc" }
1291 """,
1292 "failed to parse `submitted` field for key 'payee': "
1293 "invalid date: 'abc'")
1294
1295 check_parse_error(
1296 """
1297 payee = { amount = 123, paid = 12:34:56 }
1298 """,
1299 "failed to parse `paid` field for key 'payee': just a time of "
1300 "day by itself is not enough, a date must be included: 12:34:56")
1301
1302 check_parse_error(
1303 """
1304 payee = { amount = 123, submitted = 12:34:56.123456 }
1305 """,
1306 "failed to parse `submitted` field for key 'payee': just a time "
1307 "of day by itself is not enough, a date must be included: "
1308 "12:34:56.123456")
1309
1310 def test_negative_payee_money(self):
1311 bg = BudgetGraph([
1312 MockBug(bug_id=1,
1313 cf_budget_parent=None,
1314 cf_budget="10",
1315 cf_total_budget="10",
1316 cf_nlnet_milestone="milestone 1",
1317 cf_payees_list="""person1 = -10""",
1318 summary="",
1319 cf_is_in_nlnet_mou2="Yes"),
1320 ], EXAMPLE_CONFIG)
1321 errors = bg.get_errors()
1322 self.assertErrorTypesMatches(errors,
1323 [BudgetGraphNegativePayeeMoney,
1324 BudgetGraphPayeesMoneyMismatch])
1325 self.assertEqual(errors[0].bug_id, 1)
1326 self.assertEqual(errors[0].root_bug_id, 1)
1327 self.assertEqual(errors[0].payee_key, "person1")
1328 self.assertEqual(errors[1].bug_id, 1)
1329 self.assertEqual(errors[1].root_bug_id, 1)
1330 self.assertEqual(errors[1].payees_total, -10)
1331
1332 def test_duplicate_payments(self):
1333 bg = BudgetGraph([
1334 MockBug(bug_id=1,
1335 cf_budget_parent=None,
1336 cf_budget="10",
1337 cf_total_budget="10",
1338 cf_nlnet_milestone="milestone 1",
1339 cf_payees_list="""
1340 person1 = 5
1341 alias1 = 5
1342 """,
1343 summary="",
1344 cf_is_in_nlnet_mou2="Yes"),
1345 ], EXAMPLE_CONFIG)
1346 errors = bg.get_errors()
1347 self.assertErrorTypesMatches(errors, [])
1348 person1 = EXAMPLE_CONFIG.people["person1"]
1349 person2 = EXAMPLE_CONFIG.people["person2"]
1350 person3 = EXAMPLE_CONFIG.people["person3"]
1351 milestone1 = EXAMPLE_CONFIG.milestones["milestone 1"]
1352 milestone2 = EXAMPLE_CONFIG.milestones["milestone 2"]
1353 node1: Node = bg.nodes[1]
1354 node1_payment_person1 = node1.payments["person1"]
1355 node1_payment_alias1 = node1.payments["alias1"]
1356 self.assertEqual(bg.payments, {
1357 person1: {
1358 milestone1: [node1_payment_person1, node1_payment_alias1],
1359 milestone2: [],
1360 },
1361 person2: {milestone1: [], milestone2: []},
1362 person3: {milestone1: [], milestone2: []},
1363 })
1364 self.assertEqual(
1365 repr(node1.payment_summaries),
1366 "{Person(config=..., identifier='person1', "
1367 "full_name='Person One', "
1368 "aliases=OrderedSet(['person1_alias1', 'alias1']), email=None): "
1369 "PaymentSummary(total=10, total_paid=0, total_submitted=0, "
1370 "submitted_date=None, paid_date=None, "
1371 "state=PaymentSummaryState.NotYetSubmitted, "
1372 "payments=(Payment(node=#1, payee=Person<'person1'>, "
1373 "payee_key='person1', amount=5, state=NotYetSubmitted, "
1374 "paid=None, submitted=None), Payment(node=#1, "
1375 "payee=Person<'person1'>, payee_key='alias1', amount=5, "
1376 "state=NotYetSubmitted, paid=None, submitted=None)))}")
1377
1378 def test_incorrect_root_for_milestone(self):
1379 bg = BudgetGraph([
1380 MockBug(bug_id=1,
1381 cf_budget_parent=None,
1382 cf_budget="10",
1383 cf_total_budget="10",
1384 cf_nlnet_milestone="milestone 2",
1385 cf_payees_list="",
1386 summary="",
1387 cf_is_in_nlnet_mou2="Yes"),
1388 ], EXAMPLE_CONFIG)
1389 errors = bg.get_errors()
1390 self.assertErrorTypesMatches(errors,
1391 [BudgetGraphIncorrectRootForMilestone])
1392 self.assertEqual(errors[0].bug_id, 1)
1393 self.assertEqual(errors[0].root_bug_id, 1)
1394 self.assertEqual(errors[0].milestone, "milestone 2")
1395 self.assertEqual(errors[0].milestone_canonical_bug_id, 2)
1396 bg = BudgetGraph([
1397 MockBug(bug_id=1,
1398 cf_budget_parent=None,
1399 cf_budget="0",
1400 cf_total_budget="0",
1401 cf_nlnet_milestone="milestone 2",
1402 cf_payees_list="",
1403 summary="",
1404 cf_is_in_nlnet_mou2="Yes"),
1405 ], EXAMPLE_CONFIG)
1406 errors = bg.get_errors()
1407 self.assertErrorTypesMatches(errors, [])
1408
1409 def test_payments(self):
1410 bg = BudgetGraph([
1411 MockBug(bug_id=1,
1412 cf_budget_parent=None,
1413 cf_budget="10",
1414 cf_total_budget="10",
1415 cf_nlnet_milestone="milestone 1",
1416 cf_payees_list="person1 = 3\nperson2 = 7",
1417 summary="",
1418 cf_is_in_nlnet_mou2="Yes"),
1419 MockBug(bug_id=2,
1420 cf_budget_parent=None,
1421 cf_budget="10",
1422 cf_total_budget="10",
1423 cf_nlnet_milestone="milestone 2",
1424 cf_payees_list="person3 = 5\nperson2 = 5",
1425 summary="",
1426 cf_is_in_nlnet_mou2="Yes"),
1427 ], EXAMPLE_CONFIG)
1428 self.assertErrorTypesMatches(bg.get_errors(), [])
1429 person1 = EXAMPLE_CONFIG.people["person1"]
1430 person2 = EXAMPLE_CONFIG.people["person2"]
1431 person3 = EXAMPLE_CONFIG.people["person3"]
1432 milestone1 = EXAMPLE_CONFIG.milestones["milestone 1"]
1433 milestone2 = EXAMPLE_CONFIG.milestones["milestone 2"]
1434 node1: Node = bg.nodes[1]
1435 node2: Node = bg.nodes[2]
1436 node1_payment_person1 = node1.payments["person1"]
1437 node1_payment_person2 = node1.payments["person2"]
1438 node2_payment_person2 = node2.payments["person2"]
1439 node2_payment_person3 = node2.payments["person3"]
1440 self.assertEqual(bg.payments,
1441 {
1442 person1: {
1443 milestone1: [node1_payment_person1],
1444 milestone2: [],
1445 },
1446 person2: {
1447 milestone1: [node1_payment_person2],
1448 milestone2: [node2_payment_person2],
1449 },
1450 person3: {
1451 milestone1: [],
1452 milestone2: [node2_payment_person3],
1453 },
1454 })
1455
1456 def test_status(self):
1457 bg = BudgetGraph([MockBug(bug_id=1, status="blah")],
1458 EXAMPLE_CONFIG)
1459 errors = bg.get_errors()
1460 self.assertErrorTypesMatches(errors,
1461 [BudgetGraphUnknownStatus])
1462 self.assertEqual(errors[0].bug_id, 1)
1463 self.assertEqual(errors[0].status_str, "blah")
1464 for status in BugStatus:
1465 bg = BudgetGraph([MockBug(bug_id=1, status=status)],
1466 EXAMPLE_CONFIG)
1467 self.assertErrorTypesMatches(bg.get_errors(), [])
1468 self.assertEqual(bg.nodes[1].status, status)
1469
1470 def test_assignee(self):
1471 bg = BudgetGraph([MockBug(bug_id=1, assigned_to="blah")],
1472 EXAMPLE_CONFIG)
1473 errors = bg.get_errors()
1474 self.assertErrorTypesMatches(errors,
1475 [BudgetGraphUnknownAssignee])
1476 self.assertEqual(errors[0].bug_id, 1)
1477 self.assertEqual(errors[0].assignee, "blah")
1478 bg = BudgetGraph([MockBug(bug_id=1,
1479 assigned_to="person2@example.com")],
1480 EXAMPLE_CONFIG)
1481 self.assertErrorTypesMatches(bg.get_errors(), [])
1482 self.assertEqual(bg.nodes[1].assignee,
1483 EXAMPLE_CONFIG.people["person2"])
1484
1485 def test_closest_bug_in_mou(self):
1486 bg = BudgetGraph([
1487 MockBug(bug_id=1, cf_nlnet_milestone="milestone 1",
1488 cf_is_in_nlnet_mou2="Yes"),
1489 MockBug(bug_id=2, cf_budget_parent=1,
1490 cf_nlnet_milestone="milestone 1",
1491 cf_is_in_nlnet_mou2="Yes"),
1492 MockBug(bug_id=3, cf_budget_parent=2,
1493 cf_nlnet_milestone="milestone 1",
1494 cf_is_in_nlnet_mou2="Yes"),
1495 MockBug(bug_id=4, cf_budget_parent=2,
1496 cf_nlnet_milestone="milestone 1"),
1497 MockBug(bug_id=5, cf_budget_parent=4,
1498 cf_nlnet_milestone="milestone 1"),
1499 MockBug(bug_id=6),
1500 ], EXAMPLE_CONFIG)
1501 errors = bg.get_errors()
1502 self.assertErrorTypesMatches(errors, [])
1503 self.assertEqual(bg.nodes[1].closest_bug_in_mou, bg.nodes[1])
1504 self.assertEqual(bg.nodes[2].closest_bug_in_mou, bg.nodes[2])
1505 self.assertEqual(bg.nodes[3].closest_bug_in_mou, bg.nodes[3])
1506 self.assertEqual(bg.nodes[4].closest_bug_in_mou, bg.nodes[2])
1507 self.assertEqual(bg.nodes[5].closest_bug_in_mou, bg.nodes[2])
1508 self.assertEqual(bg.nodes[6].closest_bug_in_mou, None)
1509
1510 def test_root_with_milestone_not_in_mou(self):
1511 bg = BudgetGraph([
1512 MockBug(bug_id=1, cf_nlnet_milestone="milestone 1"),
1513 ], EXAMPLE_CONFIG)
1514 errors = bg.get_errors()
1515 self.assertErrorTypesMatches(errors,
1516 [BudgetGraphRootWithMilestoneNotInMoU])
1517 self.assertEqual(errors[0].bug_id, 1)
1518 self.assertEqual(errors[0].root_bug_id, 1)
1519 self.assertEqual(errors[0].milestone, "milestone 1")
1520
1521 def test_budget_graph_in_mou_without_milestone(self):
1522 bg = BudgetGraph([
1523 MockBug(bug_id=1, cf_is_in_nlnet_mou2="Yes"),
1524 ], EXAMPLE_CONFIG)
1525 errors = bg.get_errors()
1526 self.assertErrorTypesMatches(errors,
1527 [BudgetGraphInMoUWithoutMilestone])
1528 self.assertEqual(errors[0].bug_id, 1)
1529 self.assertEqual(errors[0].root_bug_id, 1)
1530
1531 def test_in_mou_but_parent_not_in_mou(self):
1532 bg = BudgetGraph([
1533 MockBug(bug_id=1, cf_nlnet_milestone="milestone 1",
1534 cf_is_in_nlnet_mou2="Yes"),
1535 MockBug(bug_id=2, cf_nlnet_milestone="milestone 1",
1536 cf_budget_parent=1),
1537 MockBug(bug_id=3, cf_nlnet_milestone="milestone 1",
1538 cf_budget_parent=2, cf_is_in_nlnet_mou2="Yes"),
1539 ], EXAMPLE_CONFIG)
1540 errors = bg.get_errors()
1541 self.assertErrorTypesMatches(errors,
1542 [BudgetGraphInMoUButParentNotInMoU])
1543 self.assertEqual(errors[0].bug_id, 3)
1544 self.assertEqual(errors[0].root_bug_id, 1)
1545 self.assertEqual(errors[0].parent_bug_id, 2)
1546
1547
1548 if __name__ == "__main__":
1549 unittest.main()