rewrite heuristics for which fields of bugs should change when they are inconsistent
[utils.git] / src / budget_sync / test / test_budget_graph.py
1 from budget_sync.test.mock_bug import MockBug
2 from budget_sync.budget_graph import (BudgetGraphLoopError, BudgetGraph,
3 Node, BudgetGraphMoneyWithNoMilestone,
4 BudgetGraphBaseError,
5 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
6 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks,
7 BudgetGraphNegativeMoney,
8 BudgetGraphMilestoneMismatch,
9 BudgetGraphNegativePayeeMoney,
10 BudgetGraphPayeesParseError,
11 BudgetGraphPayeesMoneyMismatch)
12 from budget_sync.money import Money
13 from typing import List, Type
14 import unittest
15
16
17 class TestErrorFormatting(unittest.TestCase):
18 def test_budget_graph_loop_error(self):
19 self.assertEqual(str(BudgetGraphLoopError([1, 2, 3, 4, 5])),
20 "Detected Loop in Budget Graph: #5 -> #1 "
21 "-> #2 -> #3 -> #4 -> #5")
22 self.assertEqual(str(BudgetGraphLoopError([1])),
23 "Detected Loop in Budget Graph: #1 -> #1")
24
25 def test_budget_graph_money_with_no_milestone(self):
26 self.assertEqual(str(BudgetGraphMoneyWithNoMilestone(1, 5)),
27 "Bug assigned money but without any assigned "
28 "milestone: #1")
29
30 def test_budget_graph_milestone_mismatch(self):
31 self.assertEqual(str(BudgetGraphMilestoneMismatch(1, 5)),
32 "Bug's assigned milestone doesn't match the "
33 "milestone assigned to the root bug: descendant "
34 "bug #1, root bug #5")
35
36 def test_budget_graph_money_mismatch(self):
37 self.assertEqual(str(
38 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
39 1, 5, "123.4")),
40 "Budget assigned to task excluding subtasks "
41 "(cf_budget field) doesn't match calculated value:"
42 " bug #1, calculated value 123.4")
43 self.assertEqual(str(
44 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
45 1, 5, "123.4")),
46 "Budget assigned to task including subtasks "
47 "(cf_total_budget field) doesn't match calculated value:"
48 " bug #1, calculated value 123.4")
49
50 def test_budget_graph_negative_money(self):
51 self.assertEqual(str(BudgetGraphNegativeMoney(1, 5)),
52 "Budget assigned to task is less than zero: bug #1")
53
54 def test_budget_graph_negative_payee_money(self):
55 self.assertEqual(str(BudgetGraphNegativePayeeMoney(1, 5, "payee1")),
56 "Budget assigned to payee for task is less than "
57 "zero: bug #1, payee 'payee1'")
58
59 def test_budget_graph_payees_parse_error(self):
60 self.assertEqual(str(
61 BudgetGraphPayeesParseError(1, "my fake parse error")),
62 "Failed to parse cf_payees_list field of bug #1: "
63 "my fake parse error")
64
65 def test_budget_graph_payees_money_mismatch(self):
66 self.assertEqual(str(
67 BudgetGraphPayeesMoneyMismatch(1, 5, Money(123), Money(456))),
68 "Total budget assigned to payees (cf_payees_list) doesn't match "
69 "expected value: bug #1, calculated total 123, expected value 456")
70
71
72 EXAMPLE_BUG1 = MockBug(bug_id=1,
73 cf_budget_parent=None,
74 cf_budget="0",
75 cf_total_budget="0",
76 cf_nlnet_milestone=None,
77 cf_payees_list="")
78 EXAMPLE_LOOP1_BUG1 = MockBug(bug_id=1,
79 cf_budget_parent=1,
80 cf_budget="0",
81 cf_total_budget="0",
82 cf_nlnet_milestone=None,
83 cf_payees_list="")
84 EXAMPLE_LOOP2_BUG1 = MockBug(bug_id=1,
85 cf_budget_parent=2,
86 cf_budget="0",
87 cf_total_budget="0",
88 cf_nlnet_milestone=None,
89 cf_payees_list="")
90 EXAMPLE_LOOP2_BUG2 = MockBug(bug_id=2,
91 cf_budget_parent=1,
92 cf_budget="0",
93 cf_total_budget="0",
94 cf_nlnet_milestone=None,
95 cf_payees_list="")
96 EXAMPLE_PARENT_BUG1 = MockBug(bug_id=1,
97 cf_budget_parent=None,
98 cf_budget="10",
99 cf_total_budget="20",
100 cf_nlnet_milestone="abc",
101 cf_payees_list="")
102 EXAMPLE_CHILD_BUG2 = MockBug(bug_id=2,
103 cf_budget_parent=1,
104 cf_budget="10",
105 cf_total_budget="10",
106 cf_nlnet_milestone="abc",
107 cf_payees_list="")
108
109
110 class TestBudgetGraph(unittest.TestCase):
111 def assertErrorTypesMatches(self, errors: List[BudgetGraphBaseError], template: List[Type]):
112 error_types = []
113 for error in errors:
114 error_types.append(type(error))
115 self.assertEqual(error_types, template)
116
117 def test_empty(self):
118 bg = BudgetGraph([])
119 self.assertEqual(len(bg.nodes), 0)
120 self.assertEqual(len(bg.roots), 0)
121
122 def test_single(self):
123 bg = BudgetGraph([EXAMPLE_BUG1])
124 self.assertEqual(len(bg.nodes), 1)
125 node: Node = bg.nodes[1]
126 self.assertEqual(bg.roots, {node})
127 self.assertIsInstance(node, Node)
128 self.assertIs(node.graph, bg)
129 self.assertIs(node.bug, EXAMPLE_BUG1)
130 self.assertIs(node.root, node)
131 self.assertIsNone(node.parent_id)
132 self.assertEqual(node.immediate_children, set())
133 self.assertEqual(node.budget_excluding_subtasks, Money(cents=0))
134 self.assertEqual(node.budget_including_subtasks, Money(cents=0))
135 self.assertIsNone(node.nlnet_milestone)
136 self.assertEqual(node.payees, {})
137
138 def test_loop1(self):
139 with self.assertRaises(BudgetGraphLoopError) as cm:
140 BudgetGraph([EXAMPLE_LOOP1_BUG1]).roots
141 self.assertEqual(cm.exception.bug_ids, [1])
142
143 def test_loop2(self):
144 with self.assertRaises(BudgetGraphLoopError) as cm:
145 BudgetGraph([EXAMPLE_LOOP2_BUG1, EXAMPLE_LOOP2_BUG2]).roots
146 self.assertEqual(cm.exception.bug_ids, [2, 1])
147
148 def test_parent_child(self):
149 bg = BudgetGraph([EXAMPLE_PARENT_BUG1, EXAMPLE_CHILD_BUG2])
150 self.assertEqual(len(bg.nodes), 2)
151 node1: Node = bg.nodes[1]
152 node2: Node = bg.nodes[2]
153 self.assertEqual(bg.roots, {node1})
154 self.assertEqual(node1, node1)
155 self.assertEqual(node2, node2)
156 self.assertNotEqual(node1, node2)
157 self.assertNotEqual(node2, node1)
158 self.assertIsInstance(node1, Node)
159 self.assertIs(node1.graph, bg)
160 self.assertIs(node1.bug, EXAMPLE_PARENT_BUG1)
161 self.assertIsNone(node1.parent_id)
162 self.assertEqual(node1.root, node1)
163 self.assertEqual(node1.immediate_children, {node2})
164 self.assertEqual(node1.budget_excluding_subtasks, Money(cents=1000))
165 self.assertEqual(node1.budget_including_subtasks, Money(cents=2000))
166 self.assertEqual(node1.nlnet_milestone, "abc")
167 self.assertEqual(list(node1.children()), [node2])
168 self.assertEqual(list(node1.children_breadth_first()), [node2])
169 self.assertEqual(node1.payees, {})
170 self.assertIsInstance(node2, Node)
171 self.assertIs(node2.graph, bg)
172 self.assertIs(node2.bug, EXAMPLE_CHILD_BUG2)
173 self.assertEqual(node2.parent_id, 1)
174 self.assertEqual(node2.root, node1)
175 self.assertEqual(node2.immediate_children, set())
176 self.assertEqual(node2.budget_excluding_subtasks, Money(cents=1000))
177 self.assertEqual(node2.budget_including_subtasks, Money(cents=1000))
178 self.assertEqual(node2.nlnet_milestone, "abc")
179 self.assertEqual(node2.payees, {})
180
181 def test_children(self):
182 bg = BudgetGraph([
183 MockBug(bug_id=1,
184 cf_budget_parent=None,
185 cf_budget="0",
186 cf_total_budget="0",
187 cf_nlnet_milestone=None,
188 cf_payees_list=""),
189 MockBug(bug_id=2,
190 cf_budget_parent=1,
191 cf_budget="0",
192 cf_total_budget="0",
193 cf_nlnet_milestone=None,
194 cf_payees_list=""),
195 MockBug(bug_id=3,
196 cf_budget_parent=1,
197 cf_budget="0",
198 cf_total_budget="0",
199 cf_nlnet_milestone=None,
200 cf_payees_list=""),
201 MockBug(bug_id=4,
202 cf_budget_parent=1,
203 cf_budget="0",
204 cf_total_budget="0",
205 cf_nlnet_milestone=None,
206 cf_payees_list=""),
207 MockBug(bug_id=5,
208 cf_budget_parent=3,
209 cf_budget="0",
210 cf_total_budget="0",
211 cf_nlnet_milestone=None,
212 cf_payees_list=""),
213 MockBug(bug_id=6,
214 cf_budget_parent=3,
215 cf_budget="0",
216 cf_total_budget="0",
217 cf_nlnet_milestone=None,
218 cf_payees_list=""),
219 MockBug(bug_id=7,
220 cf_budget_parent=5,
221 cf_budget="0",
222 cf_total_budget="0",
223 cf_nlnet_milestone=None,
224 cf_payees_list=""),
225 ])
226 self.assertEqual(len(bg.nodes), 7)
227 node1: Node = bg.nodes[1]
228 node2: Node = bg.nodes[2]
229 node3: Node = bg.nodes[3]
230 node4: Node = bg.nodes[4]
231 node5: Node = bg.nodes[5]
232 node6: Node = bg.nodes[6]
233 node7: Node = bg.nodes[7]
234 self.assertEqual(bg.roots, {node1})
235 self.assertEqual(list(node1.children()),
236 [node2, node3, node5, node7, node6, node4])
237 self.assertEqual(list(node1.children_breadth_first()),
238 [node2, node3, node4, node5, node6, node7])
239
240 def test_money_with_no_milestone(self):
241 bg = BudgetGraph([
242 MockBug(bug_id=1,
243 cf_budget_parent=None,
244 cf_budget="0",
245 cf_total_budget="10",
246 cf_nlnet_milestone=None,
247 cf_payees_list=""),
248 ])
249 errors = bg.get_errors()
250 self.assertErrorTypesMatches(errors, [
251 BudgetGraphMoneyWithNoMilestone,
252 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks])
253 self.assertEqual(errors[0].bug_id, 1)
254 self.assertEqual(errors[0].root_bug_id, 1)
255 bg = BudgetGraph([
256 MockBug(bug_id=1,
257 cf_budget_parent=None,
258 cf_budget="10",
259 cf_total_budget="0",
260 cf_nlnet_milestone=None,
261 cf_payees_list=""),
262 ])
263 errors = bg.get_errors()
264 self.assertErrorTypesMatches(errors, [
265 BudgetGraphMoneyWithNoMilestone,
266 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks])
267 self.assertEqual(errors[0].bug_id, 1)
268 self.assertEqual(errors[0].root_bug_id, 1)
269 bg = BudgetGraph([
270 MockBug(bug_id=1,
271 cf_budget_parent=None,
272 cf_budget="10",
273 cf_total_budget="10",
274 cf_nlnet_milestone=None,
275 cf_payees_list=""),
276 ])
277 errors = bg.get_errors()
278 self.assertErrorTypesMatches(errors, [BudgetGraphMoneyWithNoMilestone])
279 self.assertEqual(errors[0].bug_id, 1)
280 self.assertEqual(errors[0].root_bug_id, 1)
281
282 def test_money_mismatch(self):
283 def helper(budget, total_budget, payees_list, child_budget,
284 expected_errors, expected_fixed_error_types=None):
285 if expected_fixed_error_types is None:
286 expected_fixed_error_types = []
287 bg = BudgetGraph([
288 MockBug(bug_id=1,
289 cf_budget_parent=None,
290 cf_budget=budget,
291 cf_total_budget=total_budget,
292 cf_nlnet_milestone="abc",
293 cf_payees_list=payees_list),
294 MockBug(bug_id=2,
295 cf_budget_parent=1,
296 cf_budget=child_budget,
297 cf_total_budget=child_budget,
298 cf_nlnet_milestone="abc",
299 cf_payees_list=""),
300 ])
301 node1: Node = bg.nodes[1]
302 errors = bg.get_errors()
303 self.assertErrorTypesMatches(errors,
304 [type(i) for i in expected_errors])
305 self.assertEqual([str(i) for i in errors],
306 [str(i) for i in expected_errors])
307 bg = BudgetGraph([
308 MockBug(bug_id=1,
309 cf_budget_parent=None,
310 cf_budget=str(node1.fixed_budget_excluding_subtasks),
311 cf_total_budget=str(
312 node1.fixed_budget_including_subtasks),
313 cf_nlnet_milestone="abc",
314 cf_payees_list=payees_list),
315 MockBug(bug_id=2,
316 cf_budget_parent=1,
317 cf_budget=child_budget,
318 cf_total_budget=child_budget,
319 cf_nlnet_milestone="abc",
320 cf_payees_list=""),
321 ])
322 errors = bg.get_errors()
323 self.assertErrorTypesMatches(errors,
324 expected_fixed_error_types)
325 helper(budget="0",
326 total_budget="0",
327 payees_list="",
328 child_budget="0",
329 expected_errors=[])
330 helper(budget="0",
331 total_budget="0",
332 payees_list="",
333 child_budget="5",
334 expected_errors=[
335 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
336 1, 1, Money(5)),
337 ])
338 helper(budget="0",
339 total_budget="0",
340 payees_list="a=1",
341 child_budget="0",
342 expected_errors=[
343 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
344 1, 1, Money(1)),
345 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
346 1, 1, Money(1)),
347 ])
348 helper(budget="0",
349 total_budget="0",
350 payees_list="a=1",
351 child_budget="5",
352 expected_errors=[
353 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
354 1, 1, Money(1)),
355 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
356 1, 1, Money(6)),
357 ])
358 helper(budget="0",
359 total_budget="0",
360 payees_list="a=10",
361 child_budget="0",
362 expected_errors=[
363 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
364 1, 1, Money(10)),
365 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
366 1, 1, Money(10)),
367 ])
368 helper(budget="0",
369 total_budget="0",
370 payees_list="a=10",
371 child_budget="5",
372 expected_errors=[
373 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
374 1, 1, Money(10)),
375 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
376 1, 1, Money(15)),
377 ])
378 helper(budget="0",
379 total_budget="100",
380 payees_list="",
381 child_budget="0",
382 expected_errors=[
383 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
384 1, 1, Money(100)),
385 ])
386 helper(budget="0",
387 total_budget="100",
388 payees_list="",
389 child_budget="5",
390 expected_errors=[
391 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
392 1, 1, Money(95)),
393 ])
394 helper(budget="0",
395 total_budget="100",
396 payees_list="a=1",
397 child_budget="0",
398 expected_errors=[
399 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
400 1, 1, Money(100)),
401 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(100)),
402 ],
403 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
404 helper(budget="0",
405 total_budget="100",
406 payees_list="a=1",
407 child_budget="5",
408 expected_errors=[
409 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
410 1, 1, Money(95)),
411 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(95)),
412 ],
413 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
414 helper(budget="0",
415 total_budget="100",
416 payees_list="a=10",
417 child_budget="0",
418 expected_errors=[
419 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
420 1, 1, Money(100)),
421 BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(100)),
422 ],
423 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
424 helper(budget="0",
425 total_budget="100",
426 payees_list="a=10",
427 child_budget="5",
428 expected_errors=[
429 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
430 1, 1, Money(95)),
431 BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(95)),
432 ],
433 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
434 helper(budget="0",
435 total_budget="5",
436 payees_list="",
437 child_budget="5",
438 expected_errors=[])
439 helper(budget="0",
440 total_budget="5",
441 payees_list="a=1",
442 child_budget="5",
443 expected_errors=[
444 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(0)),
445 ],
446 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
447 helper(budget="0",
448 total_budget="5",
449 payees_list="a=10",
450 child_budget="5",
451 expected_errors=[
452 BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(0)),
453 ],
454 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
455 helper(budget="10",
456 total_budget="0",
457 payees_list="",
458 child_budget="0",
459 expected_errors=[
460 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
461 1, 1, Money(10)),
462 ])
463 helper(budget="10",
464 total_budget="0",
465 payees_list="",
466 child_budget="5",
467 expected_errors=[
468 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
469 1, 1, Money(15)),
470 ])
471 helper(budget="10",
472 total_budget="0",
473 payees_list="a=1",
474 child_budget="0",
475 expected_errors=[
476 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
477 1, 1, Money(10)),
478 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
479 ],
480 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
481 helper(budget="10",
482 total_budget="0",
483 payees_list="a=1",
484 child_budget="5",
485 expected_errors=[
486 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
487 1, 1, Money(15)),
488 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
489 ],
490 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
491 helper(budget="10",
492 total_budget="0",
493 payees_list="a=10",
494 child_budget="0",
495 expected_errors=[
496 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
497 1, 1, Money(10)),
498 ])
499 helper(budget="10",
500 total_budget="0",
501 payees_list="a=10",
502 child_budget="5",
503 expected_errors=[
504 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
505 1, 1, Money(15)),
506 ])
507 helper(budget="10",
508 total_budget="10",
509 payees_list="",
510 child_budget="0",
511 expected_errors=[])
512 helper(budget="10",
513 total_budget="10",
514 payees_list="a=1",
515 child_budget="0",
516 expected_errors=[
517 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
518 ],
519 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
520 helper(budget="10",
521 total_budget="10",
522 payees_list="a=10",
523 child_budget="0",
524 expected_errors=[])
525 helper(budget="10",
526 total_budget="100",
527 payees_list="",
528 child_budget="0",
529 expected_errors=[
530 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
531 1, 1, Money(100)),
532 ])
533 helper(budget="10",
534 total_budget="100",
535 payees_list="",
536 child_budget="5",
537 expected_errors=[
538 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
539 1, 1, Money(95)),
540 ])
541 helper(budget="10",
542 total_budget="100",
543 payees_list="a=1",
544 child_budget="0",
545 expected_errors=[
546 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
547 1, 1, Money(10)),
548 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
549 ],
550 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
551 helper(budget="10",
552 total_budget="100",
553 payees_list="a=1",
554 child_budget="5",
555 expected_errors=[
556 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
557 1, 1, Money(15)),
558 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
559 ],
560 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
561 helper(budget="10",
562 total_budget="100",
563 payees_list="a=10",
564 child_budget="0",
565 expected_errors=[
566 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
567 1, 1, Money(10)),
568 ])
569 helper(budget="10",
570 total_budget="100",
571 payees_list="a=10",
572 child_budget="5",
573 expected_errors=[
574 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
575 1, 1, Money(15)),
576 ])
577 helper(budget="10",
578 total_budget="15",
579 payees_list="",
580 child_budget="5",
581 expected_errors=[])
582 helper(budget="10",
583 total_budget="15",
584 payees_list="a=1",
585 child_budget="5",
586 expected_errors=[
587 BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
588 ],
589 expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
590 helper(budget="10",
591 total_budget="15",
592 payees_list="a=10",
593 child_budget="5",
594 expected_errors=[])
595
596 helper(budget="1",
597 total_budget="15",
598 payees_list="a=10",
599 child_budget="5",
600 expected_errors=[
601 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
602 1, 1, Money("10"))
603 ])
604
605 def test_negative_money(self):
606 bg = BudgetGraph([
607 MockBug(bug_id=1,
608 cf_budget_parent=None,
609 cf_budget="0",
610 cf_total_budget="-10",
611 cf_nlnet_milestone="abc",
612 cf_payees_list=""),
613 ])
614 errors = bg.get_errors()
615 self.assertErrorTypesMatches(errors, [
616 BudgetGraphNegativeMoney,
617 BudgetGraphMoneyMismatchForBudgetExcludingSubtasks])
618 self.assertEqual(errors[0].bug_id, 1)
619 self.assertEqual(errors[0].root_bug_id, 1)
620 self.assertEqual(errors[1].bug_id, 1)
621 self.assertEqual(errors[1].root_bug_id, 1)
622 self.assertEqual(errors[1].expected_budget_excluding_subtasks, -10)
623 bg = BudgetGraph([
624 MockBug(bug_id=1,
625 cf_budget_parent=None,
626 cf_budget="-10",
627 cf_total_budget="0",
628 cf_nlnet_milestone="abc",
629 cf_payees_list=""),
630 ])
631 errors = bg.get_errors()
632 self.assertErrorTypesMatches(errors, [
633 BudgetGraphNegativeMoney,
634 BudgetGraphMoneyMismatchForBudgetIncludingSubtasks])
635 self.assertEqual(errors[0].bug_id, 1)
636 self.assertEqual(errors[0].root_bug_id, 1)
637 self.assertEqual(errors[1].bug_id, 1)
638 self.assertEqual(errors[1].root_bug_id, 1)
639 self.assertEqual(errors[1].expected_budget_including_subtasks, -10)
640 bg = BudgetGraph([
641 MockBug(bug_id=1,
642 cf_budget_parent=None,
643 cf_budget="-10",
644 cf_total_budget="-10",
645 cf_nlnet_milestone="abc",
646 cf_payees_list=""),
647 ])
648 errors = bg.get_errors()
649 self.assertErrorTypesMatches(errors,
650 [BudgetGraphNegativeMoney])
651 self.assertEqual(errors[0].bug_id, 1)
652 self.assertEqual(errors[0].root_bug_id, 1)
653
654 def test_payees_parse(self):
655 def check(cf_payees_list, expected_payees):
656 bg = BudgetGraph([MockBug(bug_id=1,
657 cf_budget_parent=None,
658 cf_budget="0",
659 cf_total_budget="0",
660 cf_nlnet_milestone="abc",
661 cf_payees_list=cf_payees_list),
662 ])
663 self.assertEqual(len(bg.nodes), 1)
664 node: Node = bg.nodes[1]
665 self.assertEqual(node.payees, expected_payees)
666
667 check("""
668 abc = 123
669 """,
670 {"abc": Money(123)})
671 check("""
672 abc = "123"
673 """,
674 {"abc": Money(123)})
675 check("""
676 abc = "123.45"
677 """,
678 {"abc": Money("123.45")})
679 check("""
680 abc = "123.45"
681 "d e f" = "21.35"
682 """,
683 {
684 "abc": Money("123.45"),
685 "d e f": Money("21.35"),
686 })
687 check("""
688 abc = "123.45"
689 # my comments
690 "AAA" = "-21.35"
691 """,
692 {
693 "abc": Money("123.45"),
694 "AAA": Money("-21.35"),
695 })
696 check("""
697 "not-an-email@example.com" = "-2345"
698 """,
699 {
700 "not-an-email@example.com": Money(-2345),
701 })
702
703 def test_payees_money_mismatch(self):
704 bg = BudgetGraph([
705 MockBug(bug_id=1,
706 cf_budget_parent=None,
707 cf_budget="10",
708 cf_total_budget="10",
709 cf_nlnet_milestone="abc",
710 cf_payees_list="payee = 5\npayee2 = 10"),
711 ])
712 errors = bg.get_errors()
713 self.assertErrorTypesMatches(errors,
714 [BudgetGraphPayeesMoneyMismatch])
715 self.assertEqual(errors[0].bug_id, 1)
716 self.assertEqual(errors[0].root_bug_id, 1)
717 self.assertEqual(errors[0].payees_total, 15)
718
719 def test_payees_parse_error(self):
720 def check_parse_error(cf_payees_list, expected_msg):
721 errors = BudgetGraph([
722 MockBug(bug_id=1,
723 cf_budget_parent=None,
724 cf_budget="0",
725 cf_total_budget="0",
726 cf_nlnet_milestone="abc",
727 cf_payees_list=cf_payees_list),
728 ]).get_errors()
729 self.assertErrorTypesMatches(errors,
730 [BudgetGraphPayeesParseError])
731 self.assertEqual(errors[0].bug_id, 1)
732 self.assertEqual(errors[0].msg, expected_msg)
733
734 check_parse_error("""
735 "payee 1" = {}
736 """,
737 "value for key 'payee 1' is not a string or integer "
738 "(to use fractional values such as 123.45, write "
739 "\"123.45\"): {}")
740
741 check_parse_error("""
742 payee = "ashjkf"
743 """,
744 "failed to parse Money value for key 'payee': "
745 "invalid Money string: characters after sign and "
746 "before first `.` must be ascii digits")
747
748 check_parse_error("""
749 payee = "1"
750 payee = "1"
751 """,
752 "TOML parse error: Duplicate keys! (line 3"
753 " column 1 char 39)")
754
755 check_parse_error("""
756 payee = 123.45
757 """,
758 "value for key 'payee' is not a string or "
759 "integer (to use fractional values such as "
760 "123.45, write \"123.45\"): 123.45")
761
762 def test_negative_payee_money(self):
763 bg = BudgetGraph([
764 MockBug(bug_id=1,
765 cf_budget_parent=None,
766 cf_budget="10",
767 cf_total_budget="10",
768 cf_nlnet_milestone="abc",
769 cf_payees_list="""payee1 = -10"""),
770 ])
771 errors = bg.get_errors()
772 self.assertErrorTypesMatches(errors,
773 [BudgetGraphNegativePayeeMoney,
774 BudgetGraphPayeesMoneyMismatch])
775 self.assertEqual(errors[0].bug_id, 1)
776 self.assertEqual(errors[0].root_bug_id, 1)
777 self.assertEqual(errors[0].payee_key, "payee1")
778 self.assertEqual(errors[1].bug_id, 1)
779 self.assertEqual(errors[1].root_bug_id, 1)
780 self.assertEqual(errors[1].payees_total, -10)
781
782 def test_payee_keys(self):
783 bg = BudgetGraph([
784 MockBug(bug_id=1,
785 cf_budget_parent=None,
786 cf_budget="10",
787 cf_total_budget="10",
788 cf_nlnet_milestone="abc",
789 cf_payees_list="payee2 = 3\npayee1 = 7"),
790 MockBug(bug_id=2,
791 cf_budget_parent=None,
792 cf_budget="10",
793 cf_total_budget="10",
794 cf_nlnet_milestone="def",
795 cf_payees_list="""payee3 = 5\npayee2 = 5"""),
796 ])
797 self.assertErrorTypesMatches(bg.get_errors(), [])
798 self.assertEqual(bg.payee_keys, {"payee1", "payee2", "payee3"})
799
800
801 if __name__ == "__main__":
802 unittest.main()