#45 Add help function for distgit bugzilla sync toddler
Merged 3 years ago by zlopez. Opened 3 years ago by zlopez.
fedora-infra/ zlopez/toddlers distgit_bugzilla_sync_help  into  master

file modified
+1
@@ -1,5 +1,6 @@ 

  beanbag

  bs4

+ defusedxml

  fedora-messaging

  koji

  requests

@@ -1,5 +1,5 @@ 

  import logging

- from unittest.mock import Mock, patch

+ from unittest.mock import call, MagicMock, Mock, patch

  import xmlrpc.client

  

  import pytest
@@ -228,3 +228,694 @@ 

          mock_bz.assert_called_with()

          server.updateperms.assert_not_called()

          assert output == []

+ 

+ 

+ class TestGetProductInfoPackager:

+     """Test class for `toddlers.utils.bugzilla_system.get_product_info_packages` function."""

+ 

+     @patch("toddlers.utils.bugzilla_system.get_bz")

+     def test_get_product_info_packages(self, mock_bz):

+         """ Assert that compat_api 'component.get' is handled correctly. """

+         server = MagicMock()

+         server.product_get.return_value = {

+             "components": [

+                 {

+                     "name": "foo",

+                     "default_assigned_to": "default_assignee",

+                     "description": "description",

+                     "default_qa_contact": "default_qa_contact",

+                     "default_cc": "default_cc",

+                     "is_active": True,

+                 }

+             ]

+         }

+         mock_bz.return_value = server

+ 

+         output = toddlers.utils.bugzilla_system.get_product_info_packages(

+             collection="Fedora",

+         )

+ 

+         mock_bz.assert_called_with()

+         server.product_get.assert_called_with(

+             names=["Fedora"],

+             include_fields=[

+                 "components.name",

+                 "components.default_assigned_to",

+                 "components.description",

+                 "components.default_qa_contact",

+                 "components.default_cc",

+                 "components.is_active",

+             ],

+         )

+ 

+         assert output == {

+             "foo": {

+                 "initialowner": "default_assignee",

+                 "description": "description",

+                 "initialqacontact": "default_qa_contact",

+                 "initialcclist": "default_cc",

+                 "is_active": True,

+             }

+         }

+ 

+ 

+ class TestReassignTicketsToAssignee:

+     """Test class for `toddlers.utils.bugzilla_system.reassign_tickets_to_assignee` function."""

+ 

+     # Query used in multiple tests

+     query = {

+         "product": "Fedora",

+         "component": "foo",

+         "bug_status": [

+             "NEW",

+             "ASSIGNED",

+             "ON_DEV",

+             "ON_QA",

+             "MODIFIED",

+             "POST",

+             "FAILS_QA",

+             "PASSES_QA",

+             "RELEASE_PENDING",

+         ],

+         "version": ["rpms"],

+     }

+ 

+     @patch("toddlers.utils.bugzilla_system.get_bz")

+     def test_reassign_tickets_to_assignee_dry_run(self, mock_bz):

+         """Assert that dry_run doesn't do any change."""

+         old_poc = "khorne@fedoraproject.org"

+         new_poc = "tzeentch@fedoraproject.org"

+         bug = Mock()

+         bug.assigned_to = old_poc

+ 

+         server = Mock()

+         server.query.return_value = [bug]

+ 

+         mock_bz.return_value = server

+ 

+         toddlers.utils.bugzilla_system.reassign_tickets_to_assignee(

+             new_poc=new_poc,

+             old_poc=old_poc,

+             product="Fedora",

+             package="foo",

+             versions=["rpms"],

+             dry_run=True,

+         )

+ 

+         mock_bz.assert_called_with()

+         server.query.assert_called_with(query=self.query)

+         bug.setassignee.asssert_not_called()

+ 

+     @patch("toddlers.utils.bugzilla_system.get_bz")

+     def test_reassign_tickets_to_assignee_print_fas_names_no_fas_info(

+         self, mock_bz, caplog

+     ):

+         """Assert that `print_fas_names` parameter is handled correctly

+         when the FAS user info is missing.

+         """

+         old_poc = "khorne@fedoraproject.org"

+         new_poc = "tzeentch@fedoraproject.org"

+         bug = Mock()

+         bug.bug_id = 1

+         bug.assigned_to = old_poc

+ 

+         server = Mock()

+         server.query.return_value = [bug]

+ 

+         mock_bz.return_value = server

+ 

+         with caplog.at_level(logging.DEBUG):

+             toddlers.utils.bugzilla_system.reassign_tickets_to_assignee(

+                 new_poc=new_poc,

+                 old_poc=old_poc,

+                 product="Fedora",

+                 package="foo",

+                 versions=["rpms"],

+                 dry_run=True,

+                 print_fas_names=True,

+             )

+ 

+         assert (

+             "Fedora/foo reassigning bug #1 from khorne@... to tzeentch@..."

+             in caplog.text

+         )

+ 

+         mock_bz.assert_called_with()

+         server.query.assert_called_with(query=self.query)

+         bug.setassignee.asssert_not_called()

+ 

+     @patch("toddlers.utils.bugzilla_system.get_bz")

+     def test_reassign_tickets_to_assignee_print_fas_names_fas_info(

+         self, mock_bz, caplog

+     ):

+         """Assert that `print_fas_names` parameter is handled correctly

+         when the FAS user info is available.

+         """

+         old_poc = "khorne@fedoraproject.org"

+         new_poc = "tzeentch@fedoraproject.org"

+         bug = Mock()

+         bug.bug_id = 1

+         bug.assigned_to = old_poc

+ 

+         server = Mock()

+         server.query.return_value = [bug]

+ 

+         mock_bz.return_value = server

+ 

+         with caplog.at_level(logging.DEBUG):

+             toddlers.utils.bugzilla_system.reassign_tickets_to_assignee(

+                 new_poc=new_poc,

+                 old_poc=old_poc,

+                 product="Fedora",

+                 package="foo",

+                 versions=["rpms"],

+                 fas_users_info={

+                     "khorne@fedoraproject.org": "Blood God",

+                     "tzeentch@fedoraproject.org": "Master of Fate",

+                 },

+                 dry_run=True,

+                 print_fas_names=True,

+             )

+ 

+         assert (

+             "Fedora/foo reassigning bug #1 from Blood God to Master of Fate"

+             in caplog.text

+         )

+ 

+         mock_bz.assert_called_with()

+         server.query.assert_called_with(query=self.query)

+         bug.setassignee.asssert_not_called()

+ 

+     @patch("toddlers.utils.bugzilla_system.execute_bugzilla_call")

+     @patch("toddlers.utils.bugzilla_system.get_bz")

+     def test_reassign_tickets_to_assignee_xmlrpc_Fault(self, mock_bz, mock_bz_call):

+         """Assert that `xmlrpc.client.Fault` is raised correctly."""

+         old_poc = "khorne@fedoraproject.org"

+         new_poc = "tzeentch@fedoraproject.org"

+         bug = Mock()

+         bug.bug_id = 1

+         bug.assigned_to = old_poc

+ 

+         server = Mock()

+ 

+         mock_bz.return_value = server

+         mock_bz_call.side_effect = ([bug], xmlrpc.client.Fault(50, "Fault"))

+ 

+         with pytest.raises(xmlrpc.client.Fault) as exc_info:

+             toddlers.utils.bugzilla_system.reassign_tickets_to_assignee(

+                 new_poc=new_poc,

+                 old_poc=old_poc,

+                 product="Fedora",

+                 package="foo",

+                 versions=["rpms"],

+             )

+ 

+         assert exc_info.value.args == ("tzeentch@fedoraproject.org", 50, "Fault")

+ 

+         mock_bz.assert_called_with()

+         mock_bz_call.assert_has_calls(

+             [

+                 call(server.query, {"query": self.query}),

+                 call(

+                     bug.setassignee,

+                     {

+                         "assigned_to": new_poc,

+                         "comment": "This package has changed maintainer in Fedora. "

+                         + "Reassigning to the new maintainer of this component.",

+                     },

+                 ),

+             ]

+         )

+ 

+     @patch("toddlers.utils.bugzilla_system.execute_bugzilla_call")

+     @patch("toddlers.utils.bugzilla_system.get_bz")

+     def test_reassign_tickets_to_assignee_xmlrpc_ProtocolError(

+         self, mock_bz, mock_bz_call

+     ):

+         """Assert that `xmlrpc.client.ProtocolError` is raised correctly."""

+         old_poc = "khorne@fedoraproject.org"

+         new_poc = "tzeentch@fedoraproject.org"

+         bug = Mock()

+         bug.bug_id = 1

+         bug.assigned_to = old_poc

+ 

+         server = Mock()

+ 

+         mock_bz.return_value = server

+         mock_bz_call.side_effect = (

+             [bug],

+             xmlrpc.client.ProtocolError("Error", 10, "Error message", {}),

+         )

+ 

+         with pytest.raises(xmlrpc.client.ProtocolError) as exc_info:

+             toddlers.utils.bugzilla_system.reassign_tickets_to_assignee(

+                 new_poc=new_poc,

+                 old_poc=old_poc,

+                 product="Fedora",

+                 package="foo",

+                 versions=["rpms"],

+             )

+ 

+         assert exc_info.value.args == ("ProtocolError", 10, "Error message")

+ 

+         mock_bz.assert_called_with()

+         mock_bz_call.assert_has_calls(

+             [

+                 call(server.query, {"query": self.query}),

+                 call(

+                     bug.setassignee,

+                     {

+                         "assigned_to": new_poc,

+                         "comment": "This package has changed maintainer in Fedora. "

+                         + "Reassigning to the new maintainer of this component.",

+                     },

+                 ),

+             ]

+         )

+ 

+ 

+ @pytest.fixture

+ def owner():

