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