Passed
Push — master ( d8ce69...82db34 )
by Alexander
02:20
created

Backend.add_comment()   A

Complexity

Conditions 1

Size

Total Lines 15
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 15
rs 10
c 0
b 0
f 0
cc 1
nop 3
1
# Copyright (c) 2019 Alexander Todorov <[email protected]>
2
3
import os
4
5
from . import TCMS
6
7
8
class Backend:
9
    """
10
        Facilitates RPC communications with the backend and implements
11
        behavior described at:
12
        http://kiwitcms.org/blog/atodorov/2018/11/05/test-runner-plugin-specification/
13
14
        This class is intended to be used by Kiwi TCMS plugins implemented in
15
        Python. The plugin will call::
16
17
            backend = Backend()
18
            backend.configure()
19
20
            ... parse test results ...
21
22
            test_case_id = backend.test_case_get_or_create(<description>)
23
            backend.add_test_case_to_plan(test_case_id, backend.plan_id)
24
            test_case_run_id = backend.add_test_case_to_run(test_case_id, backend.run_id)
25
            backend.update_test_case_run(test_case_run_id, <status_id>, <comment>)
26
27
        :param prefix: Prefix which will be added to TestPlan.name and
28
                       TestRun.summary
29
30
                       .. versionadded:: 5.2
31
        :type prefix: str
32
    """
33
34
    _statuses = {}
35
    _cases_in_test_run = {}
36
37
    def __init__(self, prefix=''):
38
        """
39
            :param prefix: Prefix which will be added to TestPlan.name and
40
                           TestRun.summary
41
42
                           .. versionadded:: 5.2
43
            :type prefix: str
44
        """
45
        self.prefix = prefix
46
47
        self.rpc = None
48
        self.run_id = None
49
        self.plan_id = None
50
        self.product_id = None
51
        self.category_id = None
52
        self.priority_id = None
53
        self.confirmed_id = None
54
55
    def configure(self):
56
        """
57
            This method is reading all the configs from the environment
58
            and will create necessary TestPlan and TestRun containers!
59
60
            One of the main reasons for it is that
61
            :py:attr:`tcms_api.tcms_api.TCMS.exec` will try to connect
62
            immediately to Kiwi TCMS!
63
64
            .. important::
65
66
                Test runner plugins **must** call this method after
67
                initializing the backend object and **before** calling
68
                any of the other methods!
69
        """
70
        self.rpc = TCMS().exec
71
72
        self.run_id = self.get_run_id()
73
        self.plan_id = self.get_plan_id(self.run_id)
74
        self.product_id, _ = self.get_product_id(self.plan_id)
75
76
        self.category_id = self.rpc.Category.filter({
77
            'product': self.product_id
78
        })[0]['id']
79
        self.priority_id = self.rpc.Priority.filter({})[0]['id']
80
        self.confirmed_id = self.rpc.TestCaseStatus.filter({
81
            'name': 'CONFIRMED'
82
        })[0]['id']
83
84
    def get_status_id(self, name):
85
        """
86
            Get the PK of :class:`tcms.testruns.models.TestCaseRunStatus`
87
            matching the test execution status.
88
89
            .. important::
90
91
                Test runner plugins **must** call this method like so::
92
93
                    id = backend.get_status_id('FAILED')
94
95
            :param name: :class:`tcms.testruns.models.TestCaseRunStatus` name
96
            :type name: str
97
            :rtype: int
98
        """
99
        if name not in self._statuses:
100
            self._statuses[name] = self.rpc.TestCaseRunStatus.filter({
101
                'name': name
102
            })[0]['id']
103
104
        return self._statuses[name]
105
106
    def get_product_id(self, plan_id):
107
        """
108
            Return a :class:`tcms.management.models.Product` PK.
109
110
            .. warning::
111
112
                For internal use by `.configure()`!
113
114
            :param plan_id: :class:`tcms.testplans.models.TestPlan` PK
115
            :type plan_id: int
116
            :rtype: int
117
118
            Order of precedence:
119
120
            - `plan_id` is specified, then use TestPlan.product, otherwise
121
            - use `$TCMS_PRODUCT` as Product.name if specified, otherwise
122
            - use `$TRAVIS_REPO_SLUG` as Product.name if specified, otherwise
123
            - use `$JOB_NAME` as Product.name if specified
124
125
            If Product doesn't exist in the database it will be created with the
126
            first :class:`tcms.management.models.Classification` found!
127
        """