+     """Fixture for owner value.

+     Used in test classes for both add and edit component.

+     """

+     return "khorne@fedoraproject.org"

+ 

+ 

+ @pytest.fixture

+ def data(owner):

+     """Fixture for data value.

+     Used in test classes for both add and edit component.

+     """

+     return {

+         "product": "Fedora",

+         "component": "foo",

+         "initialowner": owner,

+         "description": "description",

+         "initialqacontact": "nurgle@fedoraproject.org",

+         "initialcclist": [owner],

+         "is_active": True,

+     }

+ 

+ 

+ class TestAddComponent:

+     """Test class for `toddlers.utils.bugzilla_system.add_component` function."""

+ 

+     @patch("toddlers.utils.bugzilla_system.get_bz")

+     def test_add_component_dry_run(self, mock_bz, caplog, owner):

+         """Assert that dry_run doesn't do any change."""

+         server = Mock()

+         mock_bz.return_value = server

+ 

+         with caplog.at_level(logging.DEBUG):

+             toddlers.utils.bugzilla_system.add_component(

+                 product="Fedora",

+                 owner=owner,

+                 package="foo",

+                 qa_contact="nurgle@fedoraproject.org",

+                 cc_list=[owner],

+                 dry_run=True,

+             )

+ 

+         assert (

+             caplog.records[0].getMessage()

+             == f"[ADDCOMP] Fedora/foo initialowner set to `{owner}`"

+         )

+         assert (

+             caplog.records[1].getMessage()

+             == "[ADDCOMP] Fedora/foo description set to `NA`"

+         )

+         assert (

+             caplog.records[2].getMessage()

+             == "[ADDCOMP] Fedora/foo initialqacontact set to `nurgle@fedoraproject.org`"

+         )

+         assert (

+             caplog.records[3].getMessage()

+             == f"[ADDCOMP] Fedora/foo initialcclist set to `['{owner}']`"

+         )

+         assert (

+             caplog.records[4].getMessage()

+             == "[ADDCOMP] Fedora/foo is_active set to `True`"

+         )

+         server.addcomponent.assert_not_called()

+ 

+     @patch("toddlers.utils.bugzilla_system.get_bz")

+     def test_add_component_retired(self, mock_bz, caplog, owner):

+         """Assert that nothing is done for retired package."""

+         server = Mock()

+         mock_bz.return_value = server

+ 

+         with caplog.at_level(logging.DEBUG):

+             toddlers.utils.bugzilla_system.add_component(

+                 product="Fedora",

+                 owner=owner,

+                 package="foo",

+                 qa_contact="nurgle@fedoraproject.org",

+                 cc_list=[owner],

+                 retired=True,

+                 dry_run=True,

+             )

+ 

+         assert caplog.records[0].getMessage() == "[NOADD] Fedora/foo is retired"

+         server.addcomponent.assert_not_called()

+ 

+     @patch("toddlers.utils.bugzilla_system.get_bz")

+     def test_add_component_print_fas_names(self, mock_bz, caplog, owner, data):

+         """Assert that print_fas_names works correctly."""

+         server = Mock()

+         mock_bz.return_value = server

+ 

+         with caplog.at_level(logging.DEBUG):

+             toddlers.utils.bugzilla_system.add_component(

+                 product="Fedora",

+                 owner=owner,

+                 package="foo",

+                 qa_contact="nurgle@fedoraproject.org",

+                 cc_list=[owner],

+                 fas_users_info={

+                     "khorne@fedoraproject.org": "Blood God",

+                     "nurgle@fedoraproject.org": "Papa Nurgle",

+                 },

+                 print_fas_names=True,

+             )

+ 

+         assert (

+             caplog.records[0].getMessage()

+             == "[ADDCOMP] Fedora/foo initialowner set to FAS name(s) `Blood God`"

+         )

+         assert (

+             caplog.records[1].getMessage()

+             == "[ADDCOMP] Fedora/foo description set to `NA`"

+         )

+         assert (

+             caplog.records[2].getMessage()

+             == "[ADDCOMP] Fedora/foo initialqacontact set to FAS name(s) `Papa Nurgle`"

+         )

+         assert (

+             caplog.records[3].getMessage()

+             == "[ADDCOMP] Fedora/foo initialcclist set to FAS name(s) `['Blood God']`"

+         )

+         assert (

+             caplog.records[4].getMessage()

+             == "[ADDCOMP] Fedora/foo is_active set to `True`"

+         )

+ 

+         data["description"] = "NA"

+ 

+         server.addcomponent.assert_called_with(data=data)

+ 

+     @patch("toddlers.utils.bugzilla_system.execute_bugzilla_call")

+     @patch("toddlers.utils.bugzilla_system.get_bz")

+     def test_add_component_xmlrpc_Fault(self, mock_bz, mock_bz_call, owner, data):

+         """Assert that `xmlrpc.client.Fault` is raised correctly."""

+         server = Mock()

+         mock_bz.return_value = server

+         mock_bz_call.side_effect = xmlrpc.client.Fault(50, "Fault")

+ 

+         with pytest.raises(xmlrpc.client.Fault) as exc_info:

+             toddlers.utils.bugzilla_system.add_component(

+                 product="Fedora",

+                 owner=owner,

+                 package="foo",

+                 qa_contact="nurgle@fedoraproject.org",

+                 cc_list=[owner],

+                 fas_users_info={

+                     "khorne@fedoraproject.org": "Blood God",

+                     "nurgle@fedoraproject.org": "Papa Nurgle",

+                 },

+                 print_fas_names=True,

+             )

+ 

+         data["description"] = "NA"

+ 

+         assert exc_info.value.args == (data, 50, "Fault")

+ 

+         mock_bz_call.assert_called_with(server.addcomponent, {"data": data})

+ 

+ 

+ class TestEditComponent:

+     """Test class for `toddlers.utils.bugzilla_system.edit_component` function."""

+ 

+     @patch("toddlers.utils.bugzilla_system.get_bz")

+     def test_edit_component_no_change(self, mock_bz, caplog, owner):

+         """Assert that nothing is done when no change occurs."""

+         server = Mock()

+         mock_bz.return_value = server

+ 

+         with caplog.at_level(logging.DEBUG):

+             toddlers.utils.bugzilla_system.edit_component(

+                 owner=owner,

+                 product="Fedora",

+                 package="foo",

+                 component={

+                     "initialowner": owner,

+                     "description": "description",

+                     "initialqacontact": "nurgle@fedoraproject.org",

+                     "initialcclist": "",

+                     "is_active": True,

+                 },

+                 cc_list=[],

+                 versions=[],

+                 qa_contact="nurgle@fedoraproject.org",

+                 print_no_change=True,

+             )

+ 

+         mock_bz.assert_called_with()

+         assert caplog.records[0].getMessage() == "[NOCHANGE] Fedora/foo"

+ 

+     @patch("toddlers.utils.bugzilla_system.get_bz")

+     def test_edit_component_change_same_owner_dry_run(self, mock_bz, caplog, owner):

+         """Assert that component is not updated when change occurs, but dry run is set."""

+         server = Mock()

+         mock_bz.return_value = server

+ 

+         with caplog.at_level(logging.DEBUG):

+             toddlers.utils.bugzilla_system.edit_component(

+                 owner=owner,

+                 product="Fedora",

+                 package="foo",

+                 component={

+                     "initialowner": owner,

+                     "description": "",

+                     "initialqacontact": "",

+                     "initialcclist": [],

+                     "is_active": False,

+                 },

+                 cc_list=[owner],

+                 versions=[],

+                 description="description",

+                 qa_contact="nurgle@fedoraproject.org",

+                 dry_run=True,

+             )

+ 

+         mock_bz.assert_called_with()

+         server.editcomponent.assert_not_called()

+         assert (

+             caplog.records[0].getMessage()

+             == "[EDITCOMP] Fedora/foo description changed from `` to `description`"

+         )

+         assert (

+             caplog.records[1].getMessage()

+             == "[EDITCOMP] Fedora/foo initialqacontact changed from `` "

+             "to `nurgle@fedoraproject.org`"

+         )

+         assert (

+             caplog.records[2].getMessage()

+             == "[EDITCOMP] Fedora/foo initialcclist changed from `[]` "

+             "to `['khorne@fedoraproject.org']`"

+         )

+         assert (

+             caplog.records[3].getMessage()

+             == "[EDITCOMP] Fedora/foo is_active changed from `False` to `True`"

+         )

+ 

+     @patch("toddlers.utils.bugzilla_system.reassign_tickets_to_assignee")

+     @patch("toddlers.utils.bugzilla_system.get_bz")

+     def test_edit_component_change_print_fas_names(

+         self, mock_bz, mock_set_pkg_owner, caplog, owner, data

+     ):

+         """Assert that FAS names are printed when the option is enabled."""

+         server = Mock()

+         mock_bz.return_value = server

+ 

+         with caplog.at_level(logging.DEBUG):

+             toddlers.utils.bugzilla_system.edit_component(

+                 owner=owner,

+                 product="Fedora",

+                 package="foo",

+                 component={

+                     "initialowner": "tzeentch@fedoraproject.org",

+                     "description": "",

+                     "initialqacontact": "",

+                     "initialcclist": ["slaanesh@fedoraproject.org"],

+                     "is_active": False,

+                 },

+                 cc_list=[owner],

+                 versions=[],

+                 description="description",

+                 qa_contact="nurgle@fedoraproject.org",

+                 fas_users_info={

+                     owner: "Blood God",

+                     "nurgle@fedoraproject.org": "Papa Nurgle",

+                     "tzeentch@fedoraproject.org": "Master of Fate",

+                 },

+                 print_fas_names=True,

+             )

+ 

+         mock_bz.assert_called_with()

+         server.editcomponent.assert_called_with(data=data)

