Passed
Push — master ( cddcf6...1d3855 )
by Alexander
01:59
created

tcms.issuetracker.types   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 397
Duplicated Lines 25.69 %

Importance

Changes 0
Metric Value
wmc 44
eloc 182
dl 102
loc 397
rs 8.8798
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A IssueTrackerType.__init__() 0 5 1
A IssueTrackerType.report_issue_from_testcase() 0 14 1
A IssueTrackerType.add_testcase_to_issue() 0 14 1
A IssueTrackerType.from_name() 0 9 2
A Bugzilla.add_testcase_to_issue() 0 3 2
A GitHub.is_adding_testcase_to_issue_disabled() 2 2 1
A Gitlab.report_issue_from_testcase() 23 23 2
A IssueTrackerType.is_adding_testcase_to_issue_disabled() 0 10 1
A IssueTrackerType.all_issues_link() 0 2 1
A GitHub.add_testcase_to_issue() 3 3 2
A Bugzilla.__init__() 0 13 2
A LinkOnly.is_adding_testcase_to_issue_disabled() 0 2 1
B JIRA.report_issue_from_testcase() 0 48 5
A Bugzilla.rpc() 0 11 2
A JIRA.__init__() 0 15 3
A Bugzilla.all_issues_link() 0 8 3
A JIRA.add_testcase_to_issue() 0 3 2
A Gitlab.add_testcase_to_issue() 3 3 2
A GitHub.report_issue_from_testcase() 25 25 2
A Gitlab.__init__() 5 5 1
A Bugzilla.report_issue_from_testcase() 0 25 2
A Gitlab.is_adding_testcase_to_issue_disabled() 2 2 1
A JIRA.all_issues_link() 0 8 3
A GitHub.__init__() 5 5 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like tcms.issuetracker.types often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""
2
    This module implements Kiwi TCMS interface to external issue tracking systems.
3
    :class:`tcms.issuetracker.types.IssueTrackerType` provides the interface
4
    while the rest of the classes in this module implement it! Refer to each
5
    implementor class for integration specifics!
6
"""
7
8
import os
9
import tempfile
10
from urllib.parse import urlencode
11
12
import jira
13
import github
14
import bugzilla
15
import gitlab
16
17
from django.conf import settings
18
19
from tcms.issuetracker import bugzilla_integration
20
from tcms.issuetracker import jira_integration
21
from tcms.issuetracker import github_integration
22
from tcms.issuetracker import gitlab_integration
23
24
25
class IssueTrackerType:
26
    """
27
        Represents actions which can be performed with issue trackers.
28
        This is a common interface for all issue trackers that Kiwi TCMS
29
        supports!
30
    """
31
32
    def __init__(self, tracker):
33
        """
34
            :tracker: - BugSystem object
35
        """
36
        self.tracker = tracker
37
38
    @classmethod
39
    def from_name(cls, name):
40
        """
41
            Return the class which matches ``name`` if it exists inside this
42
            module or raise an exception.
43
        """
44
        if name not in globals():
45
            raise NotImplementedError('IT of type %s is not supported' % name)
46
        return globals()[name]
47
48
    def report_issue_from_testcase(self, caserun):
49
        """
50
            When marking Test Case results inside a Test Run there is a
51
            `Report` link. When the `Report` link is clicked this method is called
52
            to help the user report an issue in the IT.
53
54
            This is implemented by constructing an URL string which will pre-fill
55
            bug details like steps to reproduce, product, version, etc from the
56
            test case. Then we open this URL into another browser window!
57
58
            :caserun: - TestCaseRun object
59
            :return: - string - URL
60
        """
61
        raise NotImplementedError()
62
63
    def add_testcase_to_issue(self, testcases, issue):
64
        """
65
            When adding issues to Test Execution results there is a
66
            'Check to add test cases to Issue tracker' checkbox. If
67
            selected this method is called to link the bug report to the
68
            test case which was used to discover the bug.
69
70
            Usually this is implemented by adding a new comment pointing
71
            back to the test case via the internal RPC object.
72
73
            :testcases: - list of TestCase objects
74
            :issue: - Bug object