128
        product_id = None
129
        product_name = None
130
131
        test_plan = self.rpc.TestPlan.filter({'pk': plan_id})
132
        if test_plan:
133
            product_id = test_plan[0]['product_id']
134
            product_name = test_plan[0]['product']
135
        else:
136
            product_name = os.environ.get('TCMS_PRODUCT',
137
                                          os.environ.get('TRAVIS_REPO_SLUG',
138
                                                         os.environ.get(
139
                                                             'JOB_NAME')))
140
            if not product_name:
141
                raise Exception('Product name not defined, '
142
                                'missing one of TCMS_PRODUCT, '
143
                                'TRAVIS_REPO_SLUG or JOB_NAME')
144
145
            product = self.rpc.Product.filter({'name': product_name})
146
            if not product:
147
                class_id = self.rpc.Classification.filter({})[0]['id']
148
                product = [self.rpc.Product.create({
149
                    'name': product_name,
150
                    'classification_id': class_id
151
                })]
152
            product_id = product[0]['id']
153
154
        return product_id, product_name
155
156
    def get_version_id(self, product_id):
157
        """
158
            Return a :class:`tcms.management.models.Version` (PK, name).
159
160
            .. warning::
161
162
                For internal use by `.configure()`!
163
164
            :param product_id: :class:`tcms.management.models.Product` PK
165
                               for which to look for Version
166
            :type product_id: int
167
            :return: (version_id, version_value)
168
            :rtype: tuple(int, str)
169
170
            Order of precedence:
171
172
            - use `$TCMS_PRODUCT_VERSION` as Version.value if specified, otherwise
173
            - use `$TRAVIS_COMMIT` as Version.value if specified, otherwise
174
            - use `$TRAVIS_PULL_REQUEST_SHA` as Version.value if specified,
175
              otherwise
176
            - use `$GIT_COMMIT` as Version.value if specified
177
178
            If Version doesn't exist in the database it will be created with the
179
            specified `product_id`!
180
        """
181
        version_val = os.environ.get(
182
            'TCMS_PRODUCT_VERSION',
183
            os.environ.get('TRAVIS_COMMIT',
184
                           os.environ.get('TRAVIS_PULL_REQUEST_SHA',
185
                                          os.environ.get('GIT_COMMIT'))))
186
        if not version_val:
187
            raise Exception('Version value not defined, '
188
                            'missing one of TCMS_PRODUCT_VERSION, '
189
                            'TRAVIS_COMMIT, TRAVIS_PULL_REQUEST_SHA '
190
                            'or GIT_COMMIT')
191
192
        version = self.rpc.Version.filter({'product': product_id,
193
                                           'value': version_val})
194
        if not version:
195
            version = [self.rpc.Version.create({'product': product_id,
196
                                                'value': version_val})]
197
198
        return version[0]['id'], version_val
199
200
    def get_build_id(self, product_id, _version_id):
201
        """
202
            Return a :class:`tcms.management.models.Build` (PK, name).
203
204
            .. warning::
205
206
                For internal use by `.configure()`!
207
208
            :param product_id: :class:`tcms.management.models.Product` PK
209
                               for which to look for Build
210
            :type product_id: int
211
            :param version_id: :class:`tcms.management.models.Version` PK
212
                               for which to look for Build
213
            :type version_id: int
214
            :return: (build_id, build_name)
215
            :rtype: tuple(int, str)
216
217
            Order of precedence:
218
219
            - use `$TCMS_BUILD` as Build.name if specified, otherwise
220
            - use `$TRAVIS_BUILD_NUMBER` as Build.name if specified, otherwise
221
            - use `$BUILD_NUMBER` as Build.name if specified
222
223
            If Build doesn't exist in the database it will be created with the
224
            specified `product_id`!
225
226
            .. note::
227
228
                For `version_id` see https://github.com/kiwitcms/Kiwi/issues/246
229
        """