+         mock_set_pkg_owner.assert_called_with(

+             new_poc=owner,

+             old_poc="tzeentch@fedoraproject.org",

+             product="Fedora",

+             package="foo",

+             versions=[],

+             fas_users_info={

+                 owner: "Blood God",

+                 "nurgle@fedoraproject.org": "Papa Nurgle",

+                 "tzeentch@fedoraproject.org": "Master of Fate",

+             },

+             dry_run=False,

+             print_fas_names=True,

+         )

+         assert (

+             caplog.records[0].getMessage()

+             == "[EDITCOMP] Fedora/foo initialowner changed from `Master of Fate` to "

+             "FAS name(s) `Blood God`"

+         )

+         assert (

+             caplog.records[1].getMessage()

+             == "[EDITCOMP] Fedora/foo description changed from `` to `description`"

+         )

+         assert (

+             caplog.records[2].getMessage()

+             == "[EDITCOMP] Fedora/foo initialqacontact changed from `` to FAS name(s) `Papa Nurgle`"

+         )

+         assert (

+             caplog.records[3].getMessage()

+             == "[EDITCOMP] Fedora/foo initialcclist changed from "

+             "`['slaanesh@fedoraproject.org']` to FAS name(s) `['Blood God']`"

+         )

+         assert (

+             caplog.records[4].getMessage()

+             == "[EDITCOMP] Fedora/foo is_active changed from `False` to `True`"

+         )

+ 

+     @patch("toddlers.utils.bugzilla_system.execute_bugzilla_call")

+     @patch("toddlers.utils.bugzilla_system.get_bz")

+     def test_edit_component_change_xmlrpc_fault(

+         self, mock_bz, mock_bz_call, owner, data

+     ):

+         """Assert that `xmlrpc.client.Fault` is handled correctly."""

+         server = Mock()

+         mock_bz.return_value = server

+         mock_bz_call.side_effect = xmlrpc.client.Fault(50, "Fault")

+ 

+         with pytest.raises(xmlrpc.client.Fault) as exc:

+             toddlers.utils.bugzilla_system.edit_component(

+                 owner=owner,

+                 product="Fedora",

+                 package="foo",

+                 component={

+                     "initialowner": "tzeentch@fedoraproject.org",

+                     "description": "",

+                     "initialqacontact": "",

+                     "initialcclist": [],

+                     "is_active": False,

+                 },

+                 cc_list=[owner],

+                 versions=[],

+                 description="description",

+                 qa_contact="nurgle@fedoraproject.org",

+                 fas_users_info={

+                     owner: "Blood God",

+                     "nurgle@fedoraproject.org": "Papa Nurgle",

+                     "tzeentch@fedoraproject.org": "Master of Fate",

+                 },

+             )

+ 

+         assert exc.value.args == (data, 50, "Fault")

+ 

+         mock_bz.assert_called_with()

+         mock_bz_call.assert_called_with(server.editcomponent, {"data": data})

+ 

+     @patch("toddlers.utils.bugzilla_system.execute_bugzilla_call")

+     @patch("toddlers.utils.bugzilla_system.get_bz")

+     def test_edit_component_change_xmlrpc_ProtocolError(

+         self, mock_bz, mock_bz_call, owner, data

+     ):

+         """Assert that `xmlrpc.client.Fault` is handled correctly."""

+         server = Mock()

+         mock_bz.return_value = server

+         mock_bz_call.side_effect = xmlrpc.client.ProtocolError(

+             "Error", 10, "Error message", {}

+         )

+ 

+         with pytest.raises(xmlrpc.client.ProtocolError) as exc:

+             toddlers.utils.bugzilla_system.edit_component(

+                 owner=owner,

+                 product="Fedora",

+                 package="foo",

+                 component={

+                     "initialowner": "tzeentch@fedoraproject.org",

+                     "description": "",

+                     "initialqacontact": "",

+                     "initialcclist": [],

+                     "is_active": False,

+                 },

+                 cc_list=[owner],

+                 versions=[],

+                 description="description",

+                 qa_contact="nurgle@fedoraproject.org",

+                 fas_users_info={

+                     owner: "Blood God",

+                     "nurgle@fedoraproject.org": "Papa Nurgle",

+                     "tzeentch@fedoraproject.org": "Master of Fate",

+                 },

+             )

+ 

+         assert exc.value.args == ("ProtocolError", 10, "Error message")

+ 

+         mock_bz.assert_called_with()

+         mock_bz_call.assert_called_with(server.editcomponent, {"data": data})

+ 

+ 

+ class TestExecuteBugzillaCall:

+     """Test class for `toddlers.utils.bugzilla_system.execute_bugzilla_call` function."""

+ 

+     def test_execute_bugzilla_call(self):

+         """Assert that bugzilla call wrapper is working correctly."""

+         mock_call = MagicMock()

+         mock_call.return_value = {"name": "foo"}

+         args = {"product": "Fedora", "data": {"component": "foo"}}

+ 

+         output = toddlers.utils.bugzilla_system.execute_bugzilla_call(

+             call=mock_call, args=args

+         )

+ 

+         assert output == {"name": "foo"}

+ 

+         mock_call.assert_called_with(product="Fedora", data={"component": "foo"})

+ 

+     @patch("time.sleep")

+     def test_execute_bugzilla_call_exception(self, mock_time, caplog):

+         """Assert that bugzilla call wrapper is working correctly when exception happens."""

+         caplog.set_level(logging.DEBUG)

+         mock_call = MagicMock()

+         mock_call.side_effect = xmlrpc.client.Fault(50, "Error")

+         args = {"product": "Fedora", "data": {"component": "foo"}}

+ 

+         with pytest.raises(xmlrpc.client.Fault):

+             toddlers.utils.bugzilla_system.execute_bugzilla_call(

+                 call=mock_call, args=args, num_of_attempts=2

+             )

+ 

+         mock_call.assert_called_with(product="Fedora", data={"component": "foo"})

+ 

+         assert caplog.records[0].getMessage() == "ERROR <Fault 50: 'Error'>"

+         assert (

+             caplog.records[1].getMessage()

+             == "Query failed, going to try again in 10 seconds"

+         )

+ 

+         mock_time.assert_called_with(10)

@@ -99,3 +99,90 @@ 

          smtp_server.sendmail.assert_called_with(

              "admin@server", ["info_admin@server"], msg

          )

+ 

+     @patch("toddlers.utils.notify.smtplib")

+     def test_notify_admins_distgit_sync_error(self, mock_smtp):

+         smtp_server = Mock()

+         mock_smtp.SMTP.return_value = smtp_server

+ 

+         msg = """To: info_admin@server

+ From: admin@server

+ Subject: Errors while syncing bugzilla with the PackageDB

+ 

+ Greetings, our mighty admins,

+ 

+ While updating bugzilla with information from the Package Database some errors were encountered.

+ Don't panic!

+ Please, be so kind and see if you can do something with them. Here is the list of the errors:

+ 

+ Oops, something happened

+ Oh no, this is bad

+ """

+ 

+         errors = ["Oops, something happened", "Oh no, this is bad"]

+ 

+         toddlers.utils.notify.notify_admins_distgit_sync_error(

+             mail_server="server.mail",

+             admin_email="admin@server",

+             recipients=["info_admin@server"],

+             errors=errors,

+         )

+         mock_smtp.SMTP.assert_called_with("server.mail")

+         smtp_server.sendmail.assert_called_with(

+             "admin@server", ["info_admin@server"], msg

+         )

+ 

+     @patch("toddlers.utils.notify.smtplib")

+     def test_notify_packager_distgit_sync_error(self, mock_smtp):

+         smtp_server = Mock()

+         mock_smtp.SMTP.return_value = smtp_server

+ 

+         msg = """To: user@mail

+ From: admin@server

+ Cc: admin@server

+ Subject: Please fix your bugzilla.redhat.com account

+ 

+ Greetings.

+ 

+ You are receiving this email because there's a problem with your

+ bugzilla.redhat.com account.

+ 

+ If you recently changed the email address associated with your

+ Fedora account in the Fedora Account System, it is now out of sync

+ with your bugzilla.redhat.com account. This leads to problems

+ with Fedora packages you own or are CC'ed on bug reports for.

+ 

+ Please take one of the following actions:

+ 

+ a) login to your old bugzilla.redhat.com account and change the email

+ address to match your current email in the Fedora account system.

+ https://bugzilla.redhat.com login, click preferences, account

+ information and enter new email address.

+ 

+ b) Create a new account in bugzilla.redhat.com to match your

+ email listed in your Fedora account system account.

+ https://bugzilla.redhat.com/ click 'new account' and enter email

+ address.

+ 

+ c) Change your Fedora Account System email to match your existing

+ bugzilla.redhat.com account.

+ https://admin.fedoraproject.org/accounts login, click on 'my account',

+ then 'edit' and change your email address.

+ 

+ If you have questions or concerns, please let us know.

+ 

+ Your prompt attention in this matter is appreciated.

+ 

+ The Fedora admins.

+ """

+ 

+         toddlers.utils.notify.notify_packager_distgit_sync_error(

+             mail_server="server.mail",

+             admin_email="admin@server",

+             user_email="user@mail",

+             cc_address="admin@server",

+         )

+         mock_smtp.SMTP.assert_called_with("server.mail")

+         smtp_server.sendmail.assert_called_with(

+             "admin@server", ["user@mail", "admin@server"], msg

+         )

@@ -0,0 +1,459 @@ 

+ """

+ Module for testing `toddlers.utils.package_summaries`.

+ """

+ import bz2

+ import gzip

+ import hashlib

+ import logging

+ import lzma

+ import os

+ import tarfile

+ from unittest.mock import call, Mock

+ 

+ import pytest

+ 

+ from toddlers.utils.package_summaries import PackageSummaries

+ 

+ 

