start adding cf_payees_list handling
[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 BudgetGraphMoneyMismatch,
6 BudgetGraphNegativeMoney,
7 BudgetGraphMilestoneMismatch,
8 BudgetGraphNegativePayeeMoney,
9 BudgetGraphPayeesParseError,
10 BudgetGraphPayeesMoneyMismatch)
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_loop_error(self):
18 self.assertEqual(str(BudgetGraphLoopError([1, 2, 3, 4, 5])),
19 "Detected Loop in Budget Graph: #5 -> #1 "
20 "-> #2 -> #3 -> #4 -> #5")
21 self.assertEqual(str(BudgetGraphLoopError([1])),
22 "Detected Loop in Budget Graph: #1 -> #1")
23
24 def test_budget_graph_money_with_no_milestone(self):
25 self.assertEqual(str(BudgetGraphMoneyWithNoMilestone(1, 5)),
26 "Bug assigned money but without any assigned "
27 "milestone: #1")
28
29 def test_budget_graph_milestone_mismatch(self):
30 self.assertEqual(str(BudgetGraphMilestoneMismatch(1, 5)),
31 "Bug's assigned milestone doesn't match the "
32 "milestone assigned to the root bug: descendant "
33 "bug #1, root bug #5")
34
35 def test_budget_graph_money_mismatch(self):
36 self.assertEqual(str(BudgetGraphMoneyMismatch(1, 5, "123.4")),
37 "Budget assigned to task excluding subtasks "
38 "(cf_budget field) doesn't match calculated value:"
39 " bug #1, calculated value 123.4")
40
41 def test_budget_graph_negative_money(self):
42 self.assertEqual(str(BudgetGraphNegativeMoney(1, 5)),
43 "Budget assigned to task is less than zero: bug #1")
44
45 def test_budget_graph_negative_payee_money(self):
46 self.assertEqual(str(BudgetGraphNegativePayeeMoney(1, 5, "payee1")),
47 "Budget assigned to payee for task is less than "
48 "zero: bug #1, payee 'payee1'")
49
50 def test_budget_graph_payees_parse_error(self):
51 self.assertEqual(str(
52 BudgetGraphPayeesParseError(1, "my fake parse error")),
53 "Failed to parse cf_payees_list field of bug #1: "
54 "my fake parse error")
55
56 def test_budget_graph_payees_money_mismatch(self):
57 self.assertEqual(str(
58 BudgetGraphPayeesMoneyMismatch(1, 5, Money(123))),
59 "Budget assigned to task excluding subtasks (cf_budget field) "
60 "doesn't match total value assigned to payees (cf_payees_list):"
61 " bug #1, calculated total 123")
62
63
64 EXAMPLE_BUG1 = MockBug(bug_id=1,
65 cf_budget_parent=None,
66 cf_budget="0",
67 cf_total_budget="0",
68 cf_nlnet_milestone=None,
69 cf_payees_list="")
70 EXAMPLE_LOOP1_BUG1 = MockBug(bug_id=1,
71 cf_budget_parent=1,
72 cf_budget="0",
73 cf_total_budget="0",
74 cf_nlnet_milestone=None,
75 cf_payees_list="")
76 EXAMPLE_LOOP2_BUG1 = MockBug(bug_id=1,
77 cf_budget_parent=2,
78 cf_budget="0",
79 cf_total_budget="0",
80 cf_nlnet_milestone=None,
81 cf_payees_list="")
82 EXAMPLE_LOOP2_BUG2 = MockBug(bug_id=2,
83 cf_budget_parent=1,
84 cf_budget="0",
85 cf_total_budget="0",
86 cf_nlnet_milestone=None,
87 cf_payees_list="")
88 EXAMPLE_PARENT_BUG1 = MockBug(bug_id=1,
89 cf_budget_parent=None,
90 cf_budget="10",
91 cf_total_budget="20",
92 cf_nlnet_milestone="abc",
93 cf_payees_list="")
94 EXAMPLE_CHILD_BUG2 = MockBug(bug_id=2,
95 cf_budget_parent=1,
96 cf_budget="10",
97 cf_total_budget="10",
98 cf_nlnet_milestone="abc",
99 cf_payees_list="")
100
101
102 class TestBudgetGraph(unittest.TestCase):
103 def assertErrorTypesMatches(self, errors: List[BudgetGraphBaseError], template: List[Type]):
104 error_types = []
105 for error in errors:
106 error_types.append(type(error))
107 self.assertEqual(error_types, template)
108
109 def test_empty(self):
110 bg = BudgetGraph([])
111 self.assertEqual(len(bg.nodes), 0)
112 self.assertEqual(len(bg.roots), 0)
113
114 def test_single(self):
115 bg = BudgetGraph([EXAMPLE_BUG1])
116 self.assertEqual(len(bg.nodes), 1)
117 node: Node = bg.nodes[1]
118 self.assertEqual(bg.roots, {node})
119 self.assertIsInstance(node, Node)
120 self.assertIs(node.graph, bg)
121 self.assertIs(node.bug, EXAMPLE_BUG1)
122 self.assertIs(node.root, node)
123 self.assertIsNone(node.parent_id)
124 self.assertEqual(node.immediate_children, set())
125 self.assertEqual(node.budget_excluding_subtasks, Money(cents=0))
126 self.assertEqual(node.budget_including_subtasks, Money(cents=0))
127 self.assertIsNone(node.nlnet_milestone)
128 self.assertEqual(node.payees, {})
129
130 def test_loop1(self):
131 with self.assertRaises(BudgetGraphLoopError) as cm:
132 BudgetGraph([EXAMPLE_LOOP1_BUG1]).roots
133 self.assertEqual(cm.exception.bug_ids, [1])
134
135 def test_loop2(self):
136 with self.assertRaises(BudgetGraphLoopError) as cm:
137 BudgetGraph([EXAMPLE_LOOP2_BUG1, EXAMPLE_LOOP2_BUG2]).roots
138 self.assertEqual(cm.exception.bug_ids, [2, 1])
139
140 def test_parent_child(self):
141 bg = BudgetGraph([EXAMPLE_PARENT_BUG1, EXAMPLE_CHILD_BUG2])
142 self.assertEqual(len(bg.nodes), 2)
143 node1: Node = bg.nodes[1]
144 node2: Node = bg.nodes[2]
145 self.assertEqual(bg.roots, {node1})
146 self.assertEqual(node1, node1)
147 self.assertEqual(node2, node2)
148 self.assertNotEqual(node1, node2)
149 self.assertNotEqual(node2, node1)
150 self.assertIsInstance(node1, Node)
151 self.assertIs(node1.graph, bg)
152 self.assertIs(node1.bug, EXAMPLE_PARENT_BUG1)
153 self.assertIsNone(node1.parent_id)
154 self.assertEqual(node1.root, node1)
155 self.assertEqual(node1.immediate_children, {node2})
156 self.assertEqual(node1.budget_excluding_subtasks, Money(cents=1000))
157 self.assertEqual(node1.budget_including_subtasks, Money(cents=2000))
158 self.assertEqual(node1.nlnet_milestone, "abc")
159 self.assertEqual(list(node1.children()), [node2])
160 self.assertEqual(node1.payees, {})
161 self.assertIsInstance(node2, Node)
162 self.assertIs(node2.graph, bg)
163 self.assertIs(node2.bug, EXAMPLE_CHILD_BUG2)
164 self.assertEqual(node2.parent_id, 1)
165 self.assertEqual(node2.root, node1)
166 self.assertEqual(node2.immediate_children, set())
167 self.assertEqual(node2.budget_excluding_subtasks, Money(cents=1000))
168 self.assertEqual(node2.budget_including_subtasks, Money(cents=1000))
169 self.assertEqual(node2.nlnet_milestone, "abc")
170 self.assertEqual(node2.payees, {})
171
172 def test_money_with_no_milestone(self):
173 bg = BudgetGraph([
174 MockBug(bug_id=1,
175 cf_budget_parent=None,
176 cf_budget="0",
177 cf_total_budget="10",
178 cf_nlnet_milestone=None,
179 cf_payees_list=""),
180 ])
181 errors = bg.get_errors()
182 self.assertErrorTypesMatches(errors,
183 [BudgetGraphMoneyWithNoMilestone,
184 BudgetGraphMoneyMismatch])
185 self.assertEqual(errors[0].bug_id, 1)
186 self.assertEqual(errors[0].root_bug_id, 1)
187 bg = BudgetGraph([
188 MockBug(bug_id=1,
189 cf_budget_parent=None,
190 cf_budget="10",
191 cf_total_budget="0",
192 cf_nlnet_milestone=None,
193 cf_payees_list=""),
194 ])
195 errors = bg.get_errors()
196 self.assertErrorTypesMatches(errors,
197 [BudgetGraphMoneyWithNoMilestone,
198 BudgetGraphMoneyMismatch])
199 self.assertEqual(errors[0].bug_id, 1)
200 self.assertEqual(errors[0].root_bug_id, 1)
201 bg = BudgetGraph([
202 MockBug(bug_id=1,
203 cf_budget_parent=None,
204 cf_budget="10",
205 cf_total_budget="10",
206 cf_nlnet_milestone=None,
207 cf_payees_list=""),
208 ])
209 errors = bg.get_errors()
210 self.assertErrorTypesMatches(errors, [BudgetGraphMoneyWithNoMilestone])
211 self.assertEqual(errors[0].bug_id, 1)
212 self.assertEqual(errors[0].root_bug_id, 1)
213
214 def test_money_mismatch(self):
215 bg = BudgetGraph([
216 MockBug(bug_id=1,
217 cf_budget_parent=None,
218 cf_budget="0",
219 cf_total_budget="10",
220 cf_nlnet_milestone="abc",
221 cf_payees_list=""),
222 ])
223 errors = bg.get_errors()
224 self.assertErrorTypesMatches(errors,
225 [BudgetGraphMoneyMismatch])
226 self.assertEqual(errors[0].bug_id, 1)
227 self.assertEqual(errors[0].root_bug_id, 1)
228 self.assertEqual(errors[0].expected_budget_excluding_subtasks, 10)
229 bg = BudgetGraph([
230 MockBug(bug_id=1,
231 cf_budget_parent=None,
232 cf_budget="10",
233 cf_total_budget="0",
234 cf_nlnet_milestone="abc",
235 cf_payees_list=""),
236 ])
237 errors = bg.get_errors()
238 self.assertErrorTypesMatches(errors,
239 [BudgetGraphMoneyMismatch])
240 self.assertEqual(errors[0].bug_id, 1)
241 self.assertEqual(errors[0].root_bug_id, 1)
242 self.assertEqual(errors[0].expected_budget_excluding_subtasks, 0)
243 bg = BudgetGraph([
244 MockBug(bug_id=1,
245 cf_budget_parent=None,
246 cf_budget="10",
247 cf_total_budget="10",
248 cf_nlnet_milestone="abc",
249 cf_payees_list=""),
250 ])
251 errors = bg.get_errors()
252 self.assertEqual(errors, [])
253 bg = BudgetGraph([
254 MockBug(bug_id=1,
255 cf_budget_parent=None,
256 cf_budget="10",
257 cf_total_budget="10",
258 cf_nlnet_milestone="abc",
259 cf_payees_list=""),
260 MockBug(bug_id=2,
261 cf_budget_parent=1,
262 cf_budget="10",
263 cf_total_budget="10",
264 cf_nlnet_milestone="abc",
265 cf_payees_list=""),
266 MockBug(bug_id=3,
267 cf_budget_parent=1,
268 cf_budget="1",
269 cf_total_budget="10",
270 cf_nlnet_milestone="abc",
271 cf_payees_list=""),
272 ])
273 errors = bg.get_errors()
274 self.assertErrorTypesMatches(errors,
275 [BudgetGraphMoneyMismatch,
276 BudgetGraphMoneyMismatch])
277 self.assertEqual(errors[0].bug_id, 1)
278 self.assertEqual(errors[0].root_bug_id, 1)
279 self.assertEqual(errors[0].expected_budget_excluding_subtasks, -10)
280 self.assertEqual(errors[1].bug_id, 3)
281 self.assertEqual(errors[1].root_bug_id, 1)
282 self.assertEqual(errors[1].expected_budget_excluding_subtasks, 10)
283
284 def test_negative_money(self):
285 bg = BudgetGraph([
286 MockBug(bug_id=1,
287 cf_budget_parent=None,
288 cf_budget="0",
289 cf_total_budget="-10",
290 cf_nlnet_milestone="abc",
291 cf_payees_list=""),
292 ])
293 errors = bg.get_errors()
294 self.assertErrorTypesMatches(errors,
295 [BudgetGraphNegativeMoney,
296 BudgetGraphMoneyMismatch])
297 self.assertEqual(errors[0].bug_id, 1)
298 self.assertEqual(errors[0].root_bug_id, 1)
299 self.assertEqual(errors[1].bug_id, 1)
300 self.assertEqual(errors[1].root_bug_id, 1)
301 self.assertEqual(errors[1].expected_budget_excluding_subtasks, -10)
302 bg = BudgetGraph([
303 MockBug(bug_id=1,
304 cf_budget_parent=None,
305 cf_budget="-10",
306 cf_total_budget="0",
307 cf_nlnet_milestone="abc",
308 cf_payees_list=""),
309 ])
310 errors = bg.get_errors()
311 self.assertErrorTypesMatches(errors,
312 [BudgetGraphNegativeMoney,
313 BudgetGraphMoneyMismatch])
314 self.assertEqual(errors[0].bug_id, 1)
315 self.assertEqual(errors[0].root_bug_id, 1)
316 self.assertEqual(errors[1].bug_id, 1)
317 self.assertEqual(errors[1].root_bug_id, 1)
318 self.assertEqual(errors[1].expected_budget_excluding_subtasks, 0)
319 bg = BudgetGraph([
320 MockBug(bug_id=1,
321 cf_budget_parent=None,
322 cf_budget="-10",
323 cf_total_budget="-10",
324 cf_nlnet_milestone="abc",
325 cf_payees_list=""),
326 ])
327 errors = bg.get_errors()
328 self.assertErrorTypesMatches(errors,
329 [BudgetGraphNegativeMoney])
330 self.assertEqual(errors[0].bug_id, 1)
331 self.assertEqual(errors[0].root_bug_id, 1)
332
333 def test_payees_parse(self):
334 def check(cf_payees_list, expected_payees):
335 bg = BudgetGraph([MockBug(bug_id=1,
336 cf_budget_parent=None,
337 cf_budget="0",
338 cf_total_budget="0",
339 cf_nlnet_milestone="abc",
340 cf_payees_list=cf_payees_list),
341 ])
342 self.assertEqual(len(bg.nodes), 1)
343 node: Node = bg.nodes[1]
344 self.assertEqual(node.payees, expected_payees)
345
346 check("""
347 abc = 123
348 """,
349 {"abc": Money(123)})
350 check("""
351 abc = "123"
352 """,
353 {"abc": Money(123)})
354 check("""
355 abc = "123.45"
356 """,
357 {"abc": Money("123.45")})
358 check("""
359 abc = "123.45"
360 "d e f" = "21.35"
361 """,
362 {
363 "abc": Money("123.45"),
364 "d e f": Money("21.35"),
365 })
366 check("""
367 abc = "123.45"
368 # my comments
369 "AAA" = "-21.35"
370 """,
371 {
372 "abc": Money("123.45"),
373 "AAA": Money("-21.35"),
374 })
375 check("""
376 "not-an-email@example.com" = "-2345"
377 """,
378 {
379 "not-an-email@example.com": Money(-2345),
380 })
381
382 def test_payees_money_mismatch(self):
383 bg = BudgetGraph([
384 MockBug(bug_id=1,
385 cf_budget_parent=None,
386 cf_budget="10",
387 cf_total_budget="10",
388 cf_nlnet_milestone="abc",
389 cf_payees_list="payee = 5\npayee2 = 10"),
390 ])
391 errors = bg.get_errors()
392 self.assertErrorTypesMatches(errors,
393 [BudgetGraphPayeesMoneyMismatch])
394 self.assertEqual(errors[0].bug_id, 1)
395 self.assertEqual(errors[0].root_bug_id, 1)
396 self.assertEqual(errors[0].payees_total, 15)
397 bg = BudgetGraph([
398 MockBug(bug_id=1,
399 cf_budget_parent=None,
400 cf_budget="0",
401 cf_total_budget="0",
402 cf_nlnet_milestone=None,
403 cf_payees_list="payee = 5\npayee2 = 10"),
404 ])
405 errors = bg.get_errors()
406 self.assertErrorTypesMatches(errors,
407 [BudgetGraphPayeesMoneyMismatch])
408 self.assertEqual(errors[0].bug_id, 1)
409 self.assertEqual(errors[0].root_bug_id, 1)
410 self.assertEqual(errors[0].payees_total, 15)
411
412 def test_payees_parse_error(self):
413 def check_parse_error(cf_payees_list, expected_msg):
414 errors = BudgetGraph([
415 MockBug(bug_id=1,
416 cf_budget_parent=None,
417 cf_budget="0",
418 cf_total_budget="0",
419 cf_nlnet_milestone="abc",
420 cf_payees_list=cf_payees_list),
421 ]).get_errors()
422 self.assertErrorTypesMatches(errors,
423 [BudgetGraphPayeesParseError])
424 self.assertEqual(errors[0].bug_id, 1)
425 self.assertEqual(errors[0].msg, expected_msg)
426
427 check_parse_error("""
428 "payee 1" = {}
429 """,
430 "value for key 'payee 1' is not a string or integer "
431 "(to use fractional values such as 123.45, write "
432 "\"123.45\"): {}")
433
434 check_parse_error("""
435 payee = "ashjkf"
436 """,
437 "failed to parse Money value for key 'payee': "
438 "invalid Money string: characters after sign and "
439 "before first `.` must be ascii digits")
440
441 check_parse_error("""
442 payee = "1"
443 payee = "1"
444 """,
445 "TOML parse error: Duplicate keys! (line 3"
446 " column 1 char 39)")
447
448 check_parse_error("""
449 payee = 123.45
450 """,
451 "value for key 'payee' is not a string or "
452 "integer (to use fractional values such as "
453 "123.45, write \"123.45\"): 123.45")
454
455 def test_negative_payee_money(self):
456 bg = BudgetGraph([
457 MockBug(bug_id=1,
458 cf_budget_parent=None,
459 cf_budget="10",
460 cf_total_budget="10",
461 cf_nlnet_milestone="abc",
462 cf_payees_list="""payee1 = -10"""),
463 ])
464 errors = bg.get_errors()
465 self.assertErrorTypesMatches(errors,
466 [BudgetGraphNegativePayeeMoney,
467 BudgetGraphPayeesMoneyMismatch])
468 self.assertEqual(errors[0].bug_id, 1)
469 self.assertEqual(errors[0].root_bug_id, 1)
470 self.assertEqual(errors[0].payee_key, "payee1")
471 self.assertEqual(errors[1].bug_id, 1)
472 self.assertEqual(errors[1].root_bug_id, 1)
473 self.assertEqual(errors[1].payees_total, -10)
474
475 def test_payee_keys(self):
476 bg = BudgetGraph([
477 MockBug(bug_id=1,
478 cf_budget_parent=None,
479 cf_budget="10",
480 cf_total_budget="10",
481 cf_nlnet_milestone="abc",
482 cf_payees_list="payee2 = 3\npayee1 = 7"),
483 MockBug(bug_id=2,
484 cf_budget_parent=None,
485 cf_budget="10",
486 cf_total_budget="10",
487 cf_nlnet_milestone="def",
488 cf_payees_list="""payee3 = 5\npayee2 = 5"""),
489 ])
490 self.assertErrorTypesMatches(bg.get_errors(), [])
491 self.assertEqual(bg.payee_keys, {"payee1", "payee2", "payee3"})
492
493
494 if __name__ == "__main__":
495 unittest.main()