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