+ class TestGetPrimaryXML:

+     """Test class for `toddlers.utils.package_summaries.PackageSummaries.get_primary_xml`."""

+ 

+     def setup(self):

+         """This will be run before every test case in this class."""

+         self.ps = PackageSummaries()

+ 

+     def test_get_primary_xml_no_response(self):

+         """Assert that no response is handled correctly."""

+         req = Mock()

+         req.get.return_value = None

+         self.ps.requests_session = req

+ 

+         result = self.ps.get_primary_xml("/var/tmp/", "https://foo.bar")

+ 

+         req.get.assert_called_with("https://foo.bar/repomd.xml", verify=True)

+ 

+         assert result is None

+ 

+     def test_get_primary_xml_parse_error(self, caplog):

+         """Assert that response is handled correctly if it can't be parsed."""

+         req = Mock()

+         resp_repomd = Mock()

+         resp_repomd.text = ""

+         req.get.return_value = resp_repomd

+         self.ps.requests_session = req

+ 

+         result = self.ps.get_primary_xml("/var/tmp/", "https://foo.bar")

+ 

+         req.get.assert_called_with("https://foo.bar/repomd.xml", verify=True)

+ 

+         assert result is None

+         assert "Failed to parse https://foo.bar/repomd.xml" in caplog.text

+ 

+     def test_get_primary_xml_no_data_nodes(self, caplog):

+         """Assert that no data nodes found is handled correctly."""

+         req = Mock()

+         resp_repomd = Mock()

+         resp_repomd.text = "<repo></repo>"

+         req.get.return_value = resp_repomd

+         self.ps.requests_session = req

+ 

+         with caplog.at_level(logging.DEBUG):

+             result = self.ps.get_primary_xml("/var/tmp/", "https://foo.bar")

+ 

+         req.get.assert_called_with("https://foo.bar/repomd.xml", verify=True)

+ 

+         assert result is None

+         assert "No primary.xml could be found in https://foo.bar" in caplog.text

+ 

+     def test_get_primary_xml_multiple_primary_db(self, caplog):

+         """Assert that multiple primary dbs found is handled correctly."""

+         req = Mock()

+         resp_repomd = Mock()

+         resp_repomd.text = """

+         <root xmlns:repo="http://linux.duke.edu/metadata/repo">

+           <repo:data type="primary"/>

+           <repo:data type="primary"/>

+         </root>

+         """

+         req.get.return_value = resp_repomd

+         self.ps.requests_session = req

+ 

+         with caplog.at_level(logging.DEBUG):

+             result = self.ps.get_primary_xml("/var/tmp/", "https://foo.bar")

+ 

+         req.get.assert_called_with("https://foo.bar/repomd.xml", verify=True)

+ 

+         assert result is None

+         assert (

+             "More than one primary.xml could be found in https://foo.bar" in caplog.text

+         )

+ 

+     def test_get_primary_xml_missing_missing_location(self, caplog):

+         """Assert that missing location node is handled correctly."""

+         req = Mock()

+         resp_repomd = Mock()

+         resp_repomd.text = """

+         <root xmlns:repo="http://linux.duke.edu/metadata/repo">

+           <repo:data type="primary"/>

+         </root>

+         """

+         req.get.return_value = resp_repomd

+         self.ps.requests_session = req

+ 

+         with caplog.at_level(logging.DEBUG):

+             result = self.ps.get_primary_xml("/var/tmp/", "https://foo.bar")

+ 

+         req.get.assert_called_with("https://foo.bar/repomd.xml", verify=True)

+ 

+         assert result is None

+         assert (

+             "No valid location found for primary.xml in https://foo.bar" in caplog.text

+         )

+ 

+     def test_get_primary_xml_missing_checksum_type(self, caplog):

+         """Assert that missing location node is handled correctly."""

+         req = Mock()

+         resp_repomd = Mock()

+         resp_repomd.text = """

+         <root xmlns:repo="http://linux.duke.edu/metadata/repo">

+           <repo:data type="primary">

+             <repo:location href="https://good.place"/>

+           </repo:data>

+         </root>

+         """

+         req.get.return_value = resp_repomd

+         self.ps.requests_session = req

+ 

+         with caplog.at_level(logging.DEBUG):

+             result = self.ps.get_primary_xml("/var/tmp/", "https://foo.bar")

+ 

+         req.get.assert_called_with("https://foo.bar/repomd.xml", verify=True)

+ 

+         assert result is None

+         assert (

+             "No valid checksum information found for primary.xml in https://foo.bar"

+             in caplog.text

+         )

+ 

+     def test_get_primary_xml_no_change(self, caplog, tmpdir):

+         """Assert that nothing is done when there is no update of db."""

+         filepath = os.path.join(tmpdir, "distgit-bugzilla-sync-primary.xml")

+         hashobj = getattr(hashlib, "sha1")()

+         with open(filepath, "wb") as f:

+             f.write(b"Hello, world!")

+         with open(filepath, "rb") as f:

+             hashobj.update(f.read())

+         req = Mock()

+         resp_repomd = Mock()

+         resp_repomd.text = f"""

+         <root xmlns:repo="http://linux.duke.edu/metadata/repo">

+           <repo:data type="primary">

+             <repo:location href="repodata/db.gzip"/>

+             <repo:open-checksum type="sha1">{hashobj.hexdigest()}</repo:open-checksum>

+           </repo:data>

+         </root>

+         """

+         req.get.return_value = resp_repomd

+         self.ps.requests_session = req

+ 

+         with caplog.at_level(logging.DEBUG):

+             result = self.ps.get_primary_xml(tmpdir, "https://foo.bar")

+ 

+         req.get.assert_called_with("https://foo.bar/repomd.xml", verify=True)

+ 

+         assert result == filepath

+         assert "No change of https://foo.bar/db.gzip" in caplog.text

+ 

+     def test_get_primary_xml_no_local_file(self, caplog, tmpdir):

+         """Assert that local db is downloaded when missing."""

+         filepath = os.path.join(tmpdir, "distgit-bugzilla-sync-primary.xml")

+         req = Mock()

+         resp_repomd = Mock()

+         resp_repomd.text = """

+         <root xmlns:repo="http://linux.duke.edu/metadata/repo">

+           <repo:data type="primary">

+             <repo:location href="repodata/db.xz"/>

+             <repo:open-checksum type="sha1">0</repo:open-checksum>

+           </repo:data>

+         </root>

+         """

+         resp_archive = Mock()

+         resp_archive.content = lzma.compress(b"Hello, world!")

+         req.get.side_effect = [resp_repomd, resp_archive]

+         self.ps.requests_session = req

+ 

+         with caplog.at_level(logging.INFO):

+             result = self.ps.get_primary_xml(tmpdir, "https://foo.bar")

+ 

+         assert req.get.mock_calls == [

+             call("https://foo.bar/repomd.xml", verify=True),

+             call("https://foo.bar/db.xz", verify=True),

+         ]

+ 

+         assert result == filepath

+         assert (

+             "Downloading file: https://foo.bar/db.xz to {}/db.xz".format(tmpdir)

+             in caplog.text

+         )

+         assert "Extracting {}/db.xz to {}".format(tmpdir, filepath) in caplog.text

+ 

+         with open(filepath, "rb") as f:

+             assert f.read() == b"Hello, world!"

+ 

+     def test_get_primary_xml_different_hash(self, caplog, tmpdir):

+         """Assert that local db is downloaded when missing."""

+         filepath = os.path.join(tmpdir, "distgit-bugzilla-sync-primary.xml")

+         with open(filepath, "wb") as f:

+             f.write(b"Hello, world!")

+         req = Mock()

+         resp_repomd = Mock()

+         resp_repomd.text = """

+         <root xmlns:repo="http://linux.duke.edu/metadata/repo">

+           <repo:data type="primary">

+             <repo:location href="repodata/db.xz"/>

+             <repo:open-checksum type="sha">0</repo:open-checksum>

+           </repo:data>

+         </root>

+         """

+         resp_archive = Mock()

+         resp_archive.content = lzma.compress(b"Hello, world!")

+         req.get.side_effect = [resp_repomd, resp_archive]

+         self.ps.requests_session = req

+ 

+         with caplog.at_level(logging.INFO):

+             result = self.ps.get_primary_xml(tmpdir, "https://foo.bar")

+ 

+         assert req.get.mock_calls == [

+             call("https://foo.bar/repomd.xml", verify=True),

+             call("https://foo.bar/db.xz", verify=True),

+         ]

+ 

+         assert result == filepath

+         assert (

+             "Downloading file: https://foo.bar/db.xz to {}/db.xz".format(tmpdir)

+             in caplog.text

+         )

+         assert "Extracting {}/db.xz to {}".format(tmpdir, filepath) in caplog.text

+ 

+         with open(filepath, "rb") as f:

+             assert f.read() == b"Hello, world!"

+ 

+     def test_get_primary_xml_tar_archive(self, caplog, tmpdir):

+         """Assert that tar archived db is extracted."""

+         filepath = os.path.join(tmpdir, "distgit-bugzilla-sync-primary.xml")

+ 

+         # Create a tar compressed test file

+         test_file = os.path.join(tmpdir, "test")

+         with open(test_file, "w") as f:

+             f.write("Hello, world!")

+         tar_file = os.path.join(tmpdir, "foo.tar")

+         with tarfile.open(tar_file, "w:gz") as tar:

+             tar.add(test_file, arcname=os.path.basename(test_file))

+ 

+         archive_data = ""

+         with open(tar_file, "rb") as f:

+             archive_data = f.read()

+ 

+         archive_name = "db.tar.gz"

+         req = Mock()

+         resp_repomd = Mock()