230
        build_number = os.environ.get('TCMS_BUILD',
231
                                      os.environ.get('TRAVIS_BUILD_NUMBER',
232
                                                     os.environ.get(
233
                                                         'BUILD_NUMBER')))
234
        if not build_number:
235
            raise Exception('Build number not defined, '
236
                            'missing one of TCMS_BUILD, '
237
                            'TRAVIS_BUILD_NUMBER or BUILD_NUMBER')
238
239
        build = self.rpc.Build.filter({'name': build_number,
240
                                       'product': product_id})
241
        if not build:
242
            build = [self.rpc.Build.create({'name': build_number,
243
                                            'product': product_id})]
244
245
        return build[0]['build_id'], build_number
246
247
    def get_plan_type_id(self):
248
        """
249
            Return an **Integration** PlanType.
250
251
            .. warning::
252
253
                For internal use by `.configure()`!
254
255
            :return: :class:`tcms.testplans.models.PlanType` PK
256
            :rtype: int
257
        """
258
        plan_type = self.rpc.PlanType.filter({'name': 'Integration'})
259
        if not plan_type:
260
            plan_type = [self.rpc.PlanType.create({'name': 'Integration'})]
261
262
        return plan_type[0]['id']
263
264
    def get_plan_id(self, run_id):
265
        """
266
            If a TestRun with PK `run_id` exists then return the TestPlan to
267
            which this TestRun is assigned, otherwise create new TestPlan with
268
            Product and Version specified by environment variables.
269
270
            .. warning::
271
272
                For internal use by `.configure()`!
273
274
            :param run_id: :class:`tcms.testruns.models.TestRun` PK
275
            :type run_id: int
276
            :return: :class:`tcms.testplans.models.TestPlan` PK
277
            :rtype: int
278
        """
279
        result = self.rpc.TestRun.filter({'pk': run_id})
280
        if not result:
281
            product_id, product_name = self.get_product_id(0)
282
            version_id, version_name = self.get_version_id(product_id)
283
284
            name = self.prefix + 'Plan for %s (%s)' % (product_name, version_name)
285
286
            result = self.rpc.TestPlan.filter({'name': name,
287
                                               'product': product_id,
288
                                               'product_version': version_id})
289
290
            if not result:
291
                plan_type_id = self.get_plan_type_id()
292
293
                result = [self.rpc.TestPlan.create({
294
                    'name': name,
295
                    'text': 'Created by tcms_api.plugin_helpers.Backend',
296
                    'product': product_id,
297
                    'product_version': version_id,
298
                    'is_active': True,
299
                    'type': plan_type_id,
300
                })]
301
302
        return result[0]['plan_id']
303
304
    def get_run_id(self):
305
        """
306
            If `$TCMS_RUN_ID` is specified then assume the caller knows
307
            what they are doing and try to add test results to that TestRun.
308
            Otherwise will create a TestPlan and TestRun in which to record
309
            the results!
310
311
            .. warning::
312
313
                For internal use by `.configure()`!
314
315
            :return: :class:`tcms.testruns.models.TestRun` PK
316
            :rtype: int
317
        """
318
        run_id = os.environ.get('TCMS_RUN_ID')
319
320
        if not run_id:
321
            product_id, product_name = self.get_product_id(0)
322
            version_id, version_val = self.get_version_id(product_id)
323
            build_id, build_number = self.get_build_id(product_id, version_id)
324
            plan_id = self.get_plan_id(0)
325
            # the user issuing the request
326
            user_id = self.rpc.User.filter()[0]['id']
327
328
            testrun = self.rpc.TestRun.create({
329
                'summary': self.prefix + 'Results for %s, %s, %s' % (
330
                    product_name, version_val, build_number
331
                ),
332
                'manager': user_id,
333
                'plan': plan_id,
334
                'build': build_id,
335
            })
336
            run_id = testrun['run_id']
337
338
        # fetch pre-existing test cases in this TestRun
339
        # used to avoid adding existing TC to TR later