75
        """
76
        raise NotImplementedError()
77
78
    # pylint: disable = invalid-name, no-self-use
79
    # todo: we should allow this method to raise and the specific error
80
    # message must be returned to the caller instead of generic one.
81
    # as it is LinkOnly tracker doesn't have any integrations but the error
82
    # message is misleading
83
    def is_adding_testcase_to_issue_disabled(self):
84
        """
85
            When is linking a TC to a Bug report disabled?
86
            Usually when all the required credentials are provided.
87
88
            :return: - boolean
89
        """
90
        return not (self.tracker.api_url
91
                    and self.tracker.api_username
92
                    and self.tracker.api_password)
93
94
    def all_issues_link(self, _ids):
95
        """
96
            Used in testruns.views.get() aka run/report.html to produce
97
            a single URL which will open all reported issues into a single
98
            page in the Issue tracker. For example Bugzilla supports listing
99
            multiple bugs into a table. GitHub on the other hand doesn't
100
            support this functionality.
101
102
            :ids: - list of issues reported against test executions
103
104
            :return: - None if not suported or string representing the URL
105
        """
106
107
108
class Bugzilla(IssueTrackerType):
109
    """
110
        Support for Bugzilla. Requires:
111
112
        :api_url: - the XML-RPC URL for your Bugzilla instance
113
        :api_username: - a username registered in Bugzilla
114
        :api_password: - the password for this username
115
116
        You can also provide the ``BUGZILLA_AUTH_CACHE_DIR`` setting (in ``product.py``)
117
        to control where authentication cookies for Bugzilla will be saved. If this
118
        is not provided a temporary directory will be used each time we try to login
119
        into Bugzilla!
120
    """
121
122
    def __init__(self, tracker):
123
        super().__init__(tracker)
124
125
        # directory for Bugzilla credentials
126
        self._bugzilla_cache_dir = getattr(
127
            settings,
128
            "BUGZILLA_AUTH_CACHE_DIR",
129
            tempfile.mkdtemp(prefix='.bugzilla-')
130
        )
131
        if not os.path.exists(self._bugzilla_cache_dir):
132
            os.makedirs(self._bugzilla_cache_dir, 0o700)
133
134
        self._rpc = None
135
136
    @property
137
    def rpc(self):
138
        if self._rpc is None:
139
            self._rpc = bugzilla.Bugzilla(
140
                self.tracker.api_url,
141
                user=self.tracker.api_username,
142
                password=self.tracker.api_password,
143
                cookiefile=self._bugzilla_cache_dir + 'cookie',
144
                tokenfile=self._bugzilla_cache_dir + 'token',
145
            )
146
        return self._rpc
147
148
    def add_testcase_to_issue(self, testcases, issue):
149
        for case in testcases:
150
            bugzilla_integration.BugzillaThread(self.rpc, case, issue).start()
151
152
    def all_issues_link(self, ids):
153
        if not self.tracker.base_url:
154
            return None
155
156
        if not self.tracker.base_url.endswith('/'):
157
            self.tracker.base_url += '/'
158
159
        return self.tracker.base_url + 'buglist.cgi?bugidtype=include&bug_id=%s' % ','.join(ids)
160
161
    def report_issue_from_testcase(self, caserun):
162
        args = {}
163
        args['cf_build_id'] = caserun.run.build.name
164
165
        txt = caserun.case.get_text_with_version(case_text_version=caserun.case_text_version)
166
167
        comment = "Filed from caserun %s\n\n" % caserun.get_full_url()
168
        comment += "Version-Release number of selected " \
169
                   "component (if applicable):\n"
170
        comment += '%s\n\n' % caserun.build.name
171
        comment += "Steps to Reproduce: \n%s\n\n" % txt
172
        comment += "Actual results: \n<describe what happened>\n\n"
173
174
        args['comment'] = comment
175
        args['component'] = caserun.case.component.values_list('name',
176
                                                               flat=True)
177
        args['product'] = caserun.run.plan.product.name
178
        args['short_desc'] = 'Test case failure: %s' % caserun.case.summary
179
        args['version'] = caserun.run.product_version
180
181
        url = self.tracker.base_url
182
        if not url.endswith('/'):
183
            url += '/'
184
185
        return url + 'enter_bug.cgi?' + urlencode(args, True)
186
187
188
class JIRA(IssueTrackerType):
189
    """
