From 1e8321a9cb1030d4e1fa47e1a9ec349ac6d0768d Mon Sep 17 00:00:00 2001 From: Jacob Lifshay Date: Tue, 5 Jul 2022 00:58:52 -0700 Subject: [PATCH] add support for reporting the closest task that is in a signed MoU --- src/budget_sync/budget_graph.py | 81 +++++++-- src/budget_sync/test/mock_bug.py | 4 +- src/budget_sync/test/test_budget_graph.py | 139 ++++++++++++-- src/budget_sync/test/test_write_budget_csv.py | 6 +- .../test/test_write_budget_markdown.py | 171 ++++++++++++++++-- src/budget_sync/write_budget_markdown.py | 12 ++ 6 files changed, 369 insertions(+), 44 deletions(-) diff --git a/src/budget_sync/budget_graph.py b/src/budget_sync/budget_graph.py index d0ce97e..3fa2d72 100644 --- a/src/budget_sync/budget_graph.py +++ b/src/budget_sync/budget_graph.py @@ -11,9 +11,9 @@ from collections import deque from datetime import date, time, datetime try: from functools import cached_property -except ImportError: - # compatability with python < 3.8 - from cached_property import cached_property +except ImportError: # :nocov: + # compatibility with python < 3.8 + from cached_property import cached_property # :nocov: class BudgetGraphBaseError(Exception): @@ -314,6 +314,7 @@ class Node: fixed_budget_excluding_subtasks: Money fixed_budget_including_subtasks: Money milestone_str: Optional[str] + is_in_nlnet_mou: bool def __init__(self, graph: "BudgetGraph", bug: Bug): self.graph = graph @@ -327,6 +328,7 @@ class Node: self.milestone_str = bug.cf_nlnet_milestone if self.milestone_str == "---": self.milestone_str = None + self.is_in_nlnet_mou = bug.cf_is_in_nlnet_mou2 == "Yes" @property def status(self) -> BugStatus: @@ -451,6 +453,18 @@ class Node: self._raise_loop_error() return retval + @cached_property + def closest_bug_in_mou(self) -> Optional["Node"]: + """returns the closest bug that is in a NLNet MoU, searching only in + this bug and parents. + """ + if self.is_in_nlnet_mou: + return self + for parent in self.parents(): + if parent.is_in_nlnet_mou: + return parent + return None + def children(self) -> Iterable["Node"]: def visitor(node: Node) -> Iterable[Node]: for i in node.immediate_children: @@ -492,6 +506,7 @@ class Node: tpp.field("fixed_budget_including_subtasks", self.fixed_budget_including_subtasks) tpp.field("milestone_str", self.milestone_str) + tpp.field("is_in_nlnet_mou", self.is_in_nlnet_mou) tpp.try_field("milestone", lambda: self.milestone, BudgetGraphBaseError) immediate_children = [_NodeSimpleReprWrapper(i) @@ -551,6 +566,7 @@ class Node: f"fixed_budget_excluding_subtasks={self.fixed_budget_excluding_subtasks}, " f"fixed_budget_including_subtasks={self.fixed_budget_including_subtasks}, " f"milestone_str={self.milestone_str!r}, " + f"is_in_nlnet_mou={self.is_in_nlnet_mou!r}, " f"milestone={milestone}, " f"immediate_children={immediate_children!r}, " f"payments={payments!r}, " @@ -651,6 +667,36 @@ class BudgetGraphIncorrectRootForMilestone(BudgetGraphError): f"#{self.milestone_canonical_bug_id}") +class BudgetGraphRootWithMilestoneNotInMoU(BudgetGraphError): + def __init__(self, bug_id: int, milestone: str): + super().__init__(bug_id, bug_id) + self.milestone = milestone + + def __str__(self): + return (f"Bug #{self.bug_id} has no parent bug set and has an " + f"assigned milestone {self.milestone!r} but isn't set " + f"to be part of the signed MoU") + + +class BudgetGraphInMoUButParentNotInMoU(BudgetGraphError): + def __init__(self, bug_id: int, parent_bug_id: int, root_bug_id: int, + milestone: str): + super().__init__(bug_id, root_bug_id) + self.parent_bug_id = parent_bug_id + self.milestone = milestone + + def __str__(self): + return (f"Bug #{self.bug_id} is set to be part of the signed MoU for " + f"milestone {self.milestone!r}, but its parent bug isn't set " + f"to be part of the signed MoU") + + +class BudgetGraphInMoUWithoutMilestone(BudgetGraphError): + def __str__(self): + return (f"Bug #{self.bug_id} is set to be part of a signed MoU but " + f"has no milestone set") + + class BudgetGraph: nodes: Dict[int, Node] @@ -688,14 +734,17 @@ class BudgetGraph: try: # check for milestone errors node.milestone - if root == node and node.milestone is not None \ - and node.milestone.canonical_bug_id != node.bug.id: - if node.budget_including_subtasks != 0 \ - or node.budget_excluding_subtasks != 0: - errors.append(BudgetGraphIncorrectRootForMilestone( - node.bug.id, node.milestone.identifier, - node.milestone.canonical_bug_id - )) + if root == node and node.milestone is not None: + if node.milestone.canonical_bug_id != node.bug.id: + if node.budget_including_subtasks != 0 \ + or node.budget_excluding_subtasks != 0: + errors.append(BudgetGraphIncorrectRootForMilestone( + node.bug.id, node.milestone.identifier, + node.milestone.canonical_bug_id + )) + elif not node.is_in_nlnet_mou: + errors.append(BudgetGraphRootWithMilestoneNotInMoU( + node.bug.id, node.milestone_str)) except BudgetGraphBaseError as e: errors.append(e) @@ -715,6 +764,16 @@ class BudgetGraph: errors.append(BudgetGraphMilestoneMismatch( node.bug.id, root.bug.id)) + if node.is_in_nlnet_mou: + if node.milestone_str is None: + errors.append(BudgetGraphInMoUWithoutMilestone(node.bug.id, + root.bug.id)) + elif node.parent is not None and \ + not node.parent.is_in_nlnet_mou: + errors.append(BudgetGraphInMoUButParentNotInMoU( + node.bug.id, node.parent.bug.id, root.bug.id, + node.milestone_str)) + if node.budget_excluding_subtasks < 0 \ or node.budget_including_subtasks < 0: errors.append(BudgetGraphNegativeMoney( diff --git a/src/budget_sync/test/mock_bug.py b/src/budget_sync/test/mock_bug.py index 17be594..4f926fc 100644 --- a/src/budget_sync/test/mock_bug.py +++ b/src/budget_sync/test/mock_bug.py @@ -12,7 +12,8 @@ class MockBug: cf_payees_list: str = "", summary: str = "", status: Union[str, BugStatus] = BugStatus.CONFIRMED, - assigned_to: str = "user@example.com"): + assigned_to: str = "user@example.com", + cf_is_in_nlnet_mou2: str = ""): self.id = bug_id self.__budget_parent = cf_budget_parent self.cf_budget = cf_budget @@ -24,6 +25,7 @@ class MockBug: self.summary = summary self.status = str(status) self.assigned_to = assigned_to + self.cf_is_in_nlnet_mou2 = cf_is_in_nlnet_mou2 @property def cf_budget_parent(self) -> int: diff --git a/src/budget_sync/test/test_budget_graph.py b/src/budget_sync/test/test_budget_graph.py index 7aaadf5..b0f9494 100644 --- a/src/budget_sync/test/test_budget_graph.py +++ b/src/budget_sync/test/test_budget_graph.py @@ -8,7 +8,9 @@ from budget_sync.budget_graph import ( BudgetGraphNegativePayeeMoney, BudgetGraphPayeesParseError, BudgetGraphPayeesMoneyMismatch, BudgetGraphUnknownMilestone, BudgetGraphIncorrectRootForMilestone, - BudgetGraphUnknownStatus, BudgetGraphUnknownAssignee) + BudgetGraphUnknownStatus, BudgetGraphUnknownAssignee, + BudgetGraphRootWithMilestoneNotInMoU, BudgetGraphInMoUButParentNotInMoU, + BudgetGraphInMoUWithoutMilestone) from budget_sync.money import Money from budget_sync.util import BugStatus from typing import List, Type @@ -94,6 +96,25 @@ class TestErrorFormatting(unittest.TestCase): "Total budget assigned to payees (cf_payees_list) doesn't match " "expected value: bug #1, calculated total 123, expected value 456") + def test_budget_graph_root_with_milestone_not_in_mou(self): + self.assertEqual(str( + BudgetGraphRootWithMilestoneNotInMoU(1, "milestone 1")), + "Bug #1 has no parent bug set and has an assigned milestone " + "'milestone 1' but isn't set to be part of the signed MoU") + + def test_budget_graph_in_mou_but_parent_not_in_mou(self): + self.assertEqual(str( + BudgetGraphInMoUButParentNotInMoU(5, 3, 1, "milestone 1")), + "Bug #5 is set to be part of the signed MoU for milestone " + "'milestone 1', but its parent bug isn't set to be part of " + "the signed MoU") + + def test_budget_graph_in_mou_without_milestone(self): + self.assertEqual(str( + BudgetGraphInMoUWithoutMilestone(1, 5)), + "Bug #1 is set to be part of a signed MoU but has no " + "milestone set") + EXAMPLE_BUG1 = MockBug(bug_id=1, cf_budget_parent=None, @@ -129,7 +150,8 @@ EXAMPLE_PARENT_BUG1 = MockBug(bug_id=1, cf_total_budget="20", cf_nlnet_milestone="milestone 1", cf_payees_list="", - summary="") + summary="", + cf_is_in_nlnet_mou2="Yes") EXAMPLE_CHILD_BUG2 = MockBug(bug_id=2, cf_budget_parent=1, cf_budget="10", @@ -186,7 +208,8 @@ class TestBudgetGraph(unittest.TestCase): "budget_excluding_subtasks=10, budget_including_subtasks=20, " "fixed_budget_excluding_subtasks=10, " "fixed_budget_including_subtasks=20, milestone_str='milestone " - "1', milestone=Milestone(config=..., identifier='milestone 1', " + "1', is_in_nlnet_mou=True, " + "milestone=Milestone(config=..., identifier='milestone 1', " "canonical_bug_id=1), immediate_children=[#2], payments=[], " "status=BugStatus.CONFIRMED, assignee=Person<'person3'>, " "resolved_payments={}, payment_summaries={}), Node(graph=..., " @@ -194,7 +217,8 @@ class TestBudgetGraph(unittest.TestCase): "budget_including_subtasks=10, " "fixed_budget_excluding_subtasks=10, " "fixed_budget_including_subtasks=10, milestone_str='milestone " - "1', milestone=Milestone(config=..., identifier='milestone 1', " + "1', is_in_nlnet_mou=False, " + "milestone=Milestone(config=..., identifier='milestone 1', " "canonical_bug_id=1), immediate_children=[], payments=[], " "status=BugStatus.CONFIRMED, assignee=Person<'person3'>, " "resolved_payments={}, payment_summaries={})], roots=[#1], " @@ -244,6 +268,7 @@ class TestBudgetGraph(unittest.TestCase): "budget_excluding_subtasks=0, budget_including_subtasks=0, " "fixed_budget_excluding_subtasks=0, " "fixed_budget_including_subtasks=0, milestone_str=None, " + "is_in_nlnet_mou=False, " "milestone=None, immediate_children=[], payments=[], " "status=, assignee=, resolved_payments={}, " @@ -292,6 +317,7 @@ alias2 = {paid=2020-03-16,amount=23} "budget_excluding_subtasks=0, budget_including_subtasks=0, " "fixed_budget_excluding_subtasks=0, " "fixed_budget_including_subtasks=0, milestone_str=None, " + "is_in_nlnet_mou=False, " "milestone=None, immediate_children=[], " "payments=[Payment(node=#1, payee=Person<'person1'>, " "payee_key='person1', amount=5, state=Paid, paid=2020-03-15, " @@ -565,13 +591,13 @@ alias2 = {paid=2020-03-16,amount=23} cf_total_budget=total_budget, cf_nlnet_milestone="milestone 1", cf_payees_list=payees_list, - summary=""), + summary="", + cf_is_in_nlnet_mou2="Yes"), MockBug(bug_id=2, cf_budget_parent=1, cf_budget=child_budget, cf_total_budget=child_budget, cf_nlnet_milestone="milestone 1", - cf_payees_list="", summary=""), ], EXAMPLE_CONFIG) node1: Node = bg.nodes[1] @@ -588,7 +614,8 @@ alias2 = {paid=2020-03-16,amount=23} node1.fixed_budget_including_subtasks), cf_nlnet_milestone="milestone 1", cf_payees_list=payees_list, - summary=""), + summary="", + cf_is_in_nlnet_mou2="Yes"), MockBug(bug_id=2, cf_budget_parent=1, cf_budget=child_budget, @@ -888,7 +915,8 @@ alias2 = {paid=2020-03-16,amount=23} cf_total_budget="-10", cf_nlnet_milestone="milestone 1", cf_payees_list="", - summary=""), + summary="", + cf_is_in_nlnet_mou2="Yes"), ], EXAMPLE_CONFIG) errors = bg.get_errors() self.assertErrorTypesMatches(errors, [ @@ -906,7 +934,8 @@ alias2 = {paid=2020-03-16,amount=23} cf_total_budget="0", cf_nlnet_milestone="milestone 1", cf_payees_list="", - summary=""), + summary="", + cf_is_in_nlnet_mou2="Yes"), ], EXAMPLE_CONFIG) errors = bg.get_errors() self.assertErrorTypesMatches(errors, [ @@ -924,7 +953,8 @@ alias2 = {paid=2020-03-16,amount=23} cf_total_budget="-10", cf_nlnet_milestone="milestone 1", cf_payees_list="", - summary=""), + summary="", + cf_is_in_nlnet_mou2="Yes"), ], EXAMPLE_CONFIG) errors = bg.get_errors() self.assertErrorTypesMatches(errors, @@ -940,7 +970,8 @@ alias2 = {paid=2020-03-16,amount=23} cf_total_budget="0", cf_nlnet_milestone="milestone 1", cf_payees_list=cf_payees_list, - summary=""), + summary="", + cf_is_in_nlnet_mou2="Yes"), ], EXAMPLE_CONFIG) self.assertErrorTypesMatches(bg.get_errors(), error_types) self.assertEqual(len(bg.nodes), 1) @@ -1168,7 +1199,8 @@ alias2 = {paid=2020-03-16,amount=23} cf_total_budget="10", cf_nlnet_milestone="milestone 1", cf_payees_list="person1 = 5\nperson2 = 10", - summary=""), + summary="", + cf_is_in_nlnet_mou2="Yes"), ], EXAMPLE_CONFIG) errors = bg.get_errors() self.assertErrorTypesMatches(errors, @@ -1186,7 +1218,8 @@ alias2 = {paid=2020-03-16,amount=23} cf_total_budget="0", cf_nlnet_milestone="milestone 1", cf_payees_list=cf_payees_list, - summary=""), + summary="", + cf_is_in_nlnet_mou2="Yes"), ], EXAMPLE_CONFIG).get_errors() self.assertErrorTypesMatches(errors, [BudgetGraphPayeesParseError]) @@ -1282,7 +1315,8 @@ alias2 = {paid=2020-03-16,amount=23} cf_total_budget="10", cf_nlnet_milestone="milestone 1", cf_payees_list="""person1 = -10""", - summary=""), + summary="", + cf_is_in_nlnet_mou2="Yes"), ], EXAMPLE_CONFIG) errors = bg.get_errors() self.assertErrorTypesMatches(errors, @@ -1306,7 +1340,8 @@ alias2 = {paid=2020-03-16,amount=23} person1 = 5 alias1 = 5 """, - summary=""), + summary="", + cf_is_in_nlnet_mou2="Yes"), ], EXAMPLE_CONFIG) errors = bg.get_errors() self.assertErrorTypesMatches(errors, []) @@ -1348,7 +1383,8 @@ alias2 = {paid=2020-03-16,amount=23} cf_total_budget="10", cf_nlnet_milestone="milestone 2", cf_payees_list="", - summary=""), + summary="", + cf_is_in_nlnet_mou2="Yes"), ], EXAMPLE_CONFIG) errors = bg.get_errors() self.assertErrorTypesMatches(errors, @@ -1364,7 +1400,8 @@ alias2 = {paid=2020-03-16,amount=23} cf_total_budget="0", cf_nlnet_milestone="milestone 2", cf_payees_list="", - summary=""), + summary="", + cf_is_in_nlnet_mou2="Yes"), ], EXAMPLE_CONFIG) errors = bg.get_errors() self.assertErrorTypesMatches(errors, []) @@ -1377,14 +1414,16 @@ alias2 = {paid=2020-03-16,amount=23} cf_total_budget="10", cf_nlnet_milestone="milestone 1", cf_payees_list="person1 = 3\nperson2 = 7", - summary=""), + summary="", + cf_is_in_nlnet_mou2="Yes"), MockBug(bug_id=2, cf_budget_parent=None, cf_budget="10", cf_total_budget="10", cf_nlnet_milestone="milestone 2", cf_payees_list="person3 = 5\nperson2 = 5", - summary=""), + summary="", + cf_is_in_nlnet_mou2="Yes"), ], EXAMPLE_CONFIG) self.assertErrorTypesMatches(bg.get_errors(), []) person1 = EXAMPLE_CONFIG.people["person1"] @@ -1443,6 +1482,68 @@ alias2 = {paid=2020-03-16,amount=23} self.assertEqual(bg.nodes[1].assignee, EXAMPLE_CONFIG.people["person2"]) + def test_closest_bug_in_mou(self): + bg = BudgetGraph([ + MockBug(bug_id=1, cf_nlnet_milestone="milestone 1", + cf_is_in_nlnet_mou2="Yes"), + MockBug(bug_id=2, cf_budget_parent=1, + cf_nlnet_milestone="milestone 1", + cf_is_in_nlnet_mou2="Yes"), + MockBug(bug_id=3, cf_budget_parent=2, + cf_nlnet_milestone="milestone 1", + cf_is_in_nlnet_mou2="Yes"), + MockBug(bug_id=4, cf_budget_parent=2, + cf_nlnet_milestone="milestone 1"), + MockBug(bug_id=5, cf_budget_parent=4, + cf_nlnet_milestone="milestone 1"), + MockBug(bug_id=6), + ], EXAMPLE_CONFIG) + errors = bg.get_errors() + self.assertErrorTypesMatches(errors, []) + self.assertEqual(bg.nodes[1].closest_bug_in_mou, bg.nodes[1]) + self.assertEqual(bg.nodes[2].closest_bug_in_mou, bg.nodes[2]) + self.assertEqual(bg.nodes[3].closest_bug_in_mou, bg.nodes[3]) + self.assertEqual(bg.nodes[4].closest_bug_in_mou, bg.nodes[2]) + self.assertEqual(bg.nodes[5].closest_bug_in_mou, bg.nodes[2]) + self.assertEqual(bg.nodes[6].closest_bug_in_mou, None) + + def test_root_with_milestone_not_in_mou(self): + bg = BudgetGraph([ + MockBug(bug_id=1, cf_nlnet_milestone="milestone 1"), + ], EXAMPLE_CONFIG) + errors = bg.get_errors() + self.assertErrorTypesMatches(errors, + [BudgetGraphRootWithMilestoneNotInMoU]) + self.assertEqual(errors[0].bug_id, 1) + self.assertEqual(errors[0].root_bug_id, 1) + self.assertEqual(errors[0].milestone, "milestone 1") + + def test_budget_graph_in_mou_without_milestone(self): + bg = BudgetGraph([ + MockBug(bug_id=1, cf_is_in_nlnet_mou2="Yes"), + ], EXAMPLE_CONFIG) + errors = bg.get_errors() + self.assertErrorTypesMatches(errors, + [BudgetGraphInMoUWithoutMilestone]) + self.assertEqual(errors[0].bug_id, 1) + self.assertEqual(errors[0].root_bug_id, 1) + + def test_in_mou_but_parent_not_in_mou(self): + bg = BudgetGraph([ + MockBug(bug_id=1, cf_nlnet_milestone="milestone 1", + cf_is_in_nlnet_mou2="Yes"), + MockBug(bug_id=2, cf_nlnet_milestone="milestone 1", + cf_budget_parent=1), + MockBug(bug_id=3, cf_nlnet_milestone="milestone 1", + cf_budget_parent=2, cf_is_in_nlnet_mou2="Yes"), + ], EXAMPLE_CONFIG) + errors = bg.get_errors() + self.assertErrorTypesMatches(errors, + [BudgetGraphInMoUButParentNotInMoU]) + self.assertEqual(errors[0].bug_id, 3) + self.assertEqual(errors[0].root_bug_id, 1) + self.assertEqual(errors[0].parent_bug_id, 2) + if __name__ == "__main__": unittest.main() diff --git a/src/budget_sync/test/test_write_budget_csv.py b/src/budget_sync/test/test_write_budget_csv.py index a95a686..08182fd 100644 --- a/src/budget_sync/test/test_write_budget_csv.py +++ b/src/budget_sync/test/test_write_budget_csv.py @@ -41,7 +41,8 @@ class TestWriteBudgetMarkdown(unittest.TestCase): person2 = {amount=421,paid=2020-01-01} """, summary="", - assigned_to="person2@example.com"), + assigned_to="person2@example.com", + cf_is_in_nlnet_mou2="Yes"), MockBug(bug_id=2, cf_budget_parent=None, cf_budget="0", @@ -49,7 +50,8 @@ class TestWriteBudgetMarkdown(unittest.TestCase): cf_nlnet_milestone="milestone 2", cf_payees_list="", summary="", - assigned_to="person2@example.com"), + assigned_to="person2@example.com", + cf_is_in_nlnet_mou2="Yes"), ], config) self.assertEqual([], budget_graph.get_errors()) # pretty_print(budget_graph) diff --git a/src/budget_sync/test/test_write_budget_markdown.py b/src/budget_sync/test/test_write_budget_markdown.py index c6dba2f..0a8b5dc 100644 --- a/src/budget_sync/test/test_write_budget_markdown.py +++ b/src/budget_sync/test/test_write_budget_markdown.py @@ -1,7 +1,7 @@ import unittest from budget_sync.config import Config from budget_sync.test.mock_bug import MockBug -from budget_sync.test.mock_path import MockPath, DIR +from budget_sync.test.mock_path import MockFilesystem, MockPath, DIR from budget_sync.test.test_mock_path import make_filesystem_and_report_if_error from budget_sync.budget_graph import BudgetGraph from budget_sync.write_budget_markdown import ( @@ -20,6 +20,38 @@ class TestWriteBudgetMarkdown(unittest.TestCase): self.assertEqual(markdown_escape("abc * def_k < &k"), r"abc \* def\_k < &k") + def format_files_dict(self, files): + assert isinstance(files, dict) + files_list: "list[str]" = [] + for path, contents in files.items(): + assert isinstance(path, str) + if contents is DIR: + files_list.append(f" {path!r}: DIR,") + continue + assert isinstance(contents, bytes) + lines: "list[str]" = [] + for line in contents.splitlines(keepends=True): + lines.append(f" {line!r}") + if len(lines) == 0: + files_list.append(f" {path!r}: b'',") + else: + lines_str = '\n'.join(lines) + files_list.append(f" {path!r}: (\n{lines_str}\n ),") + if len(files_list) == 0: + return "{}" + return "{\n" + "\n".join(files_list) + "\n}" + + def assertFiles(self, expected_files, filesystem: MockFilesystem): + files = filesystem.files + self.assertIsInstance(expected_files, dict) + if files == expected_files: + return + files_str = self.format_files_dict(files) + expected_files_str = self.format_files_dict(expected_files) + self.assertEqual( + files, expected_files, + msg=f"\nfiles:\n{files_str}\nexpected:\n{expected_files_str}") + def test(self): config = Config.from_str( """ @@ -45,16 +77,133 @@ class TestWriteBudgetMarkdown(unittest.TestCase): with make_filesystem_and_report_if_error(self) as filesystem: output_dir = MockPath("/output_dir/", filesystem=filesystem) write_budget_markdown(budget_graph, output_dir) - self.assertEqual({ - "/": DIR, - "/output_dir": DIR, - '/output_dir/person1.mdwn': b'\n\n# Person One (person1)\n\n\n\n#' - b' Status Tracking\n\n', - '/output_dir/person2.mdwn': b'\n\n# Person Two (person2)\n\n\n\n#' - b' Status Tracking\n\n', - }, filesystem.files) + self.assertFiles({ + '/': DIR, + '/output_dir': DIR, + '/output_dir/person1.mdwn': ( + b'\n' + b'\n' + b'# Person One (person1)\n' + b'\n' + b'\n' + b'\n' + b'# Status Tracking\n' + b'\n' + ), + '/output_dir/person2.mdwn': ( + b'\n' + b'\n' + b'# Person Two (person2)\n' + b'\n' + b'\n' + b'\n' + b'# Status Tracking\n' + b'\n' + ), + }, filesystem) + + def test2(self): + config = Config.from_str( + """ + bugzilla_url = "https://bugzilla.example.com/" + [milestones] + "milestone 1" = { canonical_bug_id = 1 } + [people."person1"] + email = "person1@example.com" + full_name = "Person One" + [people."person2"] + full_name = "Person Two" + """) + budget_graph = BudgetGraph([ + MockBug(bug_id=1, + cf_budget_parent=None, + cf_budget="700", + cf_total_budget="1000", + cf_nlnet_milestone="milestone 1", + cf_payees_list="", + summary="", + assigned_to="person1@example.com", + cf_is_in_nlnet_mou2="Yes"), + MockBug(bug_id=2, + cf_budget_parent=1, + cf_budget="100", + cf_total_budget="300", + cf_nlnet_milestone="milestone 1", + cf_payees_list="person2 = 100", + summary="", + assigned_to="person1@example.com", + cf_is_in_nlnet_mou2="Yes"), + MockBug(bug_id=3, + cf_budget_parent=2, + cf_budget="100", + cf_total_budget="200", + cf_nlnet_milestone="milestone 1", + cf_payees_list="person1 = 100", + summary="", + assigned_to="person1@example.com"), + MockBug(bug_id=4, + cf_budget_parent=3, + cf_budget="100", + cf_total_budget="100", + cf_nlnet_milestone="milestone 1", + cf_payees_list="person2 = 100", + summary="", + assigned_to="person1@example.com"), + ], config) + self.assertEqual([], budget_graph.get_errors()) + with make_filesystem_and_report_if_error(self) as filesystem: + output_dir = MockPath("/output_dir/", filesystem=filesystem) + write_budget_markdown(budget_graph, output_dir) + self.assertFiles({ + '/': DIR, + '/output_dir': DIR, + '/output_dir/person1.mdwn': ( + b'\n' + b'\n' + b'# Person One (person1)\n' + b'\n' + b'\n' + b'\n' + b'# Status Tracking\n' + b'\n' + b'\n' + b'## Payment not yet submitted\n' + b'\n' + b'\n' + b'### milestone 1\n' + b'\n' + b'* [Bug #3](https://bugzilla.example.com/show_bug.cgi?id=3):\n' + b' \n' + b' * €100 which is the total amount\n' + b' * the closest parent task which is in the MoU is\n' + b' [Bug #2](https://bugzilla.example.com/show_bug.cgi?id=2)\n' + ), + '/output_dir/person2.mdwn': ( + b'\n' + b'\n' + b'# Person Two (person2)\n' + b'\n' + b'\n' + b'\n' + b'# Status Tracking\n' + b'\n' + b'\n' + b'## Payment not yet submitted\n' + b'\n' + b'\n' + b'### milestone 1\n' + b'\n' + b'* [Bug #2](https://bugzilla.example.com/show_bug.cgi?id=2):\n' + b' \n' + b' * €100 which is the total amount\n' + b' * this task is in the MoU\n' + b'* [Bug #4](https://bugzilla.example.com/show_bug.cgi?id=4):\n' + b' \n' + b' * €100 which is the total amount\n' + b' * the closest parent task which is in the MoU is\n' + b' [Bug #2](https://bugzilla.example.com/show_bug.cgi?id=2)\n' + ), + }, filesystem) # TODO: add more test cases diff --git a/src/budget_sync/write_budget_markdown.py b/src/budget_sync/write_budget_markdown.py index f98bf2c..8c1d096 100644 --- a/src/budget_sync/write_budget_markdown.py +++ b/src/budget_sync/write_budget_markdown.py @@ -97,6 +97,18 @@ class MarkdownWriter: else: print(f" * €{payment.amount} which is the total amount", file=self.buffer) + closest = node.closest_bug_in_mou + if closest is node: + print(f" * this task is in the MoU", + file=self.buffer) + elif closest is not None: + print(f" * the closest parent task which is in the MoU is\n" + f" [Bug #{closest.bug.id}]({closest.bug_url})", + file=self.buffer) + else: + print(f" * neither this task nor any parent tasks are in " + f"the MoU", + file=self.buffer) def _markdown_for_person(person: Person, -- 2.30.2