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