190
        Support for JIRA. Requires:
191
192
        :api_url: - the API URL for your JIRA instance
193
        :api_username: - a username registered in JIRA
194
        :api_password: - the password for this username
195
196
        Additional control can be applied via the ``JIRA_OPTIONS`` configuration
197
        setting (in ``product.py``). By default this setting is not provided and
198
        the code uses ``jira.JIRA.DEFAULT_OPTIONS`` from the ``jira`` Python module!
199
    """
200
201
    def __init__(self, tracker):
202
        super(JIRA, self).__init__(tracker)
203
204
        if hasattr(settings, 'JIRA_OPTIONS'):
205
            options = settings.JIRA_OPTIONS
206
        else:
207
            options = None
208
209
        # b/c jira.JIRA tries to connect when object is created
210
        # see https://github.com/kiwitcms/Kiwi/issues/100
211
        if not self.is_adding_testcase_to_issue_disabled():
212
            self.rpc = jira.JIRA(
213
                tracker.api_url,
214
                basic_auth=(self.tracker.api_username, self.tracker.api_password),
215
                options=options,
216
            )
217
218
    def add_testcase_to_issue(self, testcases, issue):
219
        for case in testcases:
220
            jira_integration.JiraThread(self.rpc, case, issue).start()
221
222
    def all_issues_link(self, ids):
223
        if not self.tracker.base_url:
224
            return None
225
226
        if not self.tracker.base_url.endswith('/'):
227
            self.tracker.base_url += '/'
228
229
        return self.tracker.base_url + 'issues/?jql=issueKey%%20in%%20(%s)' % '%2C%20'.join(ids)
230
231
    def report_issue_from_testcase(self, caserun):
232
        """
233
            For the HTML API description see:
234
            https://confluence.atlassian.com/display/JIRA050/Creating+Issues+via+direct+HTML+links
235
        """
236
        # note: your jira instance needs to have the same projects
237
        # defined otherwise this will fail!
238
        project = self.rpc.project(caserun.run.plan.product.name)
239
240
        try:
241
            issue_type = self.rpc.issue_type_by_name('Bug')
242
        except KeyError:
243
            issue_type = self.rpc.issue_types()[0]
244
245
        args = {
246
            'pid': project.id,
247
            'issuetype': issue_type.id,
248
            'summary': 'Failed test: %s' % caserun.case.summary,
249
        }
250
251
        try:
252
            # apparently JIRA can't search users via their e-mail so try to
253
            # search by username and hope that it matches
254
            tested_by = caserun.tested_by
255
            if not tested_by:
256
                tested_by = caserun.assignee
257
258
            args['reporter'] = self.rpc.user(tested_by.username).key
259
        except jira.JIRAError:
260
            pass
261
262
        txt = caserun.case.get_text_with_version(case_text_version=caserun.case_text_version)
263
264
        comment = "Filed from caserun %s\n\n" % caserun.get_full_url()
265
        comment += "Product:\n%s\n\n" % caserun.run.plan.product.name
266
        comment += "Component(s):\n%s\n\n" % caserun.case.component.values_list('name', flat=True)
267
        comment += "Version-Release number of selected " \
268
                   "component (if applicable):\n"
269
        comment += "%s\n\n" % caserun.build.name
270
        comment += "Steps to Reproduce: \n%s\n\n" % txt
271
        comment += "Actual results: \n<describe what happened>\n\n"
272
        args['description'] = comment
273
274
        url = self.tracker.base_url
275
        if not url.endswith('/'):
276
            url += '/'
277
278
        return url + '/secure/CreateIssueDetails!init.jspa?' + urlencode(args, True)
279
280
281 View Code Duplication
class GitHub(IssueTrackerType):
0 ignored issues
show
Duplication introduced by Mr. Senko
This code seems to be duplicated in your project.
Loading history...
282
    """
283
        Support for GitHub. Requires:
284
285
        :base_url: - URL to a GitHub repository for which we're going to report issues
286
        :api_password: - GitHub API token.
287
288
        .. note::
289
290
            You can leave the ``api_url`` and ``api_username`` fields blank because
291
            the integration code doesn't use them!
292
293
        .. note::
294
295
            GitHub does not support displaying multiple issues in a table format like
