Passed
Push — master ( d91cfb...16ae9a )
by Alexander
02:22
created

LinkOnly.is_adding_testcase_to_issue_disabled()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
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 TestCase Run 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
    def is_adding_testcase_to_issue_disabled(self):
80
        """
81
            When is linking a TC to a Bug report disabled?
82
            Usually when all the required credentials are provided.
83
84
            :return: - boolean
85
        """
86
        return not (self.tracker.api_url
87
                    and self.tracker.api_username
88
                    and self.tracker.api_password)
89
90
    def all_issues_link(self, _ids):
91
        """
92
            Used in testruns.views.get() aka run/report.html to produce
93
            a single URL which will open all reported issues into a single
94
            page in the Issue tracker. For example Bugzilla supports listing
95
            multiple bugs into a table. GitHub on the other hand doesn't
96
            support this functionality.
97
98
            :ids: - list of issues reported against test case runs
99
100
            :return: - None if not suported or string representing the URL
101
        """
102
103
104
class Bugzilla(IssueTrackerType):
105
    """
106
        Support for Bugzilla. Requires:
107
108
        :api_url: - the XML-RPC URL for your Bugzilla instance
109
        :api_username: - a username registered in Bugzilla
110
        :api_password: - the password for this username
111
112
        You can also provide the ``BUGZILLA_AUTH_CACHE_DIR`` setting (in ``product.py``)
113
        to control where authentication cookies for Bugzilla will be saved. If this
114
        is not provided a temporary directory will be used each time we try to login
115
        into Bugzilla!
116
    """
117
118
    def __init__(self, tracker):
119
        super().__init__(tracker)
120
121
        # directory for Bugzilla credentials
122
        self._bugzilla_cache_dir = getattr(
123
            settings,
124
            "BUGZILLA_AUTH_CACHE_DIR",
125
            tempfile.mkdtemp(prefix='.bugzilla-')
126
        )
127
        if not os.path.exists(self._bugzilla_cache_dir):
128
            os.makedirs(self._bugzilla_cache_dir, 0o700)
129
130
        self._rpc = None
131
132
    @property
133
    def rpc(self):
134
        if self._rpc is None:
135
            self._rpc = bugzilla.Bugzilla(
136
                self.tracker.api_url,
137
                user=self.tracker.api_username,
138
                password=self.tracker.api_password,
139
                cookiefile=self._bugzilla_cache_dir + 'cookie',
140
                tokenfile=self._bugzilla_cache_dir + 'token',
141
            )
142
        return self._rpc
143
144
    def add_testcase_to_issue(self, testcases, issue):
145
        for case in testcases:
146
            bugzilla_integration.BugzillaThread(self.rpc, case, issue).start()
147
148
    def all_issues_link(self, ids):
149
        if not self.tracker.base_url:
150
            return None
151
152
        if not self.tracker.base_url.endswith('/'):
153
            self.tracker.base_url += '/'
154
155
        return self.tracker.base_url + 'buglist.cgi?bugidtype=include&bug_id=%s' % ','.join(ids)
156
157
    def report_issue_from_testcase(self, caserun):
158
        args = {}
159
        args['cf_build_id'] = caserun.run.build.name
160
161
        txt = caserun.case.get_text_with_version(case_text_version=caserun.case_text_version)
162
163
        comment = "Filed from caserun %s\n\n" % caserun.get_full_url()
164
        comment += "Version-Release number of selected " \
165
                   "component (if applicable):\n"
166
        comment += '%s\n\n' % caserun.build.name
167
        comment += "Steps to Reproduce: \n%s\n\n" % txt
168
        comment += "Actual results: \n<describe what happened>\n\n"
169
170
        args['comment'] = comment
171
        args['component'] = caserun.case.component.values_list('name',
172
                                                               flat=True)
173
        args['product'] = caserun.run.plan.product.name
174
        args['short_desc'] = 'Test case failure: %s' % caserun.case.summary
175
        args['version'] = caserun.run.product_version
176
177
        url = self.tracker.base_url
178
        if not url.endswith('/'):
179
            url += '/'
180
181
        return url + 'enter_bug.cgi?' + urlencode(args, True)
