add summary to MockBug
[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 BudgetGraphDuplicatePayeesForTask, BudgetGraphIncorrectRootForMilestone)
11 from budget_sync.money import Money
12 from typing import List, Type
13 import unittest
14
15
16 class TestErrorFormatting(unittest.TestCase):
17 def test_budget_graph_incorrect_root_for_milestone(self):
18 self.assertEqual(str(BudgetGraphIncorrectRootForMilestone(
19 2, "milestone 1", 1)),
20 "Bug #2 is not the canonical root bug for assigned milestone "
21 "'milestone 1' but has no parent bug set: the milestone's "
22 "canonical root bug is #1")
23
24 def test_budget_graph_duplicate_payees_for_task(self):
25 self.assertEqual(str(BudgetGraphDuplicatePayeesForTask(
26 2, 1, "alias1", "alias2")),
27 "Budget assigned to multiple aliases of the same person in a "
28 "single task: bug #2, budget assigned to both 'alias1' "
29 "and 'alias2'")
30
31 def test_budget_graph_loop_error(self):
32 self.assertEqual(str(BudgetGraphLoopError([1, 2, 3, 4, 5])),
33 "Detected Loop in Budget Graph: #5 -> #1 "
34 "-> #2 -> #3 -> #4 -> #5")
35 self.assertEqual(str(BudgetGraphLoopError([1])),
36 "Detected Loop in Budget Graph: #1 -> #1")
37
38 def test_budget_graph_money_with_no_milestone(self):
39 self.assertEqual(str(BudgetGraphMoneyWithNoMilestone(1, 5)),
40 "Bug assigned money but without any assigned "
41 "milestone: #1")
42
43 def test_budget_graph_milestone_mismatch(self):
44 self.assertEqual(str(BudgetGraphMilestoneMismatch(1, 5)),
45 "Bug's assigned milestone doesn't match the "
46 "milestone assigned to the root bug: descendant "
47 "bug #1, root bug #5")
48
49 def test_budget_graph_unknown_milestone(self):
50 self.assertEqual(str(BudgetGraphUnknownMilestone(
51 123, "fake milestone")),
52 "failed to parse cf_nlnet_milestone field of bug "
53 "#123: unknown milestone: 'fake milestone'")
54
55 def test_budget_graph_money_mismatch(self):
56 self.assertEqual(str(
57 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
58 1, 5, "123.4")),
59 "Budget assigned to task excluding subtasks "
60 "(cf_budget field) doesn't match calculated value:"
61 " bug #1, calculated value 123.4")
62 self.assertEqual(str(
63 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
64 1, 5, "123.4")),
65 "Budget assigned to task including subtasks "
66 "(cf_total_budget field) doesn't match calculated value:"
67 " bug #1, calculated value 123.4")
68
69 def test_budget_graph_negative_money(self):
70 self.assertEqual(str(BudgetGraphNegativeMoney(1, 5)),
71 "Budget assigned to task is less than zero: bug #1")
72
73 def test_budget_graph_negative_payee_money(self):
74 self.assertEqual(str(BudgetGraphNegativePayeeMoney(1, 5, "payee1")),
75 "Budget assigned to payee for task is less than "
76 "zero: bug #1, payee 'payee1'")
77
78 def test_budget_graph_payees_parse_error(self):
79 self.assertEqual(str(
80 BudgetGraphPayeesParseError(1, "my fake parse error")),
81 "Failed to parse cf_payees_list field of bug #1: "
82 "my fake parse error")
83
84 def test_budget_graph_payees_money_mismatch(self):
85 self.assertEqual(str(
86 BudgetGraphPayeesMoneyMismatch(1, 5, Money(123), Money(456))),
87 "Total budget assigned to payees (cf_payees_list) doesn't match "
88 "expected value: bug #1, calculated total 123, expected value 456")
89
90
91 EXAMPLE_BUG1 = MockBug(bug_id=1,
92 cf_budget_parent=None,
93 cf_budget="0",
94 cf_total_budget="0",
95 cf_nlnet_milestone=None,
96 cf_payees_list="",
97 summary="")
98 EXAMPLE_LOOP1_BUG1 = MockBug(bug_id=1,
99 cf_budget_parent=1,
100 cf_budget="0",
101 cf_total_budget="0",
102 cf_nlnet_milestone=None,
103 cf_payees_list="",
104 summary="")
105 EXAMPLE_LOOP2_BUG1 = MockBug(bug_id=1,
106 cf_budget_parent=2,
107 cf_budget="0",
108 cf_total_budget="0",
109 cf_nlnet_milestone=None,
110 cf_payees_list="",
111 summary="")
112 EXAMPLE_LOOP2_BUG2 = MockBug(bug_id=2,
113 cf_budget_parent=1,
114 cf_budget="0",
115 cf_total_budget="0",
116 cf_nlnet_milestone=None,
117 cf_payees_list="",
118 summary="")
119 EXAMPLE_PARENT_BUG1 = MockBug(bug_id=1,
120 cf_budget_parent=None,
121 cf_budget="10",
122 cf_total_budget="20",
123 cf_nlnet_milestone="milestone 1",
124 cf_payees_list="",
125 summary="")
126 EXAMPLE_CHILD_BUG2 = MockBug(bug_id=2,
127 cf_budget_parent=1,
128 cf_budget="10",
129 cf_total_budget="10",
130 cf_nlnet_milestone="milestone 1",
131 cf_payees_list="",
132 summary="")
133
134 EXAMPLE_CONFIG = Config.from_str(
135 """
136 bugzilla_url = "https://bugzilla.example.com/"
137 [people."person1"]
138 aliases = ["person1_alias1", "alias1"]
139 output_markdown_file = "person1.mdwn"
140 [people."person2"]
141 aliases = ["person1_alias2", "alias2", "person 2"]
142 output_markdown_file = "person2.mdwn"
143 [people."person3"]
144 output_markdown_file = "person3.mdwn"
145 [milestones]
146 "milestone 1" = { canonical_bug_id = 1 }
147 "milestone 2" = { canonical_bug_id = 2 }
148 """)
149
150
151 class TestBudgetGraph(unittest.TestCase):
152 maxDiff = None
153
154 def assertErrorTypesMatches(self, errors: List[BudgetGraphBaseError], template: List[Type]):
155 def wrap_type_list(type_list: List[Type]):
156 class TypeWrapper:
157 def __init__(self, t):
158 self.t = t
159
160 def __repr__(self):
161 return self.t.__name__
162
163 def __eq__(self, other):
164 return self.t == other.t
165 return [TypeWrapper(i) for i in type_list]
166 error_types = []
167 for error in errors:
168 error_types.append(type(error))
169 self.assertEqual(wrap_type_list(error_types), wrap_type_list(template))
170
171 def test_empty(self):
172 bg = BudgetGraph([], EXAMPLE_CONFIG)
173 self.assertEqual(len(bg.nodes), 0)
174 self.assertEqual(len(bg.roots), 0)
175 self.assertIs(bg.config, EXAMPLE_CONFIG)
176
177 def test_single(self):
178 bg = BudgetGraph([EXAMPLE_BUG1], EXAMPLE_CONFIG)
179 self.assertEqual(len(bg.nodes), 1)
180 node: Node = bg.nodes[1]
181 self.assertEqual(bg.roots, {node})
182 self.assertIsInstance(node, Node)
183 self.assertIs(node.graph, bg)
184 self.assertIs(node.bug, EXAMPLE_BUG1)
185 self.assertIs(node.root, node)
186 self.assertIsNone(node.parent_id)
187 self.assertEqual(node.immediate_children, set())
188 self.assertEqual(node.bug_url,
189 "https://bugzilla.example.com/show_bug.cgi?id=1")
190 self.assertEqual(node.budget_excluding_subtasks, Money(cents=0))
191 self.assertEqual(node.budget_including_subtasks, Money(cents=0))
192 self.assertIsNone(node.milestone)
193 self.assertEqual(node.payments, {})
194
195 def test_loop1(self):
196 with self.assertRaises(BudgetGraphLoopError) as cm:
197 BudgetGraph([EXAMPLE_LOOP1_BUG1], EXAMPLE_CONFIG).roots
198 self.assertEqual(cm.exception.bug_ids, [1])
199
200 def test_loop2(self):
201 with self.assertRaises(BudgetGraphLoopError) as cm:
202 BudgetGraph([EXAMPLE_LOOP2_BUG1, EXAMPLE_LOOP2_BUG2],
203 EXAMPLE_CONFIG).roots
204 self.assertEqual(cm.exception.bug_ids, [2, 1])
205
206 def test_parent_child(self):
207 bg = BudgetGraph([EXAMPLE_PARENT_BUG1, EXAMPLE_CHILD_BUG2],
208 EXAMPLE_CONFIG)
209 self.assertEqual(len(bg.nodes), 2)
210 node1: Node = bg.nodes[1]
211 node2: Node = bg.nodes[2]
212 self.assertEqual(bg.roots, {node1})
213 self.assertEqual(node1, node1)
214 self.assertEqual(node2, node2)
215 self.assertNotEqual(node1, node2)
216 self.assertNotEqual(node2, node1)
217 self.assertIsInstance(node1, Node)
218 self.assertIs(node1.graph, bg)
219 self.assertIs(node1.bug, EXAMPLE_PARENT_BUG1)
220 self.assertIsNone(node1.parent_id)
221 self.assertEqual(node1.root, node1)
222 self.assertEqual(node1.immediate_children, {node2})
223 self.assertEqual(node1.budget_excluding_subtasks, Money(cents=1000))
224 self.assertEqual(node1.budget_including_subtasks, Money(cents=2000))
225 self.assertEqual(node1.milestone_str, "milestone 1")
226 self.assertEqual(node1.bug_url,
227 "https://bugzilla.example.com/show_bug.cgi?id=1")
228 self.assertEqual(list(node1.children()), [node2])
229 self.assertEqual(list(node1.children_breadth_first()), [node2])
230 self.assertEqual(node1.payments, {})
231 self.assertIsInstance(node2, Node)
232 self.assertIs(node2.graph, bg)
233 self.assertIs(node2.bug, EXAMPLE_CHILD_BUG2)
234 self.assertEqual(node2.parent_id, 1)
235 self.assertEqual(node2.root, node1)
236 self.assertEqual(node2.immediate_children, set())
237 self.assertEqual(node2.budget_excluding_subtasks, Money(cents=1000))
238 self.assertEqual(node2.budget_including_subtasks, Money(cents=1000))
239 self.assertEqual(node2.milestone_str, "milestone 1")
240 self.assertEqual(node2.payments, {})
241 self.assertEqual(node2.bug_url,
242 "https://bugzilla.example.com/show_bug.cgi?id=2")
243
244 def test_children(self):
245 bg = BudgetGraph([
246 MockBug(bug_id=1,
247 cf_budget_parent=None,
248 cf_budget="0",
249 cf_total_budget="0",
250 cf_nlnet_milestone=None,
251 cf_payees_list="",
252 summary=""),
253 MockBug(bug_id=2,
254 cf_budget_parent=1,
255 cf_budget="0",
256 cf_total_budget="0",
257 cf_nlnet_milestone=None,
258 cf_payees_list="",
259 summary=""),
260 MockBug(bug_id=3,
261 cf_budget_parent=1,
262 cf_budget="0",
263 cf_total_budget="0",
264 cf_nlnet_milestone=None,
265 cf_payees_list="",
266 summary=""),
267 MockBug(bug_id=4,
268 cf_budget_parent=1,
269 cf_budget="0",
270 cf_total_budget="0",
271 cf_nlnet_milestone=None,
272 cf_payees_list="",
273 summary=""),
274 MockBug(bug_id=5,
275 cf_budget_parent=3,
276 cf_budget="0",
277 cf_total_budget="0",
278 cf_nlnet_milestone=None,
279 cf_payees_list="",
280 summary=""),
281 MockBug(bug_id=6,
282 cf_budget_parent=3,
283 cf_budget="0",
284 cf_total_budget="0",
285 cf_nlnet_milestone=None,
286 cf_payees_list="",
287 summary=""),
288 MockBug(bug_id=7,
289 cf_budget_parent=5,
290 cf_budget="0",
291 cf_total_budget="0",
292 cf_nlnet_milestone=None,
293 cf_payees_list="",
294 summary=""),
295 ], EXAMPLE_CONFIG)
296 self.assertEqual(len(bg.nodes), 7)
297 node1: Node = bg.nodes[1]
298 node2: Node = bg.nodes[2]
299 node3: Node = bg.nodes[3]
300 node4: Node = bg.nodes[4]
301 node5: Node = bg.nodes[5]
302 node6: Node = bg.nodes[6]
303 node7: Node = bg.nodes[7]
304 self.assertEqual(bg.roots, {node1})
305 self.assertEqual(list(node1.children()),
306 [node2, node3, node5, node7, node6, node4])
307 self.assertEqual(list(node1.children_breadth_first()),
308 [node2, node3, node4, node5, node6, node7])
309
310 def test_money_with_no_milestone(self):
311 bg = BudgetGraph([
312 MockBug(bug_id=1,
313 cf_budget_parent=None,
314 cf_budget="0",
315 cf_total_budget="10",
316 cf_nlnet_milestone=None,
317 cf_payees_list="",
318 summary=""),
319 ], EXAMPLE_CONFIG)
320 errors = bg.get_errors()
321 self.assertErrorTypesMatches(errors, [
322 BudgetGraphMoneyWithNoMilestone,
323 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks])
324 self.assertEqual(errors[0].bug_id, 1)
325 self.assertEqual(errors[0].root_bug_id, 1)
326 bg = BudgetGraph([
327 MockBug(bug_id=1,
328 cf_budget_parent=None,
329 cf_budget="10",
330 cf_total_budget="0",
331 cf_nlnet_milestone=None,
332 cf_payees_list="",
333 summary=""),
334 ], EXAMPLE_CONFIG)
335 errors = bg.get_errors()
336 self.assertErrorTypesMatches(errors, [
337 BudgetGraphMoneyWithNoMilestone,
338 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks])
339 self.assertEqual(errors[0].bug_id, 1)
340 self.assertEqual(errors[0].root_bug_id, 1)
341 bg = BudgetGraph([
342 MockBug(bug_id=1,
343 cf_budget_parent=None,
344 cf_budget="10",
345 cf_total_budget="10",
346 cf_nlnet_milestone=None,
347 cf_payees_list="",
348 summary=""),
349 ], EXAMPLE_CONFIG)
350 errors = bg.get_errors()
351 self.assertErrorTypesMatches(errors, [BudgetGraphMoneyWithNoMilestone])
352 self.assertEqual(errors[0].bug_id, 1)
353 self.assertEqual(errors[0].root_bug_id, 1)
354
355 def test_money_mismatch(self):
356 def helper(budget, total_budget, payees_list, child_budget,
357 expected_errors, expected_fixed_error_types=None):
358 if expected_fixed_error_types is None:
359 expected_fixed_error_types = []
360 bg = BudgetGraph([
361 MockBug(bug_id=1,
362 cf_budget_parent=None,
363 cf_budget=budget,
364 cf_total_budget=total_budget,
365 cf_nlnet_milestone="milestone 1",
366 cf_payees_list=payees_list,
367 summary=""),
368 MockBug(bug_id=2,
369 cf_budget_parent=1,
370 cf_budget=child_budget,
371 cf_total_budget=child_budget,
372 cf_nlnet_milestone="milestone 1",
373 cf_payees_list="",
374 summary=""),
375 ], EXAMPLE_CONFIG)
376 node1: Node = bg.nodes[1]
377 errors = bg.get_errors()
378 self.assertErrorTypesMatches(errors,
379 [type(i) for i in expected_errors])
380 self.assertEqual([str(i) for i in errors],
381 [str(i) for i in expected_errors])
382 bg = BudgetGraph([
383 MockBug(bug_id=1,
384 cf_budget_parent=None,
385 cf_budget=str(node1.fixed_budget_excluding_subtasks),
386 cf_total_budget=str(
387 node1.fixed_budget_including_subtasks),
388 cf_nlnet_milestone="milestone 1",
389 cf_payees_list=payees_list,
390 summary=""),
391 MockBug(bug_id=2,
392 cf_budget_parent=1,
393 cf_budget=child_budget,
394 cf_total_budget=child_budget,
395 cf_nlnet_milestone="milestone 1",
396 cf_payees_list="",
397 summary=""),
398 ], EXAMPLE_CONFIG)
399 errors = bg.get_errors()
400 self.assertErrorTypesMatches(errors,
401 expected_fixed_error_types)
402 helper(budget="0",
403 total_budget="0",
404 payees_list="",
405 child_budget="0",
406 expected_errors=[])
407 helper(budget="0",
408 total_budget="0",
409 payees_list="",
410 child_budget="5",
411 expected_errors=[
412 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
413 1, 1, Money(5)),
414 ])
415 helper(budget="0",
416 total_budget="0",
417 payees_list="person1=1",
418 child_budget="0",
419 expected_errors=[
420 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
421 1, 1, Money(1)),
422 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
423 1, 1, Money(1)),
424 ])
425 helper(budget="0",
426 total_budget="0",
427 payees_list="person1=1",
428 child_budget="5",
429 expected_errors=[
430 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
431 1, 1, Money(1)),
432 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
433 1, 1, Money(6)),
434 ])
435 helper(budget="0",
436 total_budget="0",
437 payees_list="person1=10",
438 child_budget="0",
439 expected_errors=[
440 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
441 1, 1, Money(10)),
442 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
443 1, 1, Money(10)),
444 ])
445 helper(budget="0",
446 total_budget="0",
447 payees_list="person1=10",
448 child_budget="5",
449 expected_errors=[
450 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
451 1, 1, Money(10)),
452 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
453 1, 1, Money(15)),
454 ])
455 helper(budget="0",
456 total_budget="100",
457 payees_list="",
458 child_budget="0",
459 expected_errors=[
460 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
461 1, 1, Money(100)),
462 ])
463 helper(budget="0",
464 total_budget="100",
465 payees_list="",
466 child_budget="5",
467 expected_errors=[
468 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
469 1, 1, Money(95)),
470 ])
471 helper(budget="0",
472 total_budget="100",
473 payees_list="person1=1",
474 child_budget="0",
475 expected_errors=[
476 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
477 1, 1, Money(100)),
478 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(100)),
479 ],
480 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
481 helper(budget="0",
482 total_budget="100",
483 payees_list="person1=1",
484 child_budget="5",
485 expected_errors=[
486 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
487 1, 1, Money(95)),
488 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(95)),
489 ],
490 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
491 helper(budget="0",
492 total_budget="100",
493 payees_list="person1=10",
494 child_budget="0",
495 expected_errors=[
496 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
497 1, 1, Money(100)),
498 BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(100)),
499 ],
500 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
501 helper(budget="0",
502 total_budget="100",
503 payees_list="person1=10",
504 child_budget="5",
505 expected_errors=[
506 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
507 1, 1, Money(95)),
508 BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(95)),
509 ],
510 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
511 helper(budget="0",
512 total_budget="5",
513 payees_list="",
514 child_budget="5",
515 expected_errors=[])
516 helper(budget="0",
517 total_budget="5",
518 payees_list="person1=1",
519 child_budget="5",
520 expected_errors=[
521 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(0)),
522 ],
523 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
524 helper(budget="0",
525 total_budget="5",
526 payees_list="person1=10",
527 child_budget="5",
528 expected_errors=[
529 BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(0)),
530 ],
531 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
532 helper(budget="10",
533 total_budget="0",
534 payees_list="",
535 child_budget="0",
536 expected_errors=[
537 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
538 1, 1, Money(10)),
539 ])
540 helper(budget="10",
541 total_budget="0",
542 payees_list="",
543 child_budget="5",
544 expected_errors=[
545 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
546 1, 1, Money(15)),
547 ])
548 helper(budget="10",
549 total_budget="0",
550 payees_list="person1=1",
551 child_budget="0",
552 expected_errors=[
553 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
554 1, 1, Money(10)),
555 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
556 ],
557 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
558 helper(budget="10",
559 total_budget="0",
560 payees_list="person1=1",
561 child_budget="5",
562 expected_errors=[
563 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
564 1, 1, Money(15)),
565 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
566 ],
567 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
568 helper(budget="10",
569 total_budget="0",
570 payees_list="person1=10",
571 child_budget="0",
572 expected_errors=[
573 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
574 1, 1, Money(10)),
575 ])
576 helper(budget="10",
577 total_budget="0",
578 payees_list="person1=10",
579 child_budget="5",
580 expected_errors=[
581 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
582 1, 1, Money(15)),
583 ])
584 helper(budget="10",
585 total_budget="10",
586 payees_list="",
587 child_budget="0",
588 expected_errors=[])
589 helper(budget="10",
590 total_budget="10",
591 payees_list="person1=1",
592 child_budget="0",
593 expected_errors=[
594 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
595 ],
596 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
597 helper(budget="10",
598 total_budget="10",
599 payees_list="person1=10",
600 child_budget="0",
601 expected_errors=[])
602 helper(budget="10",
603 total_budget="100",
604 payees_list="",
605 child_budget="0",
606 expected_errors=[
607 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
608 1, 1, Money(100)),
609 ])
610 helper(budget="10",
611 total_budget="100",
612 payees_list="",
613 child_budget="5",
614 expected_errors=[
615 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
616 1, 1, Money(95)),
617 ])
618 helper(budget="10",
619 total_budget="100",
620 payees_list="person1=1",
621 child_budget="0",
622 expected_errors=[
623 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
624 1, 1, Money(10)),
625 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
626 ],
627 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
628 helper(budget="10",
629 total_budget="100",
630 payees_list="person1=1",
631 child_budget="5",
632 expected_errors=[
633 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
634 1, 1, Money(15)),
635 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
636 ],
637 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
638 helper(budget="10",
639 total_budget="100",
640 payees_list="person1=10",
641 child_budget="0",
642 expected_errors=[
643 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
644 1, 1, Money(10)),
645 ])
646 helper(budget="10",
647 total_budget="100",
648 payees_list="person1=10",
649 child_budget="5",
650 expected_errors=[
651 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
652 1, 1, Money(15)),
653 ])
654 helper(budget="10",
655 total_budget="15",
656 payees_list="",
657 child_budget="5",
658 expected_errors=[])
659 helper(budget="10",
660 total_budget="15",
661 payees_list="person1=1",
662 child_budget="5",
663 expected_errors=[
664 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
665 ],
666 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
667 helper(budget="10",
668 total_budget="15",
669 payees_list="person1=10",
670 child_budget="5",
671 expected_errors=[])
672
673 helper(budget="1",
674 total_budget="15",
675 payees_list="person1=10",
676 child_budget="5",
677 expected_errors=[
678 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
679 1, 1, Money("10"))
680 ])
681
682 def test_negative_money(self):
683 bg = BudgetGraph([
684 MockBug(bug_id=1,
685 cf_budget_parent=None,
686 cf_budget="0",
687 cf_total_budget="-10",
688 cf_nlnet_milestone="milestone 1",
689 cf_payees_list="",
690 summary=""),
691 ], EXAMPLE_CONFIG)
692 errors = bg.get_errors()
693 self.assertErrorTypesMatches(errors, [
694 BudgetGraphNegativeMoney,
695 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks])
696 self.assertEqual(errors[0].bug_id, 1)
697 self.assertEqual(errors[0].root_bug_id, 1)
698 self.assertEqual(errors[1].bug_id, 1)
699 self.assertEqual(errors[1].root_bug_id, 1)
700 self.assertEqual(errors[1].expected_budget_excluding_subtasks, -10)
701 bg = BudgetGraph([
702 MockBug(bug_id=1,
703 cf_budget_parent=None,
704 cf_budget="-10",
705 cf_total_budget="0",
706 cf_nlnet_milestone="milestone 1",
707 cf_payees_list="",
708 summary=""),
709 ], EXAMPLE_CONFIG)
710 errors = bg.get_errors()
711 self.assertErrorTypesMatches(errors, [
712 BudgetGraphNegativeMoney,
713 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks])
714 self.assertEqual(errors[0].bug_id, 1)
715 self.assertEqual(errors[0].root_bug_id, 1)
716 self.assertEqual(errors[1].bug_id, 1)
717 self.assertEqual(errors[1].root_bug_id, 1)
718 self.assertEqual(errors[1].expected_budget_including_subtasks, -10)
719 bg = BudgetGraph([
720 MockBug(bug_id=1,
721 cf_budget_parent=None,
722 cf_budget="-10",
723 cf_total_budget="-10",
724 cf_nlnet_milestone="milestone 1",
725 cf_payees_list="",
726 summary=""),
727 ], EXAMPLE_CONFIG)
728 errors = bg.get_errors()
729 self.assertErrorTypesMatches(errors,
730 [BudgetGraphNegativeMoney])
731 self.assertEqual(errors[0].bug_id, 1)
732 self.assertEqual(errors[0].root_bug_id, 1)
733
734 def test_payees_parse(self):
735 def check(cf_payees_list, error_types, expected_payments):
736 bg = BudgetGraph([MockBug(bug_id=1,
737 cf_budget_parent=None,
738 cf_budget="0",
739 cf_total_budget="0",
740 cf_nlnet_milestone="milestone 1",
741 cf_payees_list=cf_payees_list,
742 summary=""),
743 ], EXAMPLE_CONFIG)
744 self.assertErrorTypesMatches(bg.get_errors(), error_types)
745 self.assertEqual(len(bg.nodes), 1)
746 node: Node = bg.nodes[1]
747 self.assertEqual([str(i) for i in node.payments.values()],
748 expected_payments)
749
750 check(
751 """
752 person1 = 123
753 """,
754 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
755 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
756 ["Payment(node=#1, payee=Person<'person1'>, "
757 "payee_key='person1', amount=123, "
758 "state=NotYetSubmitted, paid=None, submitted=None)"])
759 check(
760 """
761 abc = "123"
762 """,
763 [BudgetGraphPayeesParseError,
764 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
765 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
766 ["Payment(node=#1, payee=<unknown person>, payee_key='abc', "
767 "amount=123, state=NotYetSubmitted, paid=None, "
768 "submitted=None)"])
769 check(
770 """
771 person1 = "123.45"
772 """,
773 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
774 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
775 ["Payment(node=#1, payee=Person<'person1'>, "
776 "payee_key='person1', amount=123.45, "
777 "state=NotYetSubmitted, paid=None, submitted=None)"])
778 check(
779 """
780 person1 = "123.45"
781 "person 2" = "21.35"
782 """,
783 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
784 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
785 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
786 'amount=123.45, state=NotYetSubmitted, paid=None, '
787 'submitted=None)',
788 "Payment(node=#1, payee=Person<'person2'>, payee_key='person 2', "
789 'amount=21.35, state=NotYetSubmitted, paid=None, '
790 'submitted=None)'])
791 check(
792 """
793 person1 = "123.45"
794 "d e f" = "21.35"
795 """,
796 [BudgetGraphPayeesParseError,
797 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
798 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
799 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
800 'amount=123.45, state=NotYetSubmitted, paid=None, '
801 'submitted=None)',
802 "Payment(node=#1, payee=<unknown person>, payee_key='d e f', "
803 'amount=21.35, state=NotYetSubmitted, paid=None, '
804 'submitted=None)'])
805 check(
806 """
807 abc = "123.45"
808 # my comments
809 "AAA" = "-21.35"
810 """,
811 [BudgetGraphPayeesParseError,
812 BudgetGraphNegativePayeeMoney,
813 BudgetGraphPayeesParseError,
814 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
815 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
816 ["Payment(node=#1, payee=<unknown person>, payee_key='abc', "
817 'amount=123.45, state=NotYetSubmitted, paid=None, '
818 'submitted=None)',
819 "Payment(node=#1, payee=<unknown person>, payee_key='AAA', "
820 'amount=-21.35, state=NotYetSubmitted, paid=None, '
821 'submitted=None)'])
822 check(
823 """
824 "not-an-email@example.com" = "-2345"
825 """,
826 [BudgetGraphNegativePayeeMoney,
827 BudgetGraphPayeesParseError,
828 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
829 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
830 ['Payment(node=#1, payee=<unknown person>, '
831 "payee_key='not-an-email@example.com', amount=-2345, "
832 "state=NotYetSubmitted, paid=None, submitted=None)"])
833 check(
834 """
835 person1 = { amount = 123 }
836 """,
837 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
838 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
839 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
840 "amount=123, state=NotYetSubmitted, paid=None, submitted=None)"])
841 check(
842 """
843 person1 = { amount = 123, submitted = 2020-05-01 }
844 """,
845 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
846 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
847 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
848 + "amount=123, state=Submitted, paid=None, "
849 + "submitted=2020-05-01)"])
850 check(
851 """
852 person1 = { amount = 123, submitted = 2020-05-01T00:00:00 }
853 """,
854 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
855 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
856 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
857 + "amount=123, state=Submitted, paid=None, "
858 + "submitted=2020-05-01 00:00:00)"])
859 check(
860 """
861 person1 = { amount = 123, submitted = 2020-05-01T00:00:00Z }
862 """,
863 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
864 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
865 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
866 + "amount=123, state=Submitted, paid=None, "
867 + "submitted=2020-05-01 00:00:00+00:00)"])
868 check(
869 """
870 person1 = { amount = 123, submitted = 2020-05-01T00:00:00-07:23 }
871 """,
872 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
873 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
874 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
875 + "amount=123, state=Submitted, paid=None, "
876 + "submitted=2020-05-01 00:00:00-07:23)"])
877 check(
878 """
879 person1 = { amount = 123, paid = 2020-05-01 }
880 """,
881 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
882 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
883 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
884 + "amount=123, state=Paid, paid=2020-05-01, "
885 + "submitted=None)"])
886 check(
887 """
888 person1 = { amount = 123, paid = 2020-05-01T00:00:00 }
889 """,
890 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
891 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
892 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
893 + "amount=123, state=Paid, paid=2020-05-01 00:00:00, "
894 + "submitted=None)"])
895 check(
896 """
897 person1 = { amount = 123, paid = 2020-05-01T00:00:00Z }
898 """,
899 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
900 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
901 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
902 + "amount=123, state=Paid, paid=2020-05-01 00:00:00+00:00, "
903 + "submitted=None)"])
904 check(
905 """
906 person1 = { amount = 123, paid = 2020-05-01T00:00:00-07:23 }
907 """,
908 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
909 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
910 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
911 + "amount=123, state=Paid, paid=2020-05-01 00:00:00-07:23, "
912 + "submitted=None)"])
913 check(
914 """
915 [person1]
916 amount = 123
917 submitted = 2020-05-23
918 paid = 2020-05-01
919 """,
920 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
921 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
922 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
923 + "amount=123, state=Paid, paid=2020-05-01, "
924 + "submitted=2020-05-23)"])
925 check(
926 """
927 [person1]
928 amount = 123
929 submitted = 2020-05-23
930 paid = 2020-05-01T00:00:00
931 """,
932 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
933 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
934 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
935 + "amount=123, state=Paid, paid=2020-05-01 00:00:00, "
936 + "submitted=2020-05-23)"])
937 check(
938 """
939 [person1]
940 amount = 123
941 submitted = 2020-05-23
942 paid = 2020-05-01T00:00:00Z
943 """,
944 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
945 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
946 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
947 + "amount=123, state=Paid, paid=2020-05-01 00:00:00+00:00, "
948 + "submitted=2020-05-23)"])
949 check(
950 """
951 [person1]
952 amount = 123
953 submitted = 2020-05-23
954 paid = 2020-05-01T00:00:00-07:23
955 """,
956 [BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
957 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks],
958 ["Payment(node=#1, payee=Person<'person1'>, payee_key='person1', "
959 + "amount=123, state=Paid, paid=2020-05-01 00:00:00-07:23, "
960 + "submitted=2020-05-23)"])
961
962 def test_payees_money_mismatch(self):
963 bg = BudgetGraph([
964 MockBug(bug_id=1,
965 cf_budget_parent=None,
966 cf_budget="10",
967 cf_total_budget="10",
968 cf_nlnet_milestone="milestone 1",
969 cf_payees_list="person1 = 5\nperson2 = 10",
970 summary=""),
971 ], EXAMPLE_CONFIG)
972 errors = bg.get_errors()
973 self.assertErrorTypesMatches(errors,
974 [BudgetGraphPayeesMoneyMismatch])
975 self.assertEqual(errors[0].bug_id, 1)
976 self.assertEqual(errors[0].root_bug_id, 1)
977 self.assertEqual(errors[0].payees_total, 15)
978
979 def test_payees_parse_error(self):
980 def check_parse_error(cf_payees_list, expected_msg):
981 errors = BudgetGraph([
982 MockBug(bug_id=1,
983 cf_budget_parent=None,
984 cf_budget="0",
985 cf_total_budget="0",
986 cf_nlnet_milestone="milestone 1",
987 cf_payees_list=cf_payees_list,
988 summary=""),
989 ], EXAMPLE_CONFIG).get_errors()
990 self.assertErrorTypesMatches(errors,
991 [BudgetGraphPayeesParseError])
992 self.assertEqual(errors[0].bug_id, 1)
993 self.assertEqual(errors[0].msg, expected_msg)
994
995 check_parse_error("""
996 "payee 1" = []
997 """,
998 "value for key 'payee 1' is invalid -- it should "
999 "either be a monetary value or a table")
1000
1001 check_parse_error("""
1002 payee = "ashjkf"
1003 """,
1004 "failed to parse monetary amount for key 'payee': "
1005 "invalid Money string: characters after sign and "
1006 "before first `.` must be ascii digits")
1007
1008 check_parse_error("""
1009 payee = "1"
1010 payee = "1"
1011 """,
1012 "TOML parse error: Duplicate keys! (line 3"
1013 " column 1 char 39)")
1014
1015 check_parse_error("""
1016 payee = 123.45
1017 """,
1018 "failed to parse monetary amount for key 'payee': "
1019 "monetary amount is not a string or integer (to "
1020 "use fractional amounts such as 123.45, write "
1021 "\"123.45\"): 123.45")
1022
1023 check_parse_error("""
1024 payee = {}
1025 """,
1026 "value for key 'payee' is missing the `amount` "
1027 "field which is required")
1028
1029 check_parse_error("""
1030 payee = { amount = 123.45 }
1031 """,
1032 "failed to parse monetary amount for key 'payee': "
1033 "monetary amount is not a string or integer (to "
1034 "use fractional amounts such as 123.45, write "
1035 "\"123.45\"): 123.45")
1036
1037 check_parse_error("""
1038 payee = { amount = 123, blah = false }
1039 """,
1040 "value for key 'payee' has an unknown field: `blah`")
1041
1042 check_parse_error("""
1043 payee = { amount = 123, submitted = false }
1044 """,
1045 "failed to parse `submitted` field for key "
1046 "'payee': invalid date: false")
1047
1048 check_parse_error("""
1049 payee = { amount = 123, submitted = 123 }
1050 """,
1051 "failed to parse `submitted` field for key 'payee':"
1052 " invalid date: 123")
1053
1054 check_parse_error(
1055 """
1056 payee = { amount = 123, paid = 2020-01-01, submitted = "abc" }
1057 """,
1058 "failed to parse `submitted` field for key 'payee': "
1059 "invalid date: 'abc'")
1060
1061 check_parse_error(
1062 """
1063 payee = { amount = 123, paid = 12:34:56 }
1064 """,
1065 "failed to parse `paid` field for key 'payee': just a time of "
1066 "day by itself is not enough, a date must be included: 12:34:56")
1067
1068 check_parse_error(
1069 """
1070 payee = { amount = 123, submitted = 12:34:56.123456 }
1071 """,
1072 "failed to parse `submitted` field for key 'payee': just a time "
1073 "of day by itself is not enough, a date must be included: "
1074 "12:34:56.123456")
1075
1076 def test_negative_payee_money(self):
1077 bg = BudgetGraph([
1078 MockBug(bug_id=1,
1079 cf_budget_parent=None,
1080 cf_budget="10",
1081 cf_total_budget="10",
1082 cf_nlnet_milestone="milestone 1",
1083 cf_payees_list="""person1 = -10""",
1084 summary=""),
1085 ], EXAMPLE_CONFIG)
1086 errors = bg.get_errors()
1087 self.assertErrorTypesMatches(errors,
1088 [BudgetGraphNegativePayeeMoney,
1089 BudgetGraphPayeesMoneyMismatch])
1090 self.assertEqual(errors[0].bug_id, 1)
1091 self.assertEqual(errors[0].root_bug_id, 1)
1092 self.assertEqual(errors[0].payee_key, "person1")
1093 self.assertEqual(errors[1].bug_id, 1)
1094 self.assertEqual(errors[1].root_bug_id, 1)
1095 self.assertEqual(errors[1].payees_total, -10)
1096
1097 def test_duplicate_payments(self):
1098 bg = BudgetGraph([
1099 MockBug(bug_id=1,
1100 cf_budget_parent=None,
1101 cf_budget="10",
1102 cf_total_budget="10",
1103 cf_nlnet_milestone="milestone 1",
1104 cf_payees_list="""
1105 person1 = 5
1106 alias1 = 5
1107 """,
1108 summary=""),
1109 ], EXAMPLE_CONFIG)
1110 errors = bg.get_errors()
1111 self.assertErrorTypesMatches(errors,
1112 [BudgetGraphDuplicatePayeesForTask])
1113 self.assertEqual(errors[0].bug_id, 1)
1114 self.assertEqual(errors[0].root_bug_id, 1)
1115 self.assertEqual(errors[0].payee1_key, "person1")
1116 self.assertEqual(errors[0].payee2_key, "alias1")
1117
1118 def test_incorrect_root_for_milestone(self):
1119 bg = BudgetGraph([
1120 MockBug(bug_id=1,
1121 cf_budget_parent=None,
1122 cf_budget="10",
1123 cf_total_budget="10",
1124 cf_nlnet_milestone="milestone 2",
1125 cf_payees_list="",
1126 summary=""),
1127 ], EXAMPLE_CONFIG)
1128 errors = bg.get_errors()
1129 self.assertErrorTypesMatches(errors,
1130 [BudgetGraphIncorrectRootForMilestone])
1131 self.assertEqual(errors[0].bug_id, 1)
1132 self.assertEqual(errors[0].root_bug_id, 1)
1133 self.assertEqual(errors[0].milestone, "milestone 2")
1134 self.assertEqual(errors[0].milestone_canonical_bug_id, 2)
1135 bg = BudgetGraph([
1136 MockBug(bug_id=1,
1137 cf_budget_parent=None,
1138 cf_budget="0",
1139 cf_total_budget="0",
1140 cf_nlnet_milestone="milestone 2",
1141 cf_payees_list="",
1142 summary=""),
1143 ], EXAMPLE_CONFIG)
1144 errors = bg.get_errors()
1145 self.assertErrorTypesMatches(errors, [])
1146
1147 def test_payments(self):
1148 bg = BudgetGraph([
1149 MockBug(bug_id=1,
1150 cf_budget_parent=None,
1151 cf_budget="10",
1152 cf_total_budget="10",
1153 cf_nlnet_milestone="milestone 1",
1154 cf_payees_list="person1 = 3\nperson2 = 7",
1155 summary=""),
1156 MockBug(bug_id=2,
1157 cf_budget_parent=None,
1158 cf_budget="10",
1159 cf_total_budget="10",
1160 cf_nlnet_milestone="milestone 2",
1161 cf_payees_list="person3 = 5\nperson2 = 5",
1162 summary=""),
1163 ], EXAMPLE_CONFIG)
1164 self.assertErrorTypesMatches(bg.get_errors(), [])
1165 person1 = EXAMPLE_CONFIG.people["person1"]
1166 person2 = EXAMPLE_CONFIG.people["person2"]
1167 person3 = EXAMPLE_CONFIG.people["person3"]
1168 milestone1 = EXAMPLE_CONFIG.milestones["milestone 1"]
1169 milestone2 = EXAMPLE_CONFIG.milestones["milestone 2"]
1170 node1: Node = bg.nodes[1]
1171 node2: Node = bg.nodes[2]
1172 node1_payment_person1 = node1.payments["person1"]
1173 node1_payment_person2 = node1.payments["person2"]
1174 node2_payment_person2 = node2.payments["person2"]
1175 node2_payment_person3 = node2.payments["person3"]
1176 self.assertEqual(bg.payments,
1177 {
1178 person1: {
1179 milestone1: [node1_payment_person1],
1180 milestone2: [],
1181 },
1182 person2: {
1183 milestone1: [node1_payment_person2],
1184 milestone2: [node2_payment_person2],
1185 },
1186 person3: {
1187 milestone1: [],
1188 milestone2: [node2_payment_person3],
1189 },
1190 })
1191
1192
1193 if __name__ == "__main__":
1194 unittest.main()