296
            Bugzilla and JIRA do. This means that in Test Execution Report view you will
297
            see GitHub issues listed one by one and there will not be a link to open all
298
            of them inside GitHub's interface!
299
    """
300
301
    def __init__(self, tracker):
302
        super(GitHub, self).__init__(tracker)
303
304
        # NOTE: we use an access token so only the password field is required
305
        self.rpc = github.Github(self.tracker.api_password)
306
307
    def add_testcase_to_issue(self, testcases, issue):
308
        for case in testcases:
309
            github_integration.GitHubThread(self.rpc, self.tracker, case, issue).start()
310
311
    def is_adding_testcase_to_issue_disabled(self):
312
        return not (self.tracker.base_url and self.tracker.api_password)
313
314
    def report_issue_from_testcase(self, caserun):
315
        """
316
            GitHub only supports title and body parameters
317
        """
318
        args = {
319
            'title': 'Failed test: %s' % caserun.case.summary,
320
        }
321
322
        txt = caserun.case.get_text_with_version(case_text_version=caserun.case_text_version)
323
324
        comment = "Filed from caserun %s\n\n" % caserun.get_full_url()
325
        comment += "Product:\n%s\n\n" % caserun.run.plan.product.name
326
        comment += "Component(s):\n%s\n\n" % caserun.case.component.values_list('name', flat=True)
327
        comment += "Version-Release number of selected " \
328
                   "component (if applicable):\n"
329
        comment += "%s\n\n" % caserun.build.name
330
        comment += "Steps to Reproduce: \n%s\n\n" % txt
331
        comment += "Actual results: \n<describe what happened>\n\n"
332
        args['body'] = comment
333
334
        url = self.tracker.base_url
335
        if not url.endswith('/'):
336
            url += '/'
337
338
        return url + '/issues/new?' + urlencode(args, True)
339
340
341 View Code Duplication
class Gitlab(IssueTrackerType):
0 ignored issues
show
Duplication introduced by Filipe Arruda
This code seems to be duplicated in your project.
Loading history...
342
    """
343
        Support for Gitlab. Requires:
344
345
        :base_url: - URL to a Gitlab repository for which we're going to report issues
346
        :api_password: - Gitlab API token.
347
    """
348
349
    def __init__(self, tracker):
350
        super(Gitlab, self).__init__(tracker)
351
352
        # we use an access token so only the password field is required
353
        self.rpc = gitlab.Gitlab(self.tracker.api_url, private_token=self.tracker.api_password)
354
355
    def add_testcase_to_issue(self, testcases, issue):
356
        for case in testcases:
357
            gitlab_integration.GitlabThread(self.rpc, self.tracker, case, issue).start()
358
359
    def is_adding_testcase_to_issue_disabled(self):
360
        return not (self.tracker.base_url and self.tracker.api_password)
361
362
    def report_issue_from_testcase(self, caserun):
363
        args = {
364
            'issue[title]': 'Failed test: %s' % caserun.case.summary,
365
        }
366
367
        txt = caserun.case.get_text_with_version(case_text_version=caserun.case_text_version)
368
369
        comment = "Filed from caserun %s\n\n" % caserun.get_full_url()
370
        comment += "**Product**:\n%s\n\n" % caserun.run.plan.product.name
371
        comment += "**Component(s)**:\n%s\n\n"\
372
                   % caserun.case.component.values_list('name', flat=True)
373
        comment += "Version-Release number of selected " \
374
                   "component (if applicable):\n"
375
        comment += "%s\n\n" % caserun.build.name
376
        comment += "**Steps to Reproduce**: \n%s\n\n" % txt
377
        comment += "**Actual results**: \n<describe what happened>\n\n"
378
        args['issue[description]'] = comment
379
380
        url = self.tracker.base_url
381
        if not url.endswith('/'):
382
            url += '/'
383
384
        return url + '/issues/new?' + urlencode(args, True)
385
386
387
class LinkOnly(IssueTrackerType):
388
    """
389
        Allow only linking issues to TestExecution records. Can be used when your
390
        issue tracker is not integrated with Kiwi TCMS.
391
392
        No additional API integration available!
393
    """
394
395
    def is_adding_testcase_to_issue_disabled(self):
396
        return True
397