kiwitcms/Kiwi

View on GitHub
tcms/issuetracker/base.py

Summary

Maintainability
A
0 mins
Test Coverage
import re
from importlib import import_module

from django.conf import settings
from opengraph.opengraph import OpenGraph

RE_ENDS_IN_INT = re.compile(r"[\d]+$")


def _function_from_path(fully_qualified_dotted_path):
    """
    Helper function which returns a callable object from a
    fully qualified dotted path string!
    """
    function_name = fully_qualified_dotted_path.split(".")[-1]
    module_name = fully_qualified_dotted_path.replace(f".{function_name}", "")

    module_object = import_module(module_name)
    function_object = getattr(module_object, function_name)
    return function_object


class IssueTrackerType:
    """
    Represents actions which can be performed with issue trackers.
    This is a common interface for all issue trackers that Kiwi TCMS
    supports!
    """

    rpc_cache = {}

    def __init__(self, bug_system, request):
        """
        :bug_system: - BugSystem object
        :request: - an HTTP request object
        """
        self.bug_system = bug_system
        self.request = request

    @classmethod
    def bug_id_from_url(cls, url):
        """
        Returns a unique identifier for reported defect. This is used by the
        underlying integration libraries. Usually that identifier is an
        integer number.

        The default implementation is to leave the last group of numeric
        characters at the end of a string!
        """
        return int(RE_ENDS_IN_INT.search(url.strip()).group(0))

    @staticmethod
    def get_case_components(case):
        """
        Returns a string that contains comma separated list of components
        bound to a given testcase
        """
        case_components = ", ".join(case.component.values_list("name", flat=True))
        return case_components

    def details(self, url):  # pylint: disable=no-self-use
        """
        Returns bug details to be used later. By default this method
        returns OpenGraph metadata (dict) which is shown in the UI as tooltips.
        You can override this method to provide different information.
        """
        result = OpenGraph(url, scrape=True)
        result["from_open_graph"] = True

        # remove data which we don't need
        for key in ["_url", "url", "scrape", "type"]:
            if key in result:
                del result[key]

        return result

    def _report_comment(self, execution, user=None):  # pylint: disable=no-self-use
        """
        Returns the comment which is used in the original defect report.
        """
        txt = execution.case.get_text_with_version(execution.case_text_version)

        reporter = "Unknown"
        if user:
            reporter = user.get_full_name() or user.username

        comment = f"""Filed from execution {execution.get_full_url()}

**Reporter:**
{reporter}

**Product:**
{execution.build.version.product.name}

**Version:**
{execution.build.version.value}

**Build:**
{execution.build.name}

**Component(s):**
{self.get_case_components(execution.case)}

**Steps to reproduce**:
{txt}

**Actual results**:
<describe what happened>


"""
        return comment

    def _report_issue(self, execution, user):
        """
        Used internally to perform the actual report! If you want to override behavior
        this is the appropriate method!

        :return: (object, str) - returns the newly created issue represented as
                 an object that is understood by the internal RPC integration code
                 and the URL to the newly created issue.
        """
        raise NotImplementedError()

    def report_issue_from_testexecution(self, execution, user):
        """
        When marking TestExecution results inside a Test Run there is a
        `Report` link. When the `Report` link is clicked this method is called
        to help the user report an issue in the IT.

        This is implemented by constructing an URL string which will pre-fill
        bug details like steps to reproduce, product, version, etc from the
        test case. Then we open this URL into another browser window!

        :execution: TestExecution object
        :user: User object
        :return: - string - URL
        """
        (new_issue, url) = self._report_issue(execution, user)
        if new_issue:
            self.post_process_new_issue(new_issue, execution, user)
        return url

    def post_process_new_issue(self, new_issue, execution, user):
        """
        Perform any post-processing for newly created issues.

        :new_issue: An object specific to the actual RPC implementation
        :execution: TestExecution object
        :user: User object

        .. versionadded:: 11.4
        """
        for fully_qualified_dotted_path in settings.EXTERNAL_ISSUE_POST_PROCESSORS:
            processor_function = _function_from_path(fully_qualified_dotted_path)
            processor_function(self.rpc, new_issue, execution, user)

    def add_testexecution_to_issue(self, executions, issue_url):
        """
        When linking defect URLs to Test Execution results there is a
        'Add comment to Issue tracker' checkbox. If
        selected this method is called. It should 'link' the existing
        defect back to the TE/TR which reproduced it.

        Usually this is implemented by adding a new comment pointing
        back to the TR/TE via the internal RPC object.

        :executions: - iterable of TestExecution objects
        :issue_url: - the URL of the existing defect
        """
        bug_id = self.bug_id_from_url(issue_url)
        for execution in executions:
            self.post_comment(execution, bug_id)

    @staticmethod
    def text(execution):
        """
        Returns the text that will be posted as a comment to
        the reported bug!
        """
        return f"""---- Confirmed via test execution ----
TR-{execution.run.pk}: {execution.run.summary}
{execution.run.get_full_url()}
TE-{execution.pk}: {execution.case.summary}"""

    def post_comment(self, execution, bug_id):
        """
        :param execution: TestExecution object
        :type execution: :class:`tcms.testruns.models.TestExecution`
        :param bug_id: Unique defect identifier in the system. Usually an int.
        :type bug_id: int or str
        """
        raise NotImplementedError()

    def is_adding_testcase_to_issue_disabled(self):  # pylint: disable=invalid-name
        """
        When is linking a TC to a Bug report disabled?
        Usually when not all of the required credentials are provided.

        :return: True if bug system api url, username and password are provided
        :rtype: bool
        """
        (api_username, api_password) = self.rpc_credentials

        return not (self.bug_system.api_url and api_username and api_password)

    def _rpc_connection(self):
        """
        Returns an object which is used to communicate to the external system.
        This method is meant to be overriden by inherited classes.
        """
        raise NotImplementedError()

    @property
    def rpc(self):
        """
        Returns an object which is used to communicate to the external system.
        This property is meant to be used by the rest of the integration code
        and provides caching b/c connecting to a remote system may be a slow
        operation.
        """
        # b/c jira.JIRA tries to connect when object is created
        # see https://github.com/kiwitcms/Kiwi/issues/100
        if self.is_adding_testcase_to_issue_disabled():
            return None

        # NOTE: using a tuple as the cache-key to prevent integrations which define
        # personal ApiTokens to accidentally use a cached version
        # for the same URL but different credentials
        rpc_key = (self.bug_system.base_url, getattr(self.request, "user", None))
        if rpc_key not in self.rpc_cache:
            self.rpc_cache[rpc_key] = self._rpc_connection()

        return self.rpc_cache[rpc_key]

    @property
    def rpc_credentials(self):
        """
        Returns an tuple of (api_username, api_token_or_password) meant for connecting
        to a 3rd party issue tracker system.

        It can be overriden in order to provide more flexible integrations!

        .. versionadded:: 12.6
        """
        if settings.EXTERNAL_ISSUE_RPC_CREDENTIALS:
            credentials_function = _function_from_path(
                settings.EXTERNAL_ISSUE_RPC_CREDENTIALS
            )
            result = credentials_function(self)

            # if result is None or not a tuple then fallback
            if result and isinstance(result, tuple):
                return result

        return (self.bug_system.api_username, self.bug_system.api_password)