182
183
184
class JIRA(IssueTrackerType):
185
    """
186
        Support for JIRA. Requires:
187
188
        :api_url: - the API URL for your JIRA instance
189
        :api_username: - a username registered in JIRA
190
        :api_password: - the password for this username
191
192
        Additional control can be applied via the ``JIRA_OPTIONS`` configuration
193
        setting (in ``product.py``). By default this setting is not provided and
194
        the code uses ``jira.JIRA.DEFAULT_OPTIONS`` from the ``jira`` Python module!
195
    """
196
197
    def __init__(self, tracker):
198
        super(JIRA, self).__init__(tracker)
199
200
        if hasattr(settings, 'JIRA_OPTIONS'):
201
            options = settings.JIRA_OPTIONS
202
        else:
203
            options = None
204
205
        # b/c jira.JIRA tries to connect when object is created
206
        # see https://github.com/kiwitcms/Kiwi/issues/100
207
        if not self.is_adding_testcase_to_issue_disabled():
208
            self.rpc = jira.JIRA(
209
                tracker.api_url,
210
                basic_auth=(self.tracker.api_username, self.tracker.api_password),
211
                options=options,
212
            )
213
214
    def add_testcase_to_issue(self, testcases, issue):
215
        for case in testcases:
216
            jira_integration.JiraThread(self.rpc, case, issue).start()
217
218
    def all_issues_link(self, ids):
219
        if not self.tracker.base_url:
220
            return None
221
222
        if not self.tracker.base_url.endswith('/'):
223
            self.tracker.base_url += '/'
224
225
        return self.tracker.base_url + 'issues/?jql=issueKey%%20in%%20(%s)' % '%2C%20'.join(ids)
226
227
    def report_issue_from_testcase(self, caserun):
228
        """
229
            For the HTML API description see:
230
            https://confluence.atlassian.com/display/JIRA050/Creating+Issues+via+direct+HTML+links
231
        """
232
        # note: your jira instance needs to have the same projects
233
        # defined otherwise this will fail!
234
        project = self.rpc.project(caserun.run.plan.product.name)
235
236
        try:
237
            issue_type = self.rpc.issue_type_by_name('Bug')
238
        except KeyError:
239
            issue_type = self.rpc.issue_types()[0]
240
241
        args = {
242
            'pid': project.id,
243
            'issuetype': issue_type.id,
244
            'summary': 'Failed test: %s' % caserun.case.summary,
245
        }
246
247
        try:
248
            # apparently JIRA can't search users via their e-mail so try to
249
            # search by username and hope that it matches
250
            tested_by = caserun.tested_by
251
            if not tested_by:
252
                tested_by = caserun.assignee
253
254
            args['reporter'] = self.rpc.user(tested_by.username).key
255
        except jira.JIRAError:
256
            pass
257
258
        txt = caserun.case.get_text_with_version(case_text_version=caserun.case_text_version)
259
260
        comment = "Filed from caserun %s\n\n" % caserun.get_full_url()
261
        comment += "Product:\n%s\n\n" % caserun.run.plan.product.name
262
        comment += "Component(s):\n%s\n\n" % caserun.case.component.values_list('name', flat=True)
263
        comment += "Version-Release number of selected " \
264
                   "component (if applicable):\n"
265
        comment += "%s\n\n" % caserun.build.name
266
        comment += "Steps to Reproduce: \n%s\n\n" % txt
267
        comment += "Actual results: \n<describe what happened>\n\n"
268
        args['description'] = comment
269
270
        url = self.tracker.base_url
271
        if not url.endswith('/'):
272
            url += '/'
273
274
        return url + '/secure/CreateIssueDetails!init.jspa?' + urlencode(args, True)
275
276
277 View Code Duplication
class GitHub(IssueTrackerType):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
278
    """
279
        Support for GitHub. Requires:
280
281
        :base_url: - URL to a GitHub repository for which we're going to report issues
282
        :api_password: - GitHub API token.
283
284
        .. note::
285
286
            You can leave the ``api_url`` and ``api_username`` fields blank because
287
            the integration code doesn't use them!
288
289
        .. note::
290
291
            GitHub does not support displaying multiple issues in a table format like
292
            Bugzilla and JIRA do. This means that in Test Case Run Report view you will
293
            see GitHub issues listed one by one and there will not be a link to open all
294
            of them inside GitHub's interface!
295
    """