+         resp_repomd.text = f"""

+         <root xmlns:repo="http://linux.duke.edu/metadata/repo">

+           <repo:data type="primary">

+             <repo:location href="repodata/{archive_name}"/>

+             <repo:open-checksum type="sha1">0</repo:open-checksum>

+           </repo:data>

+         </root>

+         """

+         resp_archive = Mock()

+         resp_archive.content = archive_data

+         req.get.side_effect = [resp_repomd, resp_archive]

+         self.ps.requests_session = req

+ 

+         with caplog.at_level(logging.INFO):

+             result = self.ps.get_primary_xml(tmpdir, "https://foo.bar")

+ 

+         assert req.get.mock_calls == [

+             call("https://foo.bar/repomd.xml", verify=True),

+             call(f"https://foo.bar/{archive_name}", verify=True),

+         ]

+ 

+         assert result == filepath

+         assert (

+             "Downloading file: https://foo.bar/{0} to {1}/{0}".format(

+                 archive_name, tmpdir

+             )

+             in caplog.text

+         )

+         assert (

+             "Extracting {}/{} to {}".format(tmpdir, archive_name, filepath)

+             in caplog.text

+         )

+ 

+         with open(filepath, "r") as f:

+             assert f.read() == "Hello, world!"

+ 

+     def test_get_primary_xml_gzip_archive(self, caplog, tmpdir):

+         """Assert that gzip archived db is extracted."""

+         filepath = os.path.join(tmpdir, "distgit-bugzilla-sync-primary.xml")

+ 

+         archive_data = gzip.compress(b"Hello, world!")

+         archive_name = "db.gz"

+         req = Mock()

+         resp_repomd = Mock()

+         resp_repomd.text = f"""

+         <root xmlns:repo="http://linux.duke.edu/metadata/repo">

+           <repo:data type="primary">

+             <repo:location href="repodata/{archive_name}"/>

+             <repo:open-checksum type="sha1">0</repo:open-checksum>

+           </repo:data>

+         </root>

+         """

+         resp_archive = Mock()

+         resp_archive.content = archive_data

+         req.get.side_effect = [resp_repomd, resp_archive]

+         self.ps.requests_session = req

+ 

+         with caplog.at_level(logging.INFO):

+             result = self.ps.get_primary_xml(tmpdir, "https://foo.bar")

+ 

+         assert req.get.mock_calls == [

+             call("https://foo.bar/repomd.xml", verify=True),

+             call(f"https://foo.bar/{archive_name}", verify=True),

+         ]

+ 

+         assert result == filepath

+         assert (

+             "Downloading file: https://foo.bar/{0} to {1}/{0}".format(

+                 archive_name, tmpdir

+             )

+             in caplog.text

+         )

+         assert (

+             "Extracting {}/{} to {}".format(tmpdir, archive_name, filepath)

+             in caplog.text

+         )

+ 

+         with open(filepath, "r") as f:

+             assert f.read() == "Hello, world!"

+ 

+     def test_get_primary_xml_bz2_archive(self, caplog, tmpdir):

+         """Assert that bz2 archived db is extracted."""

+         filepath = os.path.join(tmpdir, "distgit-bugzilla-sync-primary.xml")

+ 

+         archive_data = bz2.compress(b"Hello, world!")

+         archive_name = "db.bz2"

+         req = Mock()

+         resp_repomd = Mock()

+         resp_repomd.text = f"""

+         <root xmlns:repo="http://linux.duke.edu/metadata/repo">

+           <repo:data type="primary">

+             <repo:location href="repodata/{archive_name}"/>

+             <repo:open-checksum type="sha1">0</repo:open-checksum>

+           </repo:data>

+         </root>

+         """

+         resp_archive = Mock()

+         resp_archive.content = archive_data

+         req.get.side_effect = [resp_repomd, resp_archive]

+         self.ps.requests_session = req

+ 

+         with caplog.at_level(logging.INFO):

+             result = self.ps.get_primary_xml(tmpdir, "https://foo.bar")

+ 

+         assert req.get.mock_calls == [

+             call("https://foo.bar/repomd.xml", verify=True),

+             call(f"https://foo.bar/{archive_name}", verify=True),

+         ]

+ 

+         assert result == filepath

+         assert (

+             "Downloading file: https://foo.bar/{0} to {1}/{0}".format(

+                 archive_name, tmpdir

+             )

+             in caplog.text

+         )

+         assert (

+             "Extracting {}/{} to {}".format(tmpdir, archive_name, filepath)

+             in caplog.text

+         )

+ 

+         with open(filepath, "r") as f:

+             assert f.read() == "Hello, world!"

+ 

+     def test_get_primary_xml_unsupported_archive(self, caplog, tmpdir):

+         """Assert that unsupported archived db is raise exception."""

+         filepath = os.path.join(tmpdir, "distgit-bugzilla-sync-primary.xml")

+ 

+         archive_name = "db.zip"

+         req = Mock()

+         resp_repomd = Mock()

+         resp_repomd.text = f"""

+         <root xmlns:repo="http://linux.duke.edu/metadata/repo">

+           <repo:data type="primary">

+             <repo:location href="repodata/{archive_name}"/>

+             <repo:open-checksum type="sha1">0</repo:open-checksum>

+           </repo:data>

+         </root>

+         """

+         resp_archive = Mock()

+         resp_archive.content = b"Hello, world!"

+         req.get.side_effect = [resp_repomd, resp_archive]

+         self.ps.requests_session = req

+ 

+         with caplog.at_level(logging.INFO):

+             with pytest.raises(NotImplementedError):

+                 self.ps.get_primary_xml(tmpdir, "https://foo.bar")

+ 

+         assert req.get.mock_calls == [

+             call("https://foo.bar/repomd.xml", verify=True),

+             call(f"https://foo.bar/{archive_name}", verify=True),

+         ]

+ 

+         assert (

+             "Downloading file: https://foo.bar/{0} to {1}/{0}".format(

+                 archive_name, tmpdir

+             )

+             in caplog.text

+         )

+         assert (

+             "Extracting {}/{} to {}".format(tmpdir, archive_name, filepath)

+             in caplog.text

+         )

+ 

+         assert not os.path.exists(filepath)

+ 

+ 

+ class TestGetPackageSummaries:

+     """Test class for `toddlers.utils.package_summaries.PackageSummaries.get_package_summaries`."""

+ 

+     def setup(self):

+         """This will be run before every test case in this class."""

+         self.ps = PackageSummaries()

+ 

+     def test_get_package_summaries(self, tmpdir):

+         """Assert that summuries are correctly returned."""

+         filepath = os.path.join(tmpdir, "distgit-bugzilla-sync-primary.xml")

+         xml_data = """

+         <root>

+           <package type="rpm">

+             <name>foo</name>

+             <summary>bar</summary>

+           </package>

+         </root>

+         """

+         with open(filepath, "w") as f:

+             f.write(xml_data)

+ 

+         config = {"temp_folder": tmpdir, "kojipkgs_url": "https://koji.pkgs.org"}

+         mock_primary_xml = Mock()

+         mock_primary_xml.return_value = filepath

+         self.ps.get_primary_xml = mock_primary_xml

+ 

+         result = self.ps.get_package_summaries(config)

+ 

+         mock_primary_xml.assert_called_with(

+             tmpdir, config["kojipkgs_url"] + "/repos/rawhide/latest/x86_64/repodata"

+         )

+ 

+         assert result == {"foo": "bar"}

file modified
+6
@@ -83,6 +83,12 @@ 

  # Base URL for the Koji build system

  koji_url = "https://koji.fedoraproject.org"

  

+ # Base URL for the Koji package db

If they are only used by the new toddler, let's not put these in the default configuration key (yet)

+ kojipkgs_url = "https://kojipkgs.fedoraproject.org"

+ 

+ # Temp folder to use for toddlers temp files

+ temp_folder = "/var/tmp"

I'm wondering if we want to hard-code this or retrieve it from the tempfile python module. I'm thinking the later may be better. What do you think?

+ 

  # Account to use to connect to Pagure-as-dist-git

  dist_git_url = "https://src.fedoraproject.org"

  dist_git_token_seed = "private random string to change"

@@ -1,5 +1,6 @@ 

  import logging

- from typing import Mapping

+ import time

+ from typing import Any, Callable, Iterable, Mapping, NoReturn, Optional

  import xmlrpc.client

  

  from bugzilla import Bugzilla
@@ -122,3 +123,445 @@ 

          _log.info("   Would add %s to the group %s", user_email, bz_group)

  

      return no_bz_account

+ 

+ 

+ def get_product_info_packages(collection: str) -> Mapping[str, Mapping[str, Any]]:

+     """Get product info for specific collection (Bugzilla product).

+ 

+     :arg collection: Collection in which to look for packages (example: "Fedora")

+ 

+     :return: Dictionary of product info where key is package name.

+     """

+     server = get_bz()

+ 

+     # https://bugzilla.redhat.com/docs/en/html/api/core/v1/product.html#get-product

+     _log.debug("Querying product `%s`", collection)

+     product_info_pkgs = {}

+     raw_data = execute_bugzilla_call(

+         server.product_get,

+         {

+             "names": [collection],

+             "include_fields": [

+                 "components.name",

+                 "components.default_assigned_to",

+                 "components.description",

+                 "components.default_qa_contact",

+                 "components.default_cc",

+                 "components.is_active",

+             ],

+         },

+     )

+     for package in raw_data["components"]:

+         # Change the names of the attributes, so they are the same

+         # as in another component methods

+         package_info = {

+             "initialowner": package["default_assigned_to"],

+             "description": package["description"],

+             "initialqacontact": package["default_qa_contact"],

+             "initialcclist": package["default_cc"],

+             "is_active": package["is_active"],

+         }

+         product_info_pkgs[package["name"]] = package_info

+ 

+     return product_info_pkgs

+ 

+ 