340
        for case in self.rpc.TestRun.get_cases(run_id):
341
            self._cases_in_test_run[case['case_id']] = case['case_run_id']
342
343
        return int(run_id)
344
345
    def test_case_get_or_create(self, summary):
346
        """
347
            Search for a TestCase with the specified `summary` and Product.
348
            If it doesn't exist in the database it will be created!
349
350
            .. important::
351
352
                Test runner plugins **must** call this method!
353
354
            :param summary: A TestCase summary
355
            :type summary: str
356
            :return: Serialized :class:`tcms.testcase.models.TestCase`
357
            :rtype: dict
358
        """
359
        test_case = self.rpc.TestCase.filter({
360
            'summary': summary,
361
            'category__product': self.product_id,
362
        })
363
364
        if not test_case:
365
            test_case = [self.rpc.TestCase.create({
366
                'summary': summary,
367
                'category': self.category_id,
368
                'product': self.product_id,
369
                'priority': self.priority_id,
370
                'case_status': self.confirmed_id,
371
                'notes': 'Created by tcms_api.plugin_helpers.Backend',
372
            })]
373
374
        return test_case[0]
375
376
    def add_test_case_to_plan(self, case_id, plan_id):
377
        """
378
            Add a TestCase to a TestPlan if it is not already there!
379
380
            .. important::
381
382
                Test runner plugins **must** call this method!
383
384
            :param case_id: :class:`tcms.testcases.models.TestCase` PK
385
            :type case_id: int
386
            :param plan_id: :class:`tcms.testplans.models.TestPlan` PK
387
            :type plan_id: int
388
            :return: None
389
        """
390
        if not self.rpc.TestCase.filter({'pk': case_id, 'plan': plan_id}):
391
            self.rpc.TestPlan.add_case(plan_id, case_id)
392
393
    def add_test_case_to_run(self, case_id, run_id):
394
        """
395
            Add a TestCase to a TestRun if it is not already there!
396
397
            .. important::
398
399
                Test runner plugins **must** call this method!
400
401
            :param case_id: :class:`tcms.testcases.models.TestCase` PK
402
            :type case_id: int
403
            :param run_id: :class:`tcms.testruns.models.TestRun` PK
404
            :type run_id: int
405
            :return: :class:`tcms.testruns.models.TestCaseRun` PK
406
            :rtype: int
407
        """
408
        if case_id in self._cases_in_test_run.keys():
409
            return self._cases_in_test_run[case_id]
410
411
        return self.rpc.TestRun.add_case(run_id, case_id)['case_run_id']
412
413
    def update_test_case_run(self, test_case_run_id, status_id, comment=None):
414
        """
415
            Update TestCaseRun with a status and comment.
416
417
            .. important::
418
419
                Test runner plugins **must** call this method!
420
421
            :param test_case_run_id: :class:`tcms.testruns.models.TestCaseRun` PK
422
            :type test_case_run_id: int
423
            :param status_id: :class:`tcms.testruns.models.TestCaseRunStatus` PK,
424
                              for example the ID for PASSED, FAILED, etc.
425
                              See :func:`tcms_api.tcms_api.plugin_helpers.Backend.get_status_id`
426
            :type status_id: int
427
            :param comment: the string to add as a comment, defaults to None
428
            :type comment: str
429
            :return: None
430
        """
431
        self.rpc.TestCaseRun.update(test_case_run_id,  # pylint: disable=objects-update-used
432
                                    {'status': status_id})
433
434
        if comment:
435
            self.add_comment(test_case_run_id, comment)
436
437
    def add_comment(self, test_case_run_id, comment):
438
        """
439
            Add comment string to TestCaseRun without changing the status
440
441
            .. important::
442
443
                Test runner plugins **must** call this method!
444
445
            :param test_case_run_id: :class:`tcms.testruns.models.TestCaseRun` PK
446
            :type test_case_run_id: int
447
            :param comment: the string to add as a comment
448
            :type comment: str
449
            :return: None
450
        """
451
        self.rpc.TestCaseRun.add_comment(test_case_run_id, comment)
452