1 from budget_sync
.test
.mock_bug
import MockBug
2 from budget_sync
.budget_graph
import (BudgetGraphLoopError
, BudgetGraph
,
3 Node
, BudgetGraphMoneyWithNoMilestone
,
5 BudgetGraphMoneyMismatch
,
6 BudgetGraphNegativeMoney
,
7 BudgetGraphMilestoneMismatch
,
8 BudgetGraphNegativePayeeMoney
,
9 BudgetGraphPayeesParseError
,
10 BudgetGraphPayeesMoneyMismatch
)
11 from budget_sync
.money
import Money
12 from typing
import List
, Type
16 class TestErrorFormatting(unittest
.TestCase
):
17 def test_budget_graph_loop_error(self
):
18 self
.assertEqual(str(BudgetGraphLoopError([1, 2, 3, 4, 5])),
19 "Detected Loop in Budget Graph: #5 -> #1 "
20 "-> #2 -> #3 -> #4 -> #5")
21 self
.assertEqual(str(BudgetGraphLoopError([1])),
22 "Detected Loop in Budget Graph: #1 -> #1")
24 def test_budget_graph_money_with_no_milestone(self
):
25 self
.assertEqual(str(BudgetGraphMoneyWithNoMilestone(1, 5)),
26 "Bug assigned money but without any assigned "
29 def test_budget_graph_milestone_mismatch(self
):
30 self
.assertEqual(str(BudgetGraphMilestoneMismatch(1, 5)),
31 "Bug's assigned milestone doesn't match the "
32 "milestone assigned to the root bug: descendant "
33 "bug #1, root bug #5")
35 def test_budget_graph_money_mismatch(self
):
36 self
.assertEqual(str(BudgetGraphMoneyMismatch(1, 5, "123.4")),
37 "Budget assigned to task excluding subtasks "
38 "(cf_budget field) doesn't match calculated value:"
39 " bug #1, calculated value 123.4")
41 def test_budget_graph_negative_money(self
):
42 self
.assertEqual(str(BudgetGraphNegativeMoney(1, 5)),
43 "Budget assigned to task is less than zero: bug #1")
45 def test_budget_graph_negative_payee_money(self
):
46 self
.assertEqual(str(BudgetGraphNegativePayeeMoney(1, 5, "payee1")),
47 "Budget assigned to payee for task is less than "
48 "zero: bug #1, payee 'payee1'")
50 def test_budget_graph_payees_parse_error(self
):
52 BudgetGraphPayeesParseError(1, "my fake parse error")),
53 "Failed to parse cf_payees_list field of bug #1: "
54 "my fake parse error")
56 def test_budget_graph_payees_money_mismatch(self
):
58 BudgetGraphPayeesMoneyMismatch(1, 5, Money(123))),
59 "Budget assigned to task excluding subtasks (cf_budget field) "
60 "doesn't match total value assigned to payees (cf_payees_list):"
61 " bug #1, calculated total 123")
64 EXAMPLE_BUG1
= MockBug(bug_id
=1,
65 cf_budget_parent
=None,
68 cf_nlnet_milestone
=None,
70 EXAMPLE_LOOP1_BUG1
= MockBug(bug_id
=1,
74 cf_nlnet_milestone
=None,
76 EXAMPLE_LOOP2_BUG1
= MockBug(bug_id
=1,
80 cf_nlnet_milestone
=None,
82 EXAMPLE_LOOP2_BUG2
= MockBug(bug_id
=2,
86 cf_nlnet_milestone
=None,
88 EXAMPLE_PARENT_BUG1
= MockBug(bug_id
=1,
89 cf_budget_parent
=None,
92 cf_nlnet_milestone
="abc",
94 EXAMPLE_CHILD_BUG2
= MockBug(bug_id
=2,
98 cf_nlnet_milestone
="abc",
102 class TestBudgetGraph(unittest
.TestCase
):
103 def assertErrorTypesMatches(self
, errors
: List
[BudgetGraphBaseError
], template
: List
[Type
]):
106 error_types
.append(type(error
))
107 self
.assertEqual(error_types
, template
)
109 def test_empty(self
):
111 self
.assertEqual(len(bg
.nodes
), 0)
112 self
.assertEqual(len(bg
.roots
), 0)
114 def test_single(self
):
115 bg
= BudgetGraph([EXAMPLE_BUG1
])
116 self
.assertEqual(len(bg
.nodes
), 1)
117 node
: Node
= bg
.nodes
[1]
118 self
.assertEqual(bg
.roots
, {node}
)
119 self
.assertIsInstance(node
, Node
)
120 self
.assertIs(node
.graph
, bg
)
121 self
.assertIs(node
.bug
, EXAMPLE_BUG1
)
122 self
.assertIs(node
.root
, node
)
123 self
.assertIsNone(node
.parent_id
)
124 self
.assertEqual(node
.immediate_children
, set())
125 self
.assertEqual(node
.budget_excluding_subtasks
, Money(cents
=0))
126 self
.assertEqual(node
.budget_including_subtasks
, Money(cents
=0))
127 self
.assertIsNone(node
.nlnet_milestone
)
128 self
.assertEqual(node
.payees
, {})
130 def test_loop1(self
):
131 with self
.assertRaises(BudgetGraphLoopError
) as cm
:
132 BudgetGraph([EXAMPLE_LOOP1_BUG1
]).roots
133 self
.assertEqual(cm
.exception
.bug_ids
, [1])
135 def test_loop2(self
):
136 with self
.assertRaises(BudgetGraphLoopError
) as cm
:
137 BudgetGraph([EXAMPLE_LOOP2_BUG1
, EXAMPLE_LOOP2_BUG2
]).roots
138 self
.assertEqual(cm
.exception
.bug_ids
, [2, 1])
140 def test_parent_child(self
):
141 bg
= BudgetGraph([EXAMPLE_PARENT_BUG1
, EXAMPLE_CHILD_BUG2
])
142 self
.assertEqual(len(bg
.nodes
), 2)
143 node1
: Node
= bg
.nodes
[1]
144 node2
: Node
= bg
.nodes
[2]
145 self
.assertEqual(bg
.roots
, {node1}
)
146 self
.assertEqual(node1
, node1
)
147 self
.assertEqual(node2
, node2
)
148 self
.assertNotEqual(node1
, node2
)
149 self
.assertNotEqual(node2
, node1
)
150 self
.assertIsInstance(node1
, Node
)
151 self
.assertIs(node1
.graph
, bg
)
152 self
.assertIs(node1
.bug
, EXAMPLE_PARENT_BUG1
)
153 self
.assertIsNone(node1
.parent_id
)
154 self
.assertEqual(node1
.root
, node1
)
155 self
.assertEqual(node1
.immediate_children
, {node2}
)
156 self
.assertEqual(node1
.budget_excluding_subtasks
, Money(cents
=1000))
157 self
.assertEqual(node1
.budget_including_subtasks
, Money(cents
=2000))
158 self
.assertEqual(node1
.nlnet_milestone
, "abc")
159 self
.assertEqual(list(node1
.children()), [node2
])
160 self
.assertEqual(node1
.payees
, {})
161 self
.assertIsInstance(node2
, Node
)
162 self
.assertIs(node2
.graph
, bg
)
163 self
.assertIs(node2
.bug
, EXAMPLE_CHILD_BUG2
)
164 self
.assertEqual(node2
.parent_id
, 1)
165 self
.assertEqual(node2
.root
, node1
)
166 self
.assertEqual(node2
.immediate_children
, set())
167 self
.assertEqual(node2
.budget_excluding_subtasks
, Money(cents
=1000))
168 self
.assertEqual(node2
.budget_including_subtasks
, Money(cents
=1000))
169 self
.assertEqual(node2
.nlnet_milestone
, "abc")
170 self
.assertEqual(node2
.payees
, {})
172 def test_money_with_no_milestone(self
):
175 cf_budget_parent
=None,
177 cf_total_budget
="10",
178 cf_nlnet_milestone
=None,
181 errors
= bg
.get_errors()
182 self
.assertErrorTypesMatches(errors
,
183 [BudgetGraphMoneyWithNoMilestone
,
184 BudgetGraphMoneyMismatch
])
185 self
.assertEqual(errors
[0].bug_id
, 1)
186 self
.assertEqual(errors
[0].root_bug_id
, 1)
189 cf_budget_parent
=None,
192 cf_nlnet_milestone
=None,
195 errors
= bg
.get_errors()
196 self
.assertErrorTypesMatches(errors
,
197 [BudgetGraphMoneyWithNoMilestone
,
198 BudgetGraphMoneyMismatch
])
199 self
.assertEqual(errors
[0].bug_id
, 1)
200 self
.assertEqual(errors
[0].root_bug_id
, 1)
203 cf_budget_parent
=None,
205 cf_total_budget
="10",
206 cf_nlnet_milestone
=None,
209 errors
= bg
.get_errors()
210 self
.assertErrorTypesMatches(errors
, [BudgetGraphMoneyWithNoMilestone
])
211 self
.assertEqual(errors
[0].bug_id
, 1)
212 self
.assertEqual(errors
[0].root_bug_id
, 1)
214 def test_money_mismatch(self
):
217 cf_budget_parent
=None,
219 cf_total_budget
="10",
220 cf_nlnet_milestone
="abc",
223 errors
= bg
.get_errors()
224 self
.assertErrorTypesMatches(errors
,
225 [BudgetGraphMoneyMismatch
])
226 self
.assertEqual(errors
[0].bug_id
, 1)
227 self
.assertEqual(errors
[0].root_bug_id
, 1)
228 self
.assertEqual(errors
[0].expected_budget_excluding_subtasks
, 10)
231 cf_budget_parent
=None,
234 cf_nlnet_milestone
="abc",
237 errors
= bg
.get_errors()
238 self
.assertErrorTypesMatches(errors
,
239 [BudgetGraphMoneyMismatch
])
240 self
.assertEqual(errors
[0].bug_id
, 1)
241 self
.assertEqual(errors
[0].root_bug_id
, 1)
242 self
.assertEqual(errors
[0].expected_budget_excluding_subtasks
, 0)
245 cf_budget_parent
=None,
247 cf_total_budget
="10",
248 cf_nlnet_milestone
="abc",
251 errors
= bg
.get_errors()
252 self
.assertEqual(errors
, [])
255 cf_budget_parent
=None,
257 cf_total_budget
="10",
258 cf_nlnet_milestone
="abc",
263 cf_total_budget
="10",
264 cf_nlnet_milestone
="abc",
269 cf_total_budget
="10",
270 cf_nlnet_milestone
="abc",
273 errors
= bg
.get_errors()
274 self
.assertErrorTypesMatches(errors
,
275 [BudgetGraphMoneyMismatch
,
276 BudgetGraphMoneyMismatch
])
277 self
.assertEqual(errors
[0].bug_id
, 1)
278 self
.assertEqual(errors
[0].root_bug_id
, 1)
279 self
.assertEqual(errors
[0].expected_budget_excluding_subtasks
, -10)
280 self
.assertEqual(errors
[1].bug_id
, 3)
281 self
.assertEqual(errors
[1].root_bug_id
, 1)
282 self
.assertEqual(errors
[1].expected_budget_excluding_subtasks
, 10)
284 def test_negative_money(self
):
287 cf_budget_parent
=None,
289 cf_total_budget
="-10",
290 cf_nlnet_milestone
="abc",
293 errors
= bg
.get_errors()
294 self
.assertErrorTypesMatches(errors
,
295 [BudgetGraphNegativeMoney
,
296 BudgetGraphMoneyMismatch
])
297 self
.assertEqual(errors
[0].bug_id
, 1)
298 self
.assertEqual(errors
[0].root_bug_id
, 1)
299 self
.assertEqual(errors
[1].bug_id
, 1)
300 self
.assertEqual(errors
[1].root_bug_id
, 1)
301 self
.assertEqual(errors
[1].expected_budget_excluding_subtasks
, -10)
304 cf_budget_parent
=None,
307 cf_nlnet_milestone
="abc",
310 errors
= bg
.get_errors()
311 self
.assertErrorTypesMatches(errors
,
312 [BudgetGraphNegativeMoney
,
313 BudgetGraphMoneyMismatch
])
314 self
.assertEqual(errors
[0].bug_id
, 1)
315 self
.assertEqual(errors
[0].root_bug_id
, 1)
316 self
.assertEqual(errors
[1].bug_id
, 1)
317 self
.assertEqual(errors
[1].root_bug_id
, 1)
318 self
.assertEqual(errors
[1].expected_budget_excluding_subtasks
, 0)
321 cf_budget_parent
=None,
323 cf_total_budget
="-10",
324 cf_nlnet_milestone
="abc",
327 errors
= bg
.get_errors()
328 self
.assertErrorTypesMatches(errors
,
329 [BudgetGraphNegativeMoney
])
330 self
.assertEqual(errors
[0].bug_id
, 1)
331 self
.assertEqual(errors
[0].root_bug_id
, 1)
333 def test_payees_parse(self
):
334 def check(cf_payees_list
, expected_payees
):
335 bg
= BudgetGraph([MockBug(bug_id
=1,
336 cf_budget_parent
=None,
339 cf_nlnet_milestone
="abc",
340 cf_payees_list
=cf_payees_list
),
342 self
.assertEqual(len(bg
.nodes
), 1)
343 node
: Node
= bg
.nodes
[1]
344 self
.assertEqual(node
.payees
, expected_payees
)
357 {"abc": Money("123.45")})
363 "abc": Money("123.45"),
364 "d e f": Money("21.35"),
372 "abc": Money("123.45"),
373 "AAA": Money("-21.35"),
376 "not-an-email@example.com" = "-2345"
379 "not-an-email@example.com": Money(-2345),
382 def test_payees_money_mismatch(self
):
385 cf_budget_parent
=None,
387 cf_total_budget
="10",
388 cf_nlnet_milestone
="abc",
389 cf_payees_list
="payee = 5\npayee2 = 10"),
391 errors
= bg
.get_errors()
392 self
.assertErrorTypesMatches(errors
,
393 [BudgetGraphPayeesMoneyMismatch
])
394 self
.assertEqual(errors
[0].bug_id
, 1)
395 self
.assertEqual(errors
[0].root_bug_id
, 1)
396 self
.assertEqual(errors
[0].payees_total
, 15)
399 cf_budget_parent
=None,
402 cf_nlnet_milestone
=None,
403 cf_payees_list
="payee = 5\npayee2 = 10"),
405 errors
= bg
.get_errors()
406 self
.assertErrorTypesMatches(errors
,
407 [BudgetGraphPayeesMoneyMismatch
])
408 self
.assertEqual(errors
[0].bug_id
, 1)
409 self
.assertEqual(errors
[0].root_bug_id
, 1)
410 self
.assertEqual(errors
[0].payees_total
, 15)
412 def test_payees_parse_error(self
):
413 def check_parse_error(cf_payees_list
, expected_msg
):
414 errors
= BudgetGraph([
416 cf_budget_parent
=None,
419 cf_nlnet_milestone
="abc",
420 cf_payees_list
=cf_payees_list
),
422 self
.assertErrorTypesMatches(errors
,
423 [BudgetGraphPayeesParseError
])
424 self
.assertEqual(errors
[0].bug_id
, 1)
425 self
.assertEqual(errors
[0].msg
, expected_msg
)
427 check_parse_error("""
430 "value for key 'payee 1' is not a string or integer "
431 "(to use fractional values such as 123.45, write "
434 check_parse_error("""
437 "failed to parse Money value for key 'payee': "
438 "invalid Money string: characters after sign and "
439 "before first `.` must be ascii digits")
441 check_parse_error("""
445 "TOML parse error: Duplicate keys! (line 3"
446 " column 1 char 39)")
448 check_parse_error("""
451 "value for key 'payee' is not a string or "
452 "integer (to use fractional values such as "
453 "123.45, write \"123.45\"): 123.45")
455 def test_negative_payee_money(self
):
458 cf_budget_parent
=None,
460 cf_total_budget
="10",
461 cf_nlnet_milestone
="abc",
462 cf_payees_list
="""payee1 = -10"""),
464 errors
= bg
.get_errors()
465 self
.assertErrorTypesMatches(errors
,
466 [BudgetGraphNegativePayeeMoney
,
467 BudgetGraphPayeesMoneyMismatch
])
468 self
.assertEqual(errors
[0].bug_id
, 1)
469 self
.assertEqual(errors
[0].root_bug_id
, 1)
470 self
.assertEqual(errors
[0].payee_key
, "payee1")
471 self
.assertEqual(errors
[1].bug_id
, 1)
472 self
.assertEqual(errors
[1].root_bug_id
, 1)
473 self
.assertEqual(errors
[1].payees_total
, -10)
475 def test_payee_keys(self
):
478 cf_budget_parent
=None,
480 cf_total_budget
="10",
481 cf_nlnet_milestone
="abc",
482 cf_payees_list
="payee2 = 3\npayee1 = 7"),
484 cf_budget_parent
=None,
486 cf_total_budget
="10",
487 cf_nlnet_milestone
="def",
488 cf_payees_list
="""payee3 = 5\npayee2 = 5"""),
490 self
.assertErrorTypesMatches(bg
.get_errors(), [])
491 self
.assertEqual(bg
.payee_keys
, {"payee1", "payee2", "payee3"})
494 if __name__
== "__main__":