+ def reassign_tickets_to_assignee(

+     new_poc: str,

+     old_poc: str,

+     product: str,

+     package: str,

+     versions: Iterable[str],

+     fas_users_info: Mapping[str, str] = {},

+     dry_run: bool = False,

+     print_fas_names: bool = False,

+ ) -> NoReturn:

+     """Change the tickets assignee for specific package.

+ 

+     :arg new_poc: E-mail of the new point of contact

+     :arg old_poc: E-mail of the previous point of contact

+     :arg product: The product of the package to change in bugzilla.

+         For example: "Fedora"

+     :arg package: Name of the package to change the owner for

+     :arg versions: List of versions of product to update.

+     :arg fas_users_info: Dictionary containing bugzilla e-mails mapped to FAS

+         usernames. Only used if `print_fas_names` is set to True.

+         Default to empty dictionary.

+     :arg dry_run: If True no change in bugzilla will be made.

+         Default to False.

+     :arg print_fas_names: If True FAS names of the `new_poc` and `old_poc` will be printed

+         to log at DEBUG level.

+         Dafault to False.

+ 

+     :raises xmlrpc.client.Fault: Re-raises the exception from bugzilla

+         with additional info.

+     :raises xmlrpc.client.ProtocolError: Re-raises the exception from bugzilla

+         with additional info.

+     """

+     server = get_bz()

+     bz_query = {

+         "product": product,

+         "component": package,

+         "bug_status": [

+             "NEW",

+             "ASSIGNED",

+             "ON_DEV",

+             "ON_QA",

+             "MODIFIED",

+             "POST",

+             "FAILS_QA",

+             "PASSES_QA",

+             "RELEASE_PENDING",

+         ],

+         "version": versions,

+     }

+ 

+     query_results = execute_bugzilla_call(server.query, {"query": bz_query})

+ 

+     for bug in query_results:

+         if bug.assigned_to == old_poc and bug.assigned_to != new_poc:

+             if _log.isEnabledFor(logging.DEBUG):

+                 temp_old_poc = bug.assigned_to

+                 temp_new_poc = new_poc

+                 if print_fas_names:

+                     if temp_old_poc in fas_users_info:

+                         temp_old_poc = fas_users_info[old_poc]

+                     else:

+                         temp_old_poc = old_poc.split("@", 1)[0] + "@..."

+                     if temp_new_poc in fas_users_info:

+                         temp_new_poc = fas_users_info[new_poc]

+                     else:

+                         temp_new_poc = new_poc.split("@", 1)[0] + "@..."

+                 _log.debug(

+                     "%s/%s reassigning bug #%s from %s to %s",

+                     product,

+                     package,

+                     bug.bug_id,

+                     temp_old_poc,

+                     temp_new_poc,

+                 )

+ 

+             if not dry_run:

+                 try:

+                     execute_bugzilla_call(

+                         bug.setassignee,

+                         {

+                             "assigned_to": new_poc,

+                             "comment": "This package has changed maintainer in Fedora. "

+                             "Reassigning to the new maintainer of this component.",

+                         },

+                     )

+                 except xmlrpc.client.Fault as e:

+                     # Output something useful in args

+                     e.args = (new_poc, e.faultCode, e.faultString)

+                     raise

+                 except xmlrpc.client.ProtocolError as e:

+                     e.args = ("ProtocolError", e.errcode, e.errmsg)

+                     raise

+ 

+ 

+ def add_component(

+     product: str,

+     owner: str,

+     package: str,

+     qa_contact: str,

+     cc_list: Iterable[str],

+     fas_users_info: Mapping[str, str] = {},

+     description: str = None,

+     retired: bool = False,

+     print_fas_names: bool = False,

+     dry_run: bool = False,

+ ) -> NoReturn:

+     """Add new component to a product in bugzilla.

+ 

+     :arg product: Product for which the component should be added

+         For example: "Fedora"

+     :arg owner: Bugzilla e-mail of the new owner

+     :arg package: Name of the package that should be added

+     :arg qa_contact: E-mail of QA contact for the component.

+     :arg cc_list: List of the e-mails that should be in CC for the component.

+     :arg fas_users_info: Dictionary containing bugzilla e-mails mapped to FAS

+         usernames. Only used if `print_fas_names` is set to True.

+         Default to empty dictionary.

+     :arg description: Description of the new component.

+         Default to None.

+     :arg retired: Retirement state of the package.

+         Default to False.

+     :arg print_fas_names: If True FAS names of the `new_poc` and `old_poc` will be printed

+         to log at DEBUG level.

+         Dafault to False.

+     :arg dry_run: If True no change in bugzilla will be made.

+         Default to False.

+ 

+     :raises xmlrpc.client.Fault: Re-raises the exception from bugzilla

+         with additional info.

+     """

+     server = get_bz()

+     if retired:

+         _log.debug("[NOADD] %s/%s is retired", product, package)

+         return

+ 

+     data = {

+         "product": product,

+         "component": package,

+         "description": description or "NA",

+         "initialowner": owner,

+         "initialqacontact": qa_contact,

+         "is_active": not retired,

+     }

+     if cc_list:

+         data["initialcclist"] = cc_list

+ 

+     if _log.isEnabledFor(logging.DEBUG):

+         for key in [

+             "initialowner",

+             "description",

+             "initialqacontact",

+             "initialcclist",

+             "is_active",

+         ]:

+             if print_fas_names and key in [

+                 "initialowner",

+                 "initialqacontact",

+                 "initialcclist",

+             ]:

+                 if key == "initialowner":

+                     # Print bugzilla e-mail if FAS name is not found

+                     # This shouldn't happen, but to be safe

+                     value = fas_users_info.get(owner, owner)

+ 

+                 if key == "initialqacontact":

+                     # Print bugzilla e-mail if FAS name is not found

+                     # This shouldn't happen, but to be safe

+                     value = fas_users_info.get(qa_contact, qa_contact)

+ 

+                 if key == "initialcclist":

+                     # Print bugzilla e-mail if FAS name is not found

+                     # This shouldn't happen, but to be safe

+                     value = [

+                         fas_users_info.get(cc_user, cc_user) for cc_user in cc_list

+                     ]

+ 

+                 _log.debug(

+                     "[ADDCOMP] %s/%s %s set to FAS name(s) `%s`",

+                     product,

+                     package,

+                     key,

+                     value,

+                 )

+             else:

+                 _log.debug(

+                     "[ADDCOMP] %s/%s %s set to `%s`", product, package, key, data[key]

+                 )

+ 

+     if not dry_run:

+         try:

+             execute_bugzilla_call(server.addcomponent, {"data": data})

+         except xmlrpc.client.Fault as e:

+             # Output something useful in args

+             e.args = (data, e.faultCode, e.faultString)

+             raise

+ 

+ 

+ def edit_component(

+     owner: str,

+     product: str,

+     package: str,

+     component: dict,

+     cc_list: Iterable[str],

+     versions: Iterable[str],

+     description: str = None,

+     qa_contact: str = None,

+     fas_users_info: Mapping[str, str] = {},

+     retired: bool = False,

+     print_fas_names: bool = False,

+     print_no_change: bool = False,

+     dry_run: bool = False,

+ ) -> NoReturn:

+     """Edit existing bugzilla component.

+ 

+     :arg owner: Bugzilla e-mail of the new owner

+     :arg product: Product for which the component should be added

+         For example: "Fedora"

+     :arg package: Name of the package that should be added

+     :arg component: Dictionary containing the existing component.

+          It's used to compare the new values to previous one.

+          Could be obtained by calling `get_product_info_packages`.

+     :arg cc_list: List of the e-mails that should be in CC for the component.

+     :arg versions: Versions of component where opened bugs should be updated

+          if owner is changed.

+     :arg description: Description of the new component.

+         Default to None.

+     :arg qa_contact: E-mail of QA contact for the component.

+         Default to None.

+     :arg fas_users_info: Dictionary containing bugzilla e-mails mapped to FAS

+         usernames. Only used if `print_fas_names` is set to True.

+         Default to empty dictionary.

+     :arg retired: Retirement state of the package.

+         Default to False.

+     :arg print_fas_names: If True FAS names of the `new_poc` and `old_poc` will be printed

+         to log at DEBUG level.

+         Dafault to False.

+     :arg print_no_change: If True message will be printed to log at DEBUG

+         level when no change is done.

+         Dafault to False.

+     :arg dry_run: If True no change in bugzilla will be made.

+         Default to False.

+ 

+     :raises xmlrpc.client.Fault: Re-raises the exception from bugzilla

+         with additional info.

+     :raises xmlrpc.client.ProtocolError: Re-raises the exception from bugzilla

+         with additional info.

+     """

+     server = get_bz()

+     data = {}

+ 

+     # Check for changes to the owner, qa_contact, or description

+     if component["initialowner"].lower() != owner.lower():

+         data["initialowner"] = owner

+ 

+     if description and component["description"] != description:

+         data["description"] = description

+ 

+     if qa_contact and component["initialqacontact"].lower() != qa_contact.lower():

+         data["initialqacontact"] = qa_contact

+ 

+     if len(component["initialcclist"]) != len(cc_list):

+         data["initialcclist"] = cc_list

+     else:

+         cc_list_lower = [cc_member.lower() for cc_member in cc_list]

+         for cc_member in component["initialcclist"]:

+             if cc_member.lower() not in cc_list_lower:

+                 data["initialcclist"] = cc_list

+                 break

+ 

+     if component["is_active"] != (not retired):

+         data["is_active"] = not retired

+ 

+     if data:

+         # Changes occured. Submit a request to change via xmlrpc

+         data["product"] = product

+         data["component"] = package

+ 

+         if _log.isEnabledFor(logging.DEBUG):

+             for key in [

+                 "initialowner",

+                 "description",

+                 "initialqacontact",

+                 "initialcclist",

+                 "is_active",

+             ]:

