diff --git a/elliottlib/cli/find_bugs_kernel_cli.py b/elliottlib/cli/find_bugs_kernel_cli.py index 4d26ce30..e34931f7 100644 --- a/elliottlib/cli/find_bugs_kernel_cli.py +++ b/elliottlib/cli/find_bugs_kernel_cli.py @@ -25,13 +25,13 @@ def _search_issues(jira_client, *args, **kwargs): class FindBugsKernelCli: def __init__(self, runtime: Runtime, trackers: Sequence[str], - clone: bool, reconcile: bool, comment: bool, dry_run: bool): + clone: bool, reconcile: bool, update_tracker: bool, dry_run: bool): self._runtime = runtime self._logger = runtime.logger self.trackers = list(trackers) self.clone = clone self.reconcile = reconcile - self.comment = comment + self.update_tracker = update_tracker self.dry_run = dry_run self._id_bugs: Dict[int, Bug] = {} # cache for kernel bug; key is bug_id, value is Bug object self._tracker_map: Dict[int, Issue] = {} # bug_id -> KMAINT jira mapping @@ -57,17 +57,17 @@ async def run(self): # Getting KMAINT trackers trackers_keys = self.trackers trackers: List[Issue] = [] - if not trackers_keys: - logger.info("Searching for open trackers...") - trackers = self._find_kmaint_trackers(jira_client, config.tracker_jira.project, config.tracker_jira.labels) - trackers_keys = [t.key for t in trackers] - logger.info("Found %s tracker(s): %s", len(trackers_keys), trackers_keys) - else: + if trackers_keys: logger.info("Find kernel bugs linked from KMAINT tracker(s): %s", trackers_keys) for key in trackers_keys: logger.info("Getting tracker JIRA %s...", key) tracker = jira_client.issue(key) trackers.append(tracker) + else: + logger.info("Searching for open trackers...") + trackers = self._find_kmaint_trackers(jira_client, config.tracker_jira.project, config.tracker_jira.labels) + trackers_keys = [t.key for t in trackers] + logger.info("Found %s tracker(s): %s", len(trackers_keys), trackers_keys) # Get kernel bugs linked from KMAINT trackers report: Dict[str, Any] = {"kernel_bugs": []} @@ -86,9 +86,8 @@ async def run(self): "summary": bug.summary, "tracker": tracker, }) - if self.comment: - logger.info("Checking if making a comment on tracker %s is needed...", tracker.key) - self._comment_on_tracker(jira_client, tracker, koji_api, config.target_jira) + if self.update_tracker: + self._update_tracker(jira_client, tracker, koji_api, config.target_jira) if self.clone and self._id_bugs: # Clone kernel bugs into OCP Jira @@ -175,7 +174,7 @@ def _clone_bugs(self, jira_client: JIRA, bugs: Sequence[Bug], conf: KernelBugSwe jira_client.create_issue_link("Blocks", issue.key, kmaint_tracker) result[bug_id] = [issue] else: - logger.warning("[DRY RUN] Would have created Jira for bug %s", bug_id) + logger.info("[DRY RUN] Would have created Jira for bug %s", bug_id) else: # this bug is already cloned into OCP Jira logger.info("Bug %s is already cloned into OCP: %s", bug_id, [issue.key for issue in found_issues]) result[bug_id] = found_issues @@ -190,7 +189,7 @@ def _clone_bugs(self, jira_client: JIRA, bugs: Sequence[Bug], conf: KernelBugSwe if not self.dry_run: issue.update(fields) else: - logger.warning("[DRY RUN] Would have updated Jira %s to match bug %s", issue.key, bug_id) + logger.info("[DRY RUN] Would have updated Jira %s to match bug %s", issue.key, bug_id) return result @@ -204,30 +203,24 @@ def _print_report(report: Dict, out: TextIO): text = f"{bug['tracker']}\t{bug['id']}\t{'N/A' if not cloned_issues else ','.join(cloned_issues)}\t{bug['status']}\t{bug['summary']}" print_func(text, file=out) - def _comment_on_tracker(self, jira_client: JIRA, tracker: Issue, koji_api: koji.ClientSession, - conf: KernelBugSweepConfig.TargetJiraConfig): + def _update_tracker(self, jira_client: JIRA, tracker: Issue, koji_api: koji.ClientSession, + conf: KernelBugSweepConfig.TargetJiraConfig): logger = self._runtime.logger + logger.info("Checking if an update to tracker %s is needed...", tracker.key) # Determine which NVRs have the fix. e.g. ["kernel-5.14.0-284.14.1.el9_2"] nvrs, candidate, shipped = early_kernel.get_tracker_builds_and_tags(logger, tracker, koji_api, conf) - tracker_message = None if shipped: - tracker_message = f"Build(s) {nvrs} was/were already shipped and tagged into {shipped}." + early_kernel.process_shipped_tracker(logger, self.dry_run, jira_client, tracker, nvrs, shipped) elif candidate: - tracker_message = f"Build(s) {nvrs} was/were already tagged into {candidate}." - if not tracker_message: - logger.info("No need to make a comment on %s", tracker.key) - return - comments = jira_client.comments(tracker.key) - if any(map(lambda comment: comment.body == tracker_message, comments)): - logger.info("A comment was already made on %s", tracker.key) - return - logger.info("Making a comment on tracker %s", tracker.key) - if not self.dry_run: - jira_client.add_comment(tracker.key, tracker_message) - logger.info("Left a comment on tracker %s", tracker.key) + early_kernel.comment_on_tracker( + logger, self.dry_run, jira_client, tracker, + [f"Build(s) {nvrs} was/were already tagged into {candidate}."] + # do not reword, see NOTE in method + ) else: - logger.warning("[DRY RUN] Would have left a comment on tracker %s", tracker.key) + logger.info("No need to update tracker %s", tracker.key) + return @staticmethod def _new_jira_fields_from_bug(bug: Bug, ocp_target_version: str, kmaint_tracker: Optional[str], conf: KernelBugSweepConfig.TargetJiraConfig): @@ -294,10 +287,10 @@ def _new_jira_fields_from_bug(bug: Bug, ocp_target_version: str, kmaint_tracker: is_flag=True, default=False, help="Update summary, description, etc for already cloned Jira bugs. Must be used with --clone") -@click.option("--comment", +@click.option("--update-tracker", is_flag=True, default=False, - help="Make comments on KMAINT trackers") + help="Update KMAINT trackers state, links, and comments") @click.option("--dry-run", is_flag=True, default=False, @@ -306,7 +299,7 @@ def _new_jira_fields_from_bug(bug: Bug, ocp_target_version: str, kmaint_tracker: @click_coroutine async def find_bugs_kernel_cli( runtime: Runtime, trackers: Tuple[str, ...], clone: bool, - reconcile: bool, comment: bool, dry_run: bool): + reconcile: bool, update_tracker: bool, dry_run: bool): """Find kernel bugs in Bugzilla for weekly kernel release through OCP. Example 1: Find kernel bugs and print them out @@ -327,7 +320,7 @@ async def find_bugs_kernel_cli( trackers=trackers, clone=clone, reconcile=reconcile, - comment=comment, + update_tracker=update_tracker, dry_run=dry_run ) await cli.run() diff --git a/elliottlib/cli/find_bugs_kernel_clones_cli.py b/elliottlib/cli/find_bugs_kernel_clones_cli.py index 96a88d6e..153ecb33 100644 --- a/elliottlib/cli/find_bugs_kernel_clones_cli.py +++ b/elliottlib/cli/find_bugs_kernel_clones_cli.py @@ -25,19 +25,19 @@ class FindBugsKernelClonesCli: def __init__(self, runtime: Runtime, trackers: Sequence[str], bugs: Sequence[str], - move: bool, comment: bool, dry_run: bool): + move: bool, update_tracker: bool, dry_run: bool): self._runtime = runtime self._logger = runtime.logger self.trackers = list(trackers) self.bugs = list(bugs) self.move = move - self.comment = comment + self.update_tracker = update_tracker self.dry_run = dry_run def run(self): logger = self._logger - if self.comment and not self.move: - raise ElliottFatalError("--comment must be used with --move") + if self.update_tracker and not self.move: + raise ElliottFatalError("--update-tracker must be used with --move") if self._runtime.assembly_type is not AssemblyTypes.STREAM: raise ElliottFatalError("This command only supports stream assembly.") group_config = self._runtime.group_config @@ -140,7 +140,7 @@ def _find_trackers_for_bugs(self, tracker_bugs.setdefault(tracker.key, []).append(bug) return trackers, tracker_bugs - def _process_shipped_bugs(self, logger, bug_keys, bugs, jira_client: JIRA, nvrs, prod_brew_tag) -> str: + def _process_shipped_bugs(self, logger, bug_keys, bugs, jira_client: JIRA, nvrs, prod_brew_tag): # when NVRs are shipped, ensure the associated bugs are closed with a comment logger.info("Build(s) %s shipped (tagged into %s). Moving bug Jira(s) %s to CLOSED...", nvrs, prod_brew_tag, bug_keys) for bug in bugs: @@ -148,27 +148,11 @@ def _process_shipped_bugs(self, logger, bug_keys, bugs, jira_client: JIRA, nvrs, if current_status.lower() != "closed": new_status = 'CLOSED' message = f"Elliott changed bug status from {current_status} to {new_status} because {nvrs} was/were already shipped and tagged into {prod_brew_tag}." - self._move_jira(jira_client, bug, new_status, message) + early_kernel.move_jira(logger, self.dry_run, jira_client, bug, new_status, message) else: logger.info("No need to move %s because its status is %s", bug.key, current_status) - return f"Build(s) {nvrs} was/were already shipped and tagged into {prod_brew_tag}." # see wording NOTE - def _process_shipped_tracker(self, logger, tracker, jira_client: JIRA, nvrs) -> List[str]: - # when NVRs are shipped, ensure the associated tracker is closed with a comment - # and a link to any advisory that shipped them - logger.info("Build(s) %s shipped (tagged into %s). Looking for advisories...", nvrs) - tracker_messages = [] - - logger.info("Moving tracker Jira %s to CLOSED...") - current_status: str = tracker.fields.status.name - if current_status.lower() != "closed": - self._move_jira(jira_client, tracker, "CLOSED") - else: - logger.info("No need to move %s because its status is %s", tracker.key, current_status) - - return tracker_messages - - def _process_candidate_bugs(self, logger, bug_keys, bugs, jira_client: JIRA, nvrs, candidate_brew_tag) -> str: + def _process_candidate_bugs(self, logger, bug_keys, bugs, jira_client: JIRA, nvrs, candidate_brew_tag): # when NVRs are tagged, ensure the associated bugs are modified with a comment logger.info("Build(s) %s tagged into %s. Moving Jira(s) %s to MODIFIED...", nvrs, candidate_brew_tag, bug_keys) for bug in bugs: @@ -176,10 +160,9 @@ def _process_candidate_bugs(self, logger, bug_keys, bugs, jira_client: JIRA, nvr if current_status.lower() in {"new", "assigned", "post"}: new_status = 'MODIFIED' message = f"Elliott changed bug status from {current_status} to {new_status} because {nvrs} was/were already tagged into {candidate_brew_tag}." - self._move_jira(jira_client, bug, new_status, message) + early_kernel.move_jira(logger, self.dry_run, jira_client, bug, new_status, message) else: logger.info("No need to move %s because its status is %s", bug.key, current_status) - return f"Build(s) {nvrs} was/were already tagged into {candidate_brew_tag}." # see wording NOTE def _update_jira_bugs(self, jira_client: JIRA, found_bugs: List[Issue], koji_api: koji.ClientSession, config: KernelBugSweepConfig): logger = self._runtime.logger @@ -189,42 +172,18 @@ def _update_jira_bugs(self, jira_client: JIRA, found_bugs: List[Issue], koji_api nvrs, candidate, shipped = early_kernel.get_tracker_builds_and_tags(logger, tracker, koji_api, config.target_jira) bugs = tracker_bugs[tracker_id] bug_keys = [bug.key for bug in bugs] - tracker_message = [] if shipped: - tracker_message.append(self._process_shipped_bugs(logger, bug_keys, bugs, jira_client, nvrs, shipped)) - tracker_message.extend(self._process_shipped_tracker(logger, tracker, jira_client, nvrs)) + self._process_shipped_bugs(logger, bug_keys, bugs, jira_client, nvrs, shipped) + if self.update_tracker: + early_kernel.process_shipped_tracker(logger, self.dry_run, jira_client, tracker, nvrs, shipped) elif candidate: - tracker_message.append(self._process_candidate_bugs(logger, bug_keys, bugs, jira_client, nvrs, candidate)) - - if self.comment and tracker_message: - logger.info("Checking if making a comment on tracker %s is needed", tracker.key) - # wording NOTE: this logic will re-comment on past bugs if the wording is not - # exactly as previously commented. think long and hard before changing the wording - # of any of these comments. - comments = jira_client.comments(tracker.key) - for message in tracker_message: - if any(map(lambda comment: comment.body == message, comments)): - logger.info("A comment was already made on %s", tracker.key) - continue - logger.info("Making a comment on tracker %s", tracker.key) - if self.dry_run: - logger.warning("[DRY RUN] Would have left a comment on tracker %s", tracker.key) - else: - jira_client.add_comment(tracker.key, message) - logger.info("Left a comment on tracker %s", tracker.key) - - def _move_jira(self, jira_client: JIRA, issue: Issue, new_status: str, comment: Optional[str]): - logger = self._runtime.logger - current_status: str = issue.fields.status.name - logger.info("Moving %s from %s to %s", issue.key, current_status, new_status) - if self.dry_run: - logger.warning("[DRY RUN] Would have moved Jira %s from %s to %s", issue.key, current_status, new_status) - else: - jira_client.assign_issue(issue.key, jira_client.current_user()) - jira_client.transition_issue(issue.key, new_status) - if comment: - jira_client.add_comment(issue.key, comment) - logger.info("Moved %s from %s to %s", issue.key, current_status, new_status) + self._process_candidate_bugs(logger, bug_keys, bugs, jira_client, nvrs, candidate) + if self.update_tracker: + early_kernel.comment_on_tracker( + logger, self.dry_run, jira_client, tracker, + [f"Build(s) {nvrs} was/were already tagged into {candidate}."] + # do not reword, see NOTE in method + ) @staticmethod def _print_report(report: Dict, out: TextIO): @@ -244,10 +203,10 @@ def _print_report(report: Dict, out: TextIO): is_flag=True, default=False, help="Auto move Jira bugs to MODIFIED or CLOSED") -@click.option("--comment", +@click.option("--update-tracker", is_flag=True, default=False, - help="Make comments on KMAINT trackers") + help="Update KMAINT trackers state, links, and comments") @click.option("--dry-run", is_flag=True, default=False, @@ -255,7 +214,7 @@ def _print_report(report: Dict, out: TextIO): @click.pass_obj def find_bugs_kernel_clones_cli( runtime: Runtime, trackers: Tuple[str, ...], issues: Tuple[str, ...], - move: bool, comment: bool, dry_run: bool): + move: bool, update_tracker: bool, dry_run: bool): """Find cloned kernel bugs in JIRA for weekly kernel release through OCP. Example 1: List all bugs in JIRA @@ -266,9 +225,9 @@ def find_bugs_kernel_clones_cli( \b $ elliott -g openshift-4.14 find-bugs:kernel-clones --move - Example 3: Move bugs and leave a comment on the KMAINT tracker + Example 3: Move bugs and update the KMAINT tracker \b - $ elliott -g openshift-4.14 find-bugs:kernel-clones --move --comment + $ elliott -g openshift-4.14 find-bugs:kernel-clones --move --update-tracker """ runtime.initialize(mode="none") cli = FindBugsKernelClonesCli( @@ -276,7 +235,7 @@ def find_bugs_kernel_clones_cli( trackers=trackers, bugs=issues, move=move, - comment=comment, + update_tracker=update_tracker, dry_run=dry_run ) cli.run() diff --git a/elliottlib/early_kernel.py b/elliottlib/early_kernel.py index 521e9ce3..cb700c1b 100644 --- a/elliottlib/early_kernel.py +++ b/elliottlib/early_kernel.py @@ -1,7 +1,9 @@ from typing import Dict, List, Optional, Sequence, TextIO, Tuple, cast import re import koji -from jira import Issue +from errata_tool.build import Build +from errata_tool import Erratum, ErrataException +from jira import Issue, JIRA from elliottlib.config_model import KernelBugSweepConfig from elliottlib import brew @@ -27,3 +29,105 @@ def get_tracker_builds_and_tags( candidate = all(candidate_brew_tag in tags for tags in build_tags) return nvrs, candidate_brew_tag if candidate else None, prod_brew_tag if shipped else None + + +def _advisories_for_builds(nvrs: List[str]) -> List[Erratum]: + advisories = {} + for nvr in nvrs: + try: + build = Build(nvr) + except ErrataException: + continue # probably build not yet added to an advisory + for errata_id in build.all_errata_ids: + if errata_id in advisories: + continue # already loaded + # TODO: optimize with errata.get_raw_erratum + advisory = Erratum(errata_id=errata_id) + if advisory.errata_state == "SHIPPED_LIVE": + advisories[errata_id] = advisory + return list(advisories.values()) + + +def _link_tracker_advisories( + logger, dry_run: bool, jira_client: JIRA, + advisories: List[Erratum], nvrs: List[str], tracker: Issue, +) -> List[str]: + tracker_messages = [] + links = set(link.raw['object']['url'] for link in jira_client.remote_links(tracker)) # check if we already linked advisories + for advisory in advisories: + if advisory.url() in links: + logger.info(f"Tracker {tracker.id} already links {advisory.url()} ({advisory.synopsis})") + continue + tracker_messages.append(f"Build(s) {nvrs} shipped in advisory {advisory.url()} with title:\n{advisory.synopsis}") + if dry_run: + logger.info(f"[DRY RUN] Tracker {tracker.id} would have added link {advisory.url()} ({advisory.errata_name}: {advisory.synopsis})") + else: + jira_client.add_simple_link( + tracker, dict( + url=advisory.url(), + title=f"{advisory.errata_name}: {advisory.synopsis}")) + return tracker_messages + + +def process_shipped_tracker( + logger, dry_run: bool, + jira_client: JIRA, tracker: Issue, + nvrs: List[str], shipped_tag: str, +) -> List[str]: + # when NVRs are shipped, ensure the associated tracker is closed with a comment + # and a link to any advisory that shipped them + logger.info("Build(s) %s shipped (tagged into %s). Looking for advisories...", nvrs, shipped_tag) + advisories = _advisories_for_builds(nvrs) + if not advisories: + raise RuntimeError(f"NVRs {nvrs} tagged into {shipped_tag} but not found in any shipped advisories!") + tracker_messages = _link_tracker_advisories(logger, dry_run, jira_client, advisories, nvrs, tracker) + + logger.info("Moving tracker Jira %s to CLOSED...", tracker) + current_status: str = tracker.fields.status.name + if current_status.lower() != "closed": + if tracker_messages: + comment_on_tracker(logger, dry_run, jira_client, tracker, tracker_messages) + else: + logger.warning("Closing Jira %s without adding any messages; prematurely closed?", tracker) + move_jira(logger, dry_run, jira_client, tracker, "CLOSED") + else: + logger.info("No need to move %s because its status is %s", tracker.key, current_status) + + +def move_jira( + logger, dry_run: bool, + jira_client: JIRA, issue: Issue, + new_status: str, comment: str = None, +): + current_status: str = issue.fields.status.name + if dry_run: + logger.info("[DRY RUN] Would have moved Jira %s from %s to %s", issue.key, current_status, new_status) + else: + logger.info("Moving %s from %s to %s", issue.key, current_status, new_status) + jira_client.assign_issue(issue.key, jira_client.current_user()) + jira_client.transition_issue(issue.key, new_status) + if comment: + jira_client.add_comment(issue.key, comment) + logger.info("Moved %s from %s to %s", issue.key, current_status, new_status) + + +def comment_on_tracker( + logger, dry_run: bool, + jira_client: JIRA, tracker: Issue, + comments: List[str], +): + # wording NOTE: commenting is intended to avoid duplicates, but this logic may re-comment on old + # trackers if the wording changes after previously commented. think long and hard before + # changing the wording of any tracker comments. + logger.info("Checking if making a comment on tracker %s is needed", tracker.key) + previous_comments = jira_client.comments(tracker.key) + for comment in comments: + if any(previous.body == comment for previous in previous_comments): + logger.info("Intended comment was already made on %s", tracker.key) + continue + logger.info("Making a comment on tracker %s", tracker.key) + if dry_run: + logger.info("[DRY RUN] Would have left a comment on tracker %s", tracker.key) + else: + jira_client.add_comment(tracker.key, comment) + logger.info("Left a comment on tracker %s", tracker.key) diff --git a/tests/test_early_kernel.py b/tests/test_early_kernel.py new file mode 100644 index 00000000..c3e63240 --- /dev/null +++ b/tests/test_early_kernel.py @@ -0,0 +1,137 @@ +from io import StringIO +from unittest import TestCase +from unittest.mock import ANY, MagicMock, Mock, patch + +import koji +from jira import JIRA, Issue +from errata_tool import Erratum +from errata_tool.build import Build + +from elliottlib.config_model import KernelBugSweepConfig +from elliottlib import early_kernel + + +class TestEarlyKernel(TestCase): + @patch("elliottlib.brew.get_builds_tags") + def test_get_tracker_builds_and_tags(self, get_builds_tags: Mock): + logger = MagicMock() + conf = KernelBugSweepConfig.TargetJiraConfig( + project="TARGET-PROJECT", + component="Target Component", + version="4.14", target_release="4.14.z", + candidate_brew_tag="fake-candidate", prod_brew_tag="fake-prod") + tracker = MagicMock(spec=Issue, key="TRACKER-1", fields=MagicMock( + summary="kernel-1.0.1-1.fake and kernel-rt-1.0.1-1.fake early delivery via OCP", + description="Fixes bugzilla.redhat.com/show_bug.cgi?id=5 and bz6.", + )) + koji_api = MagicMock(spec=koji.ClientSession) + get_builds_tags.return_value = [ + [{"name": "irrelevant-1"}, {"name": "fake-candidate"}], + [{"name": "irrelevant-2"}, {"name": "fake-candidate"}], + ] + + nvrs, candidate, shipped = early_kernel.get_tracker_builds_and_tags(logger, tracker, koji_api, conf) + self.assertEqual(["kernel-1.0.1-1.fake", "kernel-rt-1.0.1-1.fake"], nvrs) + self.assertEqual("fake-candidate", candidate) + self.assertFalse(shipped) + + @patch("elliottlib.early_kernel._advisories_for_builds") + @patch("elliottlib.early_kernel._link_tracker_advisories") + @patch("elliottlib.early_kernel.comment_on_tracker") + @patch("elliottlib.early_kernel.move_jira") + def test_process_shipped_tracker(self, move_jira: Mock, comment_on_tracker: Mock, + _link_tracker_advisories: Mock, _advisories_for_builds: Mock): + logger = MagicMock() + jira_client = MagicMock(spec=JIRA) + tracker = MagicMock(spec=Issue, key="TRACKER-1", fields=MagicMock( + summary="kernel-1.0.1-1.fake and kernel-rt-1.0.1-1.fake early delivery via OCP", + description="Fixes bugzilla.redhat.com/show_bug.cgi?id=5 and bz6.", + status=Mock(), # need to set "name" but can't in a mock - set later + )) + nvrs = ["kernel-1.0.1-1.fake", "kernel-rt-1.0.1-1.fake"] + advisory = MagicMock(spec=Erratum) + _advisories_for_builds.return_value = [advisory] + _link_tracker_advisories.return_value = ["comment"] + + setattr(tracker.fields.status, "name", "CLOSED") + early_kernel.process_shipped_tracker(logger, False, jira_client, tracker, nvrs, "tag") + comment_on_tracker.assert_not_called() + move_jira.assert_not_called() + + setattr(tracker.fields.status, "name", "New") + early_kernel.process_shipped_tracker(logger, False, jira_client, tracker, nvrs, "tag") + comment_on_tracker.assert_called_once_with(logger, False, jira_client, tracker, ["comment"]) + move_jira.assert_called_once_with(logger, False, jira_client, tracker, "CLOSED") + + @patch("elliottlib.early_kernel.Erratum") + @patch("elliottlib.early_kernel.Build") + def test_advisories_for_builds(self, build_clz: Mock, erratum_clz: Mock): + build_clz.return_value = MagicMock(spec=Build, all_errata_ids=[42]) + advisory = erratum_clz.return_value = MagicMock(spec=Erratum, errata_state="SHIPPED_LIVE") + + self.assertEqual([advisory], early_kernel._advisories_for_builds(nvrs=["nvr-1", "nvr-2"])) + erratum_clz.assert_called_once_with(errata_id=42) + + advisory.errata_state = "QE" + self.assertEqual([], early_kernel._advisories_for_builds(nvrs=["nvr-1", "nvr-2"])) + + def test_link_tracker_advisories(self): + tracker = MagicMock(spec=Issue, id=42) + advisory = MagicMock(spec=Erratum, errata_name="RHBA-42", synopsis="shipped some stuff") + jira_client = MagicMock(spec=JIRA) + jira_client.remote_links.return_value = [ + MagicMock(raw=dict(object=dict(url="http://example.com"))), + ] + + # test adding an existing link does not happen + advisory.url.return_value = "http://example.com" + msgs = early_kernel._link_tracker_advisories( + MagicMock(), False, jira_client, [advisory], ["nvrs"], tracker + ) + jira_client.add_simple_link.assert_not_called() + self.assertEqual([], msgs) + + # test adding a new link does happen + advisory.url.return_value = "http://different.example.com" + msgs = early_kernel._link_tracker_advisories( + MagicMock(), False, jira_client, [advisory], ["nvrs"], tracker + ) + jira_client.add_simple_link.assert_called_once_with(tracker, ANY) + self.assertEqual(1, len(msgs)) + + def test_move_jira(self): + runtime = MagicMock() + jira_client = MagicMock(spec=JIRA) + comment = "Test message" + issue = MagicMock(spec=Issue, **{ + "key": "FOO-1", "fields": MagicMock(), + "fields.labels": ["art:bz#1", "art:kmaint:KMAINT-1"], + "fields.status.name": "New", + }) + jira_client.current_user.return_value = "fake-user" + early_kernel.move_jira(runtime.logger(), False, jira_client, issue, "MODIFIED", comment) + jira_client.assign_issue.assert_called_once_with("FOO-1", "fake-user") + jira_client.transition_issue.assert_called_once_with("FOO-1", "MODIFIED") + + def test_comment_on_tracker(self): + logger = MagicMock() + jira_client = MagicMock(spec=JIRA) + tracker = MagicMock(spec=Issue, key="TRACKER-1", fields=MagicMock( + summary="kernel-1.0.1-1.fake and kernel-rt-1.0.1-1.fake early delivery via OCP", + description="Fixes bugzilla.redhat.com/show_bug.cgi?id=5 and bz6.", + )) + comment1, comment2 = "Comment 1", "Comment 2" + + # Test 1: making a comment + jira_client.comments.return_value = [MagicMock(body=comment1)] + early_kernel.comment_on_tracker(logger, False, jira_client, tracker, [comment2]) + jira_client.add_comment.assert_called_once_with("TRACKER-1", comment2) + + # Test 2: not making a comment because a comment has been made + jira_client.comments.return_value = [ + MagicMock(body=comment1), + MagicMock(body=comment2), + ] + jira_client.add_comment.reset_mock() + early_kernel.comment_on_tracker(logger, False, jira_client, tracker, [comment2, comment1]) + jira_client.add_comment.assert_not_called() diff --git a/tests/test_find_bugs_kernel_cli.py b/tests/test_find_bugs_kernel_cli.py index b242caf9..7b3837cf 100644 --- a/tests/test_find_bugs_kernel_cli.py +++ b/tests/test_find_bugs_kernel_cli.py @@ -12,6 +12,7 @@ from elliottlib.config_model import KernelBugSweepConfig from elliottlib.runtime import Runtime from elliottlib.bzutil import JIRABugTracker +from elliottlib import early_kernel class TestFindBugsKernelCli(IsolatedAsyncioTestCase): @@ -27,7 +28,7 @@ def test_find_kmaint_trackers(self): def test_get_and_filter_bugs(self): runtime = MagicMock() cli = FindBugsKernelCli( - runtime=runtime, trackers=[], clone=True, reconcile=True, comment=True, dry_run=False) + runtime=runtime, trackers=[], clone=True, reconcile=True, update_tracker=True, dry_run=False) bz_client = MagicMock(spec=Bugzilla) bz_client.getbugs.return_value = [ MagicMock(spec=Bug, id=1, weburl="irrelevant", cf_zstream_target_release=None), @@ -45,7 +46,7 @@ def test_get_and_filter_bugs(self): def test_find_bugs(self, _get_and_filter_bugs: Mock): runtime = MagicMock() cli = FindBugsKernelCli( - runtime=runtime, trackers=[], clone=True, reconcile=True, comment=True, dry_run=False) + runtime=runtime, trackers=[], clone=True, reconcile=True, update_tracker=True, dry_run=False) bz_client = MagicMock(spec=Bugzilla) tracker = MagicMock(spec=Issue, key="TRACKER-1", fields=MagicMock( summary="foo-1.0.1-1.el8_6 and bar-1.0.1-1.el8_6 early delivery via OCP", @@ -72,7 +73,7 @@ def test_clone_bugs1(self): # Test cloning a bug that has not already been cloned runtime = MagicMock() cli = FindBugsKernelCli( - runtime=runtime, trackers=[], clone=True, reconcile=True, comment=True, dry_run=False) + runtime=runtime, trackers=[], clone=True, reconcile=True, update_tracker=True, dry_run=False) jira_client = MagicMock(spec=JIRA) bugs = [ MagicMock(spec=Bug, id=1, weburl="https://example.com/1", @@ -112,7 +113,7 @@ def test_clone_bugs2(self): # Test cloning a bug that has already been cloned runtime = MagicMock() cli = FindBugsKernelCli( - runtime=runtime, trackers=[], clone=True, reconcile=True, comment=True, dry_run=False) + runtime=runtime, trackers=[], clone=True, reconcile=True, update_tracker=True, dry_run=False) jira_client = MagicMock(spec=JIRA) bugs = [ MagicMock(spec=Bug, id=1, weburl="https://example.com/1", @@ -168,42 +169,6 @@ def test_print_report(self): TRACKER-1 2 N/A Verified test bug 2 """.strip()) - @patch("elliottlib.brew.get_builds_tags") - def test_comment_on_tracker(self, get_builds_tags: Mock): - runtime = MagicMock() - cli = FindBugsKernelCli( - runtime=runtime, trackers=[], clone=True, reconcile=True, comment=True, dry_run=False) - jira_client = MagicMock(spec=JIRA) - conf = KernelBugSweepConfig.TargetJiraConfig( - project="TARGET-PROJECT", - component="Target Component", - version="4.14", target_release="4.14.z", - candidate_brew_tag="fake-candidate", prod_brew_tag="fake-prod") - tracker = MagicMock(spec=Issue, key="TRACKER-1", fields=MagicMock( - summary="kernel-1.0.1-1.fake and kernel-rt-1.0.1-1.fake early delivery via OCP", - description="Fixes bugzilla.redhat.com/show_bug.cgi?id=5 and bz6.", - )) - koji_api = MagicMock(spec=koji.ClientSession) - get_builds_tags.return_value = [ - [{"name": "irrelevant-1"}, {"name": "fake-candidate"}], - [{"name": "irrelevant-2"}, {"name": "fake-candidate"}], - ] - jira_client.comments.return_value = [] - - # Test 1: making a comment - cli._comment_on_tracker(jira_client, tracker, koji_api, conf) - jira_client.add_comment.assert_called_once_with("TRACKER-1", "Build(s) ['kernel-1.0.1-1.fake', 'kernel-rt-1.0.1-1.fake'] was/were already tagged into fake-candidate.") - - # Test 2: not making a comment because a comment has been made - jira_client.comments.return_value = [ - MagicMock(body="irrelevant 1"), - MagicMock(body="Build(s) ['kernel-1.0.1-1.fake', 'kernel-rt-1.0.1-1.fake'] was/were already tagged into fake-candidate."), - MagicMock(body="irrelevant 2"), - ] - jira_client.add_comment.reset_mock() - cli._comment_on_tracker(jira_client, tracker, koji_api, conf) - jira_client.add_comment.assert_not_called() - def test_new_jira_fields_from_bug(self): bug = MagicMock(spec=Bug, id=12345, cf_zstream_target_release="8.6.0", weburl="https://example.com/12345", @@ -242,11 +207,11 @@ def test_new_jira_fields_from_bug(self): @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._print_report") @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._clone_bugs") - @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._comment_on_tracker") + @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._update_tracker") @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._find_bugs") @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._find_kmaint_trackers") async def test_run_without_specified_trackers( - self, _find_kmaint_trackers: Mock, _find_bugs: Mock, _comment_on_tracker: Mock, + self, _find_kmaint_trackers: Mock, _find_bugs: Mock, _update_tracker: Mock, _clone_bugs: Mock, _print_report: Mock): runtime = MagicMock( autospec=Runtime, assembly_type=AssemblyTypes.STREAM, @@ -292,18 +257,18 @@ async def test_run_without_specified_trackers( summary="fake summary 10003", description="fake description 10003"), ] cli = FindBugsKernelCli( - runtime=runtime, trackers=[], clone=True, reconcile=True, comment=True, dry_run=False) + runtime=runtime, trackers=[], clone=True, reconcile=True, update_tracker=True, dry_run=False) await cli.run() - _comment_on_tracker.assert_called_once_with(ANY, _find_kmaint_trackers.return_value[0], ANY, ANY) + _update_tracker.assert_called_once_with(ANY, _find_kmaint_trackers.return_value[0], ANY, ANY) _clone_bugs.assert_called_once_with(ANY, _find_bugs.return_value, ANY) @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._print_report") @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._clone_bugs") - @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._comment_on_tracker") + @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._update_tracker") @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._find_bugs") @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._find_kmaint_trackers") async def test_run_with_specified_trackers( - self, _find_kmaint_trackers: Mock, _find_bugs: Mock, _comment_on_tracker: Mock, + self, _find_kmaint_trackers: Mock, _find_bugs: Mock, _update_tracker: Mock, _clone_bugs: Mock, _print_report: Mock): runtime = MagicMock( autospec=Runtime, assembly_type=AssemblyTypes.STREAM, @@ -349,8 +314,8 @@ async def test_run_with_specified_trackers( summary="fake summary 10003", description="fake description 10003"), ] cli = FindBugsKernelCli( - runtime=runtime, trackers=["TRACKER-999"], clone=True, reconcile=True, comment=True, dry_run=False) + runtime=runtime, trackers=["TRACKER-999"], clone=True, reconcile=True, update_tracker=True, dry_run=False) await cli.run() - _comment_on_tracker.assert_called_once() + _update_tracker.assert_called_once() _clone_bugs.assert_called_once_with(ANY, _find_bugs.return_value, ANY) _find_kmaint_trackers.assert_not_called() diff --git a/tests/test_find_bugs_kernel_clones_cli.py b/tests/test_find_bugs_kernel_clones_cli.py index b6987732..8d30759f 100644 --- a/tests/test_find_bugs_kernel_clones_cli.py +++ b/tests/test_find_bugs_kernel_clones_cli.py @@ -9,6 +9,7 @@ from elliottlib.cli.find_bugs_kernel_clones_cli import FindBugsKernelClonesCli from elliottlib.config_model import KernelBugSweepConfig from elliottlib.bzutil import JIRABugTracker +from elliottlib import early_kernel class TestFindBugsKernelClonesCli(IsolatedAsyncioTestCase): @@ -34,7 +35,7 @@ def setUp(self) -> None: def test_get_jira_bugs(self): runtime = MagicMock() cli = FindBugsKernelClonesCli( - runtime=runtime, trackers=[], bugs=[], move=True, comment=True, dry_run=False) + runtime=runtime, trackers=[], bugs=[], move=True, update_tracker=True, dry_run=False) jira_client = MagicMock(spec=JIRA) component = MagicMock() component.configure_mock(name="RHCOS") @@ -64,12 +65,14 @@ def test_search_for_jira_bugs(self): jira_client.search_issues.assert_called_once_with(expected_jql, maxResults=0) self.assertEqual([issue.key for issue in actual], ["FOO-1", "FOO-2", "FOO-3"]) - @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._move_jira") + @patch("elliottlib.early_kernel.process_shipped_tracker") + @patch("elliottlib.early_kernel.move_jira") @patch("elliottlib.brew.get_builds_tags") - def test_update_jira_bugs(self, get_builds_tags: Mock, _move_jira: Mock): + def test_update_jira_bugs(self, get_builds_tags: Mock, _move_jira: Mock, + process_shipped_tracker: Mock): runtime = MagicMock() jira_client = MagicMock(spec=JIRA) - jira_client.issue.return_value = MagicMock(spec=Issue, ** { + tracker = jira_client.issue.return_value = MagicMock(spec=Issue, ** { "key": "KMAINT-1", "fields": MagicMock(), "fields.project.key": "KMAINT", @@ -78,7 +81,7 @@ def test_update_jira_bugs(self, get_builds_tags: Mock, _move_jira: Mock): "fields.description": "Fixes bugzilla.redhat.com/show_bug.cgi?id=5 and bz6.", }) cli = FindBugsKernelClonesCli( - runtime=runtime, trackers=[], bugs=[], move=True, comment=True, dry_run=False) + runtime=runtime, trackers=[], bugs=[], move=True, update_tracker=True, dry_run=False) bugs = [ MagicMock(spec=Issue, **{ "key": "FOO-1", "fields": MagicMock(), @@ -103,24 +106,19 @@ def test_update_jira_bugs(self, get_builds_tags: Mock, _move_jira: Mock): [{"name": "irrelevant-2"}, {"name": "rhaos-4.14-rhel-9-candidate"}], ] cli._update_jira_bugs(jira_client, bugs, koji_api, self._config) - _move_jira.assert_any_call(jira_client, bugs[0], "MODIFIED", ANY) - _move_jira.assert_any_call(jira_client, bugs[1], "MODIFIED", ANY) + _move_jira.assert_any_call(ANY, False, jira_client, bugs[0], "MODIFIED", ANY) + _move_jira.assert_any_call(ANY, False, jira_client, bugs[1], "MODIFIED", ANY) - def test_move_jira(self): - runtime = MagicMock() - jira_client = MagicMock(spec=JIRA) - cli = FindBugsKernelClonesCli( - runtime=runtime, trackers=[], bugs=[], move=True, comment=True, dry_run=False) - comment = "Test message" - issue = MagicMock(spec=Issue, **{ - "key": "FOO-1", "fields": MagicMock(), - "fields.labels": ["art:bz#1", "art:kmaint:KMAINT-1"], - "fields.status.name": "New", - }) - jira_client.current_user.return_value = "fake-user" - cli._move_jira(jira_client, issue, "MODIFIED", comment) - jira_client.assign_issue.assert_called_once_with("FOO-1", "fake-user") - jira_client.transition_issue.assert_called_once_with("FOO-1", "MODIFIED") + # now with shipped + _move_jira.reset_mock() + get_builds_tags.return_value = [ + [{"name": "rhaos-4.14-rhel-9"}, {"name": "rhaos-4.14-rhel-9-candidate"}], + [{"name": "rhaos-4.14-rhel-9"}, {"name": "rhaos-4.14-rhel-9-candidate"}], + ] + cli._update_jira_bugs(jira_client, bugs, koji_api, self._config) + _move_jira.assert_any_call(ANY, False, jira_client, bugs[0], "CLOSED", ANY) + process_shipped_tracker.assert_called_once_with(ANY, False, ANY, tracker, ANY, + "rhaos-4.14-rhel-9") def test_print_report(self): report = { @@ -163,7 +161,6 @@ async def test_run_without_specified_bugs(self, _search_for_jira_bugs: Mock, _up }, } ) - jira_client = runtime.bug_trackers.return_value._client found_bugs = [ MagicMock(spec=Issue, **{ "key": "FOO-1", "fields": MagicMock(), @@ -187,9 +184,9 @@ async def test_run_without_specified_bugs(self, _search_for_jira_bugs: Mock, _up ] _search_for_jira_bugs.return_value = found_bugs cli = FindBugsKernelClonesCli( - runtime=runtime, trackers=[], bugs=[], move=True, comment=True, dry_run=False) + runtime=runtime, trackers=[], bugs=[], move=True, update_tracker=True, dry_run=False) cli.run() - _update_jira_bugs.assert_called_once_with(jira_client, found_bugs, ANY, ANY) + _update_jira_bugs.assert_called_once_with(ANY, found_bugs, ANY, ANY) expected_report = { 'jira_issues': [ {'key': 'FOO-1', 'summary': 'Fake bug 1', 'status': 'New'}, @@ -226,7 +223,6 @@ async def test_run_with_specified_bugs(self, _search_for_jira_bugs: Mock, _updat }, } ) - jira_client = runtime.bug_trackers.return_value._client found_bugs = [ MagicMock(spec=Issue, **{ "key": "FOO-1", "fields": MagicMock(), @@ -250,9 +246,10 @@ async def test_run_with_specified_bugs(self, _search_for_jira_bugs: Mock, _updat ] _get_jira_bugs.return_value = found_bugs cli = FindBugsKernelClonesCli( - runtime=runtime, trackers=[], bugs=["FOO-1", "FOO-2", "FOO-3"], move=True, comment=True, dry_run=False) + runtime=runtime, trackers=[], bugs=["FOO-1", "FOO-2", "FOO-3"], move=True, + update_tracker=True, dry_run=False) cli.run() - _update_jira_bugs.assert_called_once_with(jira_client, found_bugs, ANY, ANY) + _update_jira_bugs.assert_called_once_with(ANY, found_bugs, ANY, ANY) expected_report = { 'jira_issues': [ {'key': 'FOO-1', 'summary': 'Fake bug 1', 'status': 'New'},