296
297
    def __init__(self, tracker):
298
        super(GitHub, self).__init__(tracker)
299
300
        # NOTE: we use an access token so only the password field is required
301
        self.rpc = github.Github(self.tracker.api_password)
302
303
    def add_testcase_to_issue(self, testcases, issue):
304
        for case in testcases:
305
            github_integration.GitHubThread(self.rpc, self.tracker, case, issue).start()
306
307
    def is_adding_testcase_to_issue_disabled(self):
308
        return not (self.tracker.base_url and self.tracker.api_password)
309
310
    def report_issue_from_testcase(self, caserun):
311
        """
312
            GitHub only supports title and body parameters
313
        """
314
        args = {
315
            'title': 'Failed test: %s' % caserun.case.summary,
316
        }
317
318
        txt = caserun.case.get_text_with_version(case_text_version=caserun.case_text_version)
319
320
        comment = "Filed from caserun %s\n\n" % caserun.get_full_url()
321
        comment += "Product:\n%s\n\n" % caserun.run.plan.product.name
322
        comment += "Component(s):\n%s\n\n" % caserun.case.component.values_list('name', flat=True)
323
        comment += "Version-Release number of selected " \
324
                   "component (if applicable):\n"
325
        comment += "%s\n\n" % caserun.build.name
326
        comment += "Steps to Reproduce: \n%s\n\n" % txt
327
        comment += "Actual results: \n<describe what happened>\n\n"
328
        args['body'] = comment
329
330
        url = self.tracker.base_url
331
        if not url.endswith('/'):
332
            url += '/'
333
334
        return url + '/issues/new?' + urlencode(args, True)
335
336
337 View Code Duplication
class Gitlab(IssueTrackerType):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
338
    """
339
        Support for Gitlab. Requires:
340
341
        :base_url: - URL to a Gitlab repository for which we're going to report issues
342
        :api_password: - Gitlab API token.
343
    """
344
345
    def __init__(self, tracker):
346
        super(Gitlab, self).__init__(tracker)
347
348
        # we use an access token so only the password field is required
349
        self.rpc = gitlab.Gitlab(self.tracker.api_url, private_token=self.tracker.api_password)
350
351
    def add_testcase_to_issue(self, testcases, issue):
352
        for case in testcases:
353
            gitlab_integration.GitlabThread(self.rpc, self.tracker, case, issue).start()
354
355
    def is_adding_testcase_to_issue_disabled(self):
356
        return not (self.tracker.base_url and self.tracker.api_password)
357
358
    def report_issue_from_testcase(self, caserun):
359
        args = {
360
            'issue[title]': 'Failed test: %s' % caserun.case.summary,
361
        }
362
363
        txt = caserun.case.get_text_with_version(case_text_version=caserun.case_text_version)
364
365
        comment = "Filed from caserun %s\n\n" % caserun.get_full_url()
366
        comment += "**Product**:\n%s\n\n" % caserun.run.plan.product.name
367
        comment += "**Component(s)**:\n%s\n\n"\
368
                   % caserun.case.component.values_list('name', flat=True)
369
        comment += "Version-Release number of selected " \
370
                   "component (if applicable):\n"
371
        comment += "%s\n\n" % caserun.build.name
372
        comment += "**Steps to Reproduce**: \n%s\n\n" % txt
373
        comment += "**Actual results**: \n<describe what happened>\n\n"
374
        args['issue[description]'] = comment
375
376
        url = self.tracker.base_url
377
        if not url.endswith('/'):
378
            url += '/'
379
380
        return url + '/issues/new?' + urlencode(args, True)
381
382
383
class LinkOnly(IssueTrackerType):
384
    """
385
        LinkOnly Issue Tracker, for when your issue tracker is not one of the supported ones.
386
387
        Because of this, no API related functionallities are available
388
    """
389
390
    def is_adding_testcase_to_issue_disabled(self):
391
        return True
392