+                 if data.get(key) is not None:

+                     old_value = component[key]

+                     if isinstance(old_value, list):

+                         old_value = sorted(old_value)

+                     new_value = data.get(key)

+                     if isinstance(new_value, list):

+                         new_value = sorted(new_value)

+ 

+                 if print_fas_names and key in [

+                     "initialowner",

+                     "initialqacontact",

+                     "initialcclist",

+                 ]:

+                     if key == "initialowner":

+                         # Print bugzilla e-mail if FAS name is not found

+                         # This shouldn't happen, but to be safe

+                         old_value = fas_users_info.get(component[key], component[key])

+                         new_value = fas_users_info.get(owner, owner)

+ 

+                     if key == "initialqacontact":

+                         # Print bugzilla e-mail if FAS name is not found

+                         # This shouldn't happen, but to be safe

+                         old_value = fas_users_info.get(component[key], component[key])

+                         new_value = fas_users_info.get(qa_contact, qa_contact)

+ 

+                     if key == "initialcclist":

+                         # Print bugzilla e-mail if FAS name is not found

+                         # This shouldn't happen, but to be safe

+                         old_value = [

+                             fas_users_info.get(cc_user, cc_user)

+                             for cc_user in component[key]

+                         ]

+                         new_value = [

+                             fas_users_info.get(cc_user, cc_user) for cc_user in cc_list

+                         ]

+ 

+                     _log.debug(

+                         "[EDITCOMP] %s/%s %s changed from `%s` to FAS name(s) `%s`",

+                         product,

+                         package,

+                         key,

+                         old_value,

+                         new_value,

+                     )

+                 else:

+                     if data.get(key) is not None:

+                         _log.debug(

+                             "[EDITCOMP] %s/%s %s changed from `%s` to `%s`",

+                             product,

+                             package,

+                             key,

+                             old_value,

+                             new_value,

+                         )

+         owner_changed = "initialowner" in data

+ 

+         # FIXME: initialowner has been made mandatory for some

+         # reason.  Asking dkl why.

+         data["initialowner"] = owner

+ 

+         if not dry_run:

+             try:

+                 execute_bugzilla_call(server.editcomponent, {"data": data})

+             except xmlrpc.client.Fault as e:

+                 # Output something useful in args

+                 e.args = (data, e.faultCode, e.faultString)

+                 raise

+             except xmlrpc.client.ProtocolError as e:

+                 e.args = ("ProtocolError", e.errcode, e.errmsg)

+                 raise

+ 

+         if owner_changed:

+             reassign_tickets_to_assignee(

+                 new_poc=owner,

+                 old_poc=component["initialowner"],

+                 product=product,

+                 package=package,

+                 versions=versions,

+                 fas_users_info=fas_users_info,

+                 dry_run=dry_run,

+                 print_fas_names=print_fas_names,

+             )

+ 

+     else:

+         if print_no_change:

+             _log.debug("[NOCHANGE] %s/%s", product, package)

+ 

+ 

+ def execute_bugzilla_call(

+     call: Callable, args: dict, num_of_attempts: int = 5

+ ) -> Optional[dict]:

+     """Wrapper function for Bugzilla calls. It repeats the API call for `num_of_attempts.`

+ 

+     :arg call: API method to call.

+     :arg args: Arguments for the API call, it will be unpacked when given to call.

+     :arg num_of_attempts: Number of attempts to make before raising the exception.

+         Default to 5.

+ 

+     :return: Received data.

+     """

+     raw_data = None

+     for i in range(num_of_attempts):

+         try:

+             raw_data = call(**args)

+             break

+         except Exception as e:

+             if i >= num_of_attempts - 1:

+                 raise

+ 

+             _log.debug("ERROR %s", e)

+             _log.debug("Query failed, going to try again in 10 seconds")

+             # Wait 10 seconds and try again

+             time.sleep(10)

+     return raw_data

file modified
+93 -2
@@ -110,7 +110,94 @@ 

      )

  

  

- def send_email(to_addresses, from_address, subject, content, mail_server):

+ def notify_admins_distgit_sync_error(mail_server, admin_email, recipients, errors):

+     """Send e-mail to admins about errors encountered during synchronization of bugzilla

+     and distgit.

+ 

+     Params:

+         mail_server (str): Mail server to sent e-mail from

+         admin_email (str): Admin e-mail to use as sender

+         recipients (list): List containing e-mails of recipients

+         errors (list): List of errors to report

+     """

+     errors = "\n".join(errors)

+     message = f"""Greetings, our mighty admins,

+ 

+ While updating bugzilla with information from the Package Database some errors were encountered.

+ Don't panic!

+ Please, be so kind and see if you can do something with them. Here is the list of the errors:

+ 

+ {errors}

Much better! :)

+ """

+ 

+     send_email(

+         to_addresses=recipients,

+         from_address=admin_email,

+         subject="Errors while syncing bugzilla with the PackageDB",

+         content=message,

+         mail_server=mail_server,

+     )

+ 

+ 

+ def notify_packager_distgit_sync_error(

+     mail_server, admin_email, user_email, cc_address

+ ):

+     """Send e-mail to packager about error encountered during synchronization of bugzilla

+     and distgit.

+ 

+     Params:

+         mail_server (str): Mail server to sent e-mail from

+         admin_email (str): Admin e-mail to use as sender

+         user_email (str): Mail of the receiver

+         cc_address (str): Copy mail address

+     """

+     message = """Greetings.

+ 

+ You are receiving this email because there's a problem with your

+ bugzilla.redhat.com account.

+ 

+ If you recently changed the email address associated with your

+ Fedora account in the Fedora Account System, it is now out of sync

+ with your bugzilla.redhat.com account. This leads to problems

+ with Fedora packages you own or are CC'ed on bug reports for.

+ 

+ Please take one of the following actions:

+ 

+ a) login to your old bugzilla.redhat.com account and change the email

+ address to match your current email in the Fedora account system.

+ https://bugzilla.redhat.com login, click preferences, account

+ information and enter new email address.

+ 

+ b) Create a new account in bugzilla.redhat.com to match your

+ email listed in your Fedora account system account.

+ https://bugzilla.redhat.com/ click 'new account' and enter email

+ address.

+ 

+ c) Change your Fedora Account System email to match your existing

+ bugzilla.redhat.com account.

+ https://admin.fedoraproject.org/accounts login, click on 'my account',

+ then 'edit' and change your email address.

+ 

+ If you have questions or concerns, please let us know.

+ 

+ Your prompt attention in this matter is appreciated.

+ 

+ The Fedora admins.

+ """

+ 

+     send_email(

+         to_addresses=[user_email],

+         from_address=admin_email,

+         subject="Please fix your bugzilla.redhat.com account",

+         content=message,

+         mail_server=mail_server,

+         cc_address=[cc_address],

+     )

+ 

+ 

+ def send_email(

+     to_addresses, from_address, subject, content, mail_server, cc_address=None

+ ):

      """Actually sends the email to the list of addresses specified from the

      address given with the subject and content and via the given email server.

      """
@@ -119,9 +206,13 @@ 

      msg = EmailMessage()

      msg.add_header("To", ", ".join(to_addresses))

      msg.add_header("From", from_address)

+ 

+     if cc_address is not None:

+         msg.add_header("Cc", ",".join(cc_address))

+         to_addresses += cc_address

+ 

      msg.add_header("Subject", subject)

      msg.set_payload(content)

- 

      smtp = smtplib.SMTP(mail_server)

      smtp.sendmail(from_address, to_addresses, msg.as_string())

      smtp.quit()

@@ -0,0 +1,257 @@ 

+ """

+ This module provides the functionality to download the latest primary.xml

+ database from koji on the rawhide repo.

+ Decompress that xml file (which are downloaded compressed).

+ Read its content and build a dictionary with the package names as keys

+ and their summaries as values.

+ 

+ This code can then be used to create an in-memory cache of this information

+ which can then later be re-used in other places.

+ This prevents relying on remote services such as mdapi (of which a lot of

+ code here is coming from) when needing to access the summary of a lot of

+ packages.

+ """

+ import contextlib

+ import hashlib

+ import logging

+ import os

+ import time

+ from typing import Mapping, NoReturn, Optional

+ import xml

+ 

+ from defusedxml import cElementTree as etree

+ 

+ from .requests import make_session

+ 

+ 

+ repomd_xml_namespace = {

+     "repo": "http://linux.duke.edu/metadata/repo",

+     "rpm": "http://linux.duke.edu/metadata/rpm",

+ }

+ 

+ log = logging.getLogger(__name__)

+ 

+ 

+ class PackageSummaries:

+     """

+     Class for retrieving package summaries from koji.

+     """

+ 

+     def __init__(self):

+         """

+         Constructor.

+         """

+         self.requests_session = make_session()

+ 

+     def _download_db(self, repomd_url: str, archive: str) -> NoReturn:

+         """Download db file.

+ 

+         Params:

+             repomd_url (str): Repository metadata URL

+             archive (str): Download destination

+         """

+         log.info("Downloading file: %s to %s", repomd_url, archive)

+         response = self.requests_session.get(repomd_url, verify=True)

+         with open(archive, "wb") as stream:

+             stream.write(response.content)

+ 

+     def _decompress_db(self, archive: str, location: str) -> NoReturn:

+         """Decompress the given archive at the specified location.

+ 

+         Params:

+             archive (str): Path to archive to extract

+             location (str): Destination path

+ 

+         Raises:

+             NotImplementedError - when archive format is not supported

+         """

+         log.info("Extracting %s to %s", archive, location)

+         if archive.endswith(".xz"):

+             import lzma

+ 

+             with contextlib.closing(lzma.LZMAFile(archive)) as stream_xz:

+                 data = stream_xz.read()

+             with open(location, "wb") as stream:

+                 stream.write(data)

+         elif archive.endswith(".tar.gz"):

+             import tarfile

+ 

+             with tarfile.open(archive, "r:gz") as tar:

+                 # Let's assume that there is only one file in archive

+                 tarinfo = tar.next()

+                 tar.extract(tarinfo, path=os.path.dirname(location))

+                 os.rename(

+                     os.path.join(os.path.dirname(location), tarinfo.name), location

+                 )

+         elif archive.endswith(".gz"):

+             import gzip

+ 

+             with open(location, "wb") as out:

+                 with gzip.open(archive, "rb") as inp:

+                     out.write(inp.read())

+         elif archive.endswith(".bz2"):

+             import bz2

+ 

+             with open(location, "wb") as out:

+                 bzar = bz2.BZ2File(archive)

+                 out.write(bzar.read())

+                 bzar.close()

+         else:

+             raise NotImplementedError(archive)

+ 

+     def _needs_update(self, local_file: str, remote_sha: str, sha_type: str) -> bool:

+         """Compare hash of a local and remote file.

+         Return True if our local file needs to be updated.

+ 

+         Params:

+             local_file: Local file

+             remote_sha: Hash of the remote file

+             sha_type: Type of the SHA hashing algorithm

+ 

+         Returns:

+             True if our local file needs to be updated.

+         """

+ 

+         if not os.path.isfile(local_file):

+             # If we have never downloaded this before, then obviously it has

+             # "changed"

+             return True

+ 

+         # Old epel5 doesn't even know which sha it is using...

+         if sha_type == "sha":

+             sha_type = "sha1"

+ 

+         hashobj = getattr(hashlib, sha_type)()

+         with open(local_file, "rb") as f:

+             hashobj.update(f.read())

+ 

+         local_sha = hashobj.hexdigest()

+         if local_sha != remote_sha:

+             return True

+ 

+         return False

+ 

+     def get_primary_xml(self, destfolder: str, url: str) -> Optional[str]:

+         """Retrieve the repo metadata at the given url and store them in destination

+         folder.

+ 

+         Params:

+             destfolder: Path to destination folder

+             url: URL to download the metadata from

+ 

+         Returns:

+             None or filepath to repository.xml

+         """

+         repomd_url = url + "/repomd.xml"

+         response = self.requests_session.get(repomd_url, verify=True)

+         if not bool(response):

+             log.warning("Failed to get %s %s", repomd_url, response)

+             return

+ 

+         try:

+             root = etree.fromstring(response.text)

+         except xml.etree.ElementTree.ParseError:

+             log.warning("Failed to parse %s %s", repomd_url, response)

+             return

+ 

+         data_nodes = list(

+             root.findall('repo:data[@type="primary"]', repomd_xml_namespace)

+         )

+         if not data_nodes:

+             log.debug("No primary.xml could be found in %s", url)

+             return

+         if len(data_nodes) > 1:

+             log.debug("More than one primary.xml could be found in %s", url)

+             return

+ 

+         primary_node = data_nodes[0]

+ 

+         location_node = primary_node.find("repo:location", repomd_xml_namespace)

+         if location_node is None or "href" not in location_node.attrib:

+             log.debug("No valid location found for primary.xml in %s", url)

+             return

+ 

+         cksuminfo_node = primary_node.find("repo:open-checksum", repomd_xml_namespace)

+         if cksuminfo_node is None or "type" not in cksuminfo_node.attrib:

+             log.debug("No valid checksum information found for primary.xml in %s", url)

+             return

+ 

+         filename = location_node.attrib["href"].replace("repodata/", "")

+         hash_digest = cksuminfo_node.text

+         hash_type = cksuminfo_node.attrib["type"]

+ 

+         repomd_url = url + "/" + filename

+ 

+         # First, determine if the file has changed by comparing hash

+         db = "distgit-bugzilla-sync-primary.xml"

+ 

+         # Have we downloaded this before?  Did it change?

+         destfile = os.path.join(destfolder, db)

+         if not self._needs_update(destfile, hash_digest, hash_type):

+             log.debug("No change of %s", repomd_url)

+         else:

+             # If it has changed, then download it and move it into place.

+             archive = os.path.join(destfolder, filename)

+ 

+             self._download_db(repomd_url, archive)

+             self._decompress_db(archive, destfile)

+ 

+         return destfile

+ 

+     def get_package_summaries(

+         self, config: Mapping[str, str]

+     ) -> Optional[Mapping[str, str]]:

+         """Get package summaries from XML package db file.

+ 

+         Params:

+           config: Configuration dictionary

+ 

+         Returns:

+           None or dictionary containing package summaries with name of the package as key.

+         """

+         summaries = {}

+ 

+         start = time.time()

+ 

+         primary_xml = self.get_primary_xml(

+             config["temp_folder"],

+             config["kojipkgs_url"] + "/repos/rawhide/latest/x86_64/repodata",

+         )

+ 

+         context = etree.iterparse(primary_xml, events=("start", "end"))

+ 

+         root = None

+ 

+         # iterate over the rest of the primary.xml tree

+         for event, elem in context:

+             if not root:

+                 root = elem

+                 continue

+ 

+             if (

+                 event == "end"

+                 and elem.tag == "package"

+                 and elem.get("type", "rpm") == "rpm"

+             ):

+                 name = elem.findtext("name")

+                 summary = elem.findtext("summary")

+                 if name is not None and summary is not None:

+                     summaries[name] = summary

+                 # remove package child from root element to keep memory consumption low

+                 root.clear()

+ 

+         delta = time.time() - start

+         log.info("Parsed in %s seconds -- ie: %s minutes", delta, delta / 60)

+ 

+         return summaries

+ 

+ 

+ if __name__ == "__main__":  # pragma: no cover

+     logging.basicConfig(level=logging.DEBUG)

+     ps = PackageSummaries()

+     dummy_config = {

+         "kojipkgs_url": "https://kojipkgs.fedoraproject.org",

+         "temp_folder": "/var/tmp",

+     }

+     result = ps.get_package_summaries(dummy_config)

+     print(result)

This PR adds various help functions for distgit bugzilla sync toddler
for Bugzilla, package metadata handling and mail notifications.

The toddler itself will be added later, but to make the PRs smaller I
decided to push the help functions first.

Signed-off-by: Michal Konečný mkonecny@redhat.com

Build succeeded.

  • tox : SUCCESS in 6m 05s

If this doesn't change, should we move it elsewhere to repeat it less?

within brackets you don't need the + to concatenate strings. I'm surprise black or flake8 don't mention this

within brackets you don't need the + to concatenate strings. I'm surprise black or flake8 don't mention this

I see three options:
1) create one test class for each function and add this as attribute to class for set_package_owner tests
2) Move it somewhere on the top of the file outside the class
3) Add it as attribute to the class

Which one do you prefer, @pingou?

I probably tried it without the +, but I'm not sure if I already had brackets in place or not. Will fix

If they are only used by the new toddler, let's not put these in the default configuration key (yet)

1 new commit added

  • Remove the + sign inside brackets
3 years ago

I'm wondering if we want to hard-code this or retrieve it from the tempfile python module. I'm thinking the later may be better. What do you think?

This doesn't set the default assignee of the component, it updates/re-assign existing tickets of that component. Maybe reassign_ticket_to_assignee would be more self-explanatory?

Maybe: to a product in bugzilla?

Build succeeded.

  • tox : SUCCESS in 7m 04s

That method sends an email to Fedora's admin, not to the packager :)

owh :( This is a little dry no?
You're sending this email to me, Kevin, smooge, Mark... let's make it a little bit nicer ;-)

IIRC the current email specify which email address is set in FAS, does it not?

I see three options:
1) create one test class for each function and add this as attribute to class for set_package_owner tests
2) Move it somewhere on the top of the file outside the class
3) Add it as attribute to the class

Which one do you prefer, @pingou?

No strong preferences, 2 or 3 seem simpler than 1, but you're the one doing the work :)

missing something?
same as above?

Nothing is missing here, it's just checking if this was called without any parameter.

If they are only used by the new toddler, let's not put these in the default configuration key (yet)

These are used by package_summaries module, which is already part of the PR.

I'm wondering if we want to hard-code this or retrieve it from the tempfile python module. I'm thinking the later may be better. What do you think?

I remember in the-new-hotness that I needed to hardcode it, because the default tempfile folder couldn't be used in OpenShift. This is why I made it configurable.

reassign_ticket_to_assignee sounds better to me.

Your wording sounds better. I will use it.

That method sends an email to Fedora's admin, not to the packager :)

Probably copied the text from other method and didn't noticed this. Will fix.

Hm, I copied what was in distgit_bugzilla_sync. How do you want to make it nicer?

You are right, the distgit_bugzilla_sync is getting the e-mail associated with FAS account and tries to login to bugzilla.

I see three options:
1) create one test class for each function and add this as attribute to class for set_package_owner tests
2) Move it somewhere on the top of the file outside the class
3) Add it as attribute to the class

Which one do you prefer, @pingou?

No strong preferences, 2 or 3 seem simpler than 1, but you're the one doing the work :)

I will go with option 1 then, I like it much better.

1 new commit added

  • Create fixtures from reusable data
3 years ago

Build succeeded.

  • tox : SUCCESS in 7m 00s

1 new commit added

  • Enhance notify module
3 years ago

Build succeeded.

  • tox : SUCCESS in 6m 09s

1 new commit added

  • Update docsctring for add_component
3 years ago

Build succeeded.

  • tox : SUCCESS in 6m 27s

@pingou Our comments should be addressed now :-)

Let's squash and then I think we'll be able to merge :)

rebased onto a20cefd

3 years ago

Build succeeded.

  • tox : SUCCESS in 6m 12s

Pull-Request has been merged by zlopez

3 years ago