Passed
Push — master ( 3727b7...72c322 )
by Alexander
02:28
created

tcms.testruns.models   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 360
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 43
eloc 248
dl 0
loc 360
rs 8.96
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
A TestRun.get_percentage() 0 7 2
A TestRun.add_cc() 0 4 1
A TestRun.update_completion_status() 0 5 2
A TestRun.remove_tag() 0 2 1
A TestRun.add_tag() 0 4 1
A TestRun._get_completed_case_run_percentage() 0 8 1
A TestRun.remove_cc() 0 2 1
A TestRun._get_total_case_run_num() 0 2 1
A TestRun.get_bug_count() 0 10 1
A TestRun.add_case_run() 0 22 3
A TestRun.get_notify_addrs() 0 18 4
A TestRun._get_absolute_url() 0 2 1
A TestRun.to_xmlrpc() 0 6 1
A TestRun.__str__() 0 2 1
B TestRun.stats_caseruns_status() 0 55 8
A TestExecution.remove_bug() 0 2 1
A TestExecution.get_bugs_count() 0 2 1
A TestExecution.__str__() 0 2 1
A TestExecution.add_bug() 0 9 1
A TestExecution.links() 0 5 1
A TestExecution.to_xmlrpc() 0 7 2
A TestExecution.get_bugs() 0 3 1
A TestExecution._get_absolute_url() 0 3 1
A TestExecutionStatus.get_names() 0 4 1
A TestExecutionStatus.__str__() 0 2 1
A TestExecutionStatus.icon() 0 3 2
A TestExecutionStatus.get_names_ids() 0 4 1

How to fix   Complexity   

Complexity

Complex classes like tcms.testruns.models 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
# -*- coding: utf-8 -*-
2
import datetime
3
from collections import namedtuple
4
5
from django.conf import settings
6
from django.urls import reverse
7
from django.db import models
8
from django.db.models import Count
9
from django.utils.translation import override
10
11
import vinaigrette
12
13
from tcms.core.models import TCMSActionModel
14
from tcms.core.history import KiwiHistoricalRecords
15
from tcms.core.contrib.linkreference.models import LinkReference
16
from tcms.testcases.models import Bug
17
from tcms.xmlrpc.serializer import TestExecutionXMLRPCSerializer
18
from tcms.xmlrpc.serializer import TestRunXMLRPCSerializer
19
from tcms.xmlrpc.utils import distinct_filter
20
21
22
TestExecutionStatusSubtotal = namedtuple('TestCaseRunStatusSubtotal', [
23
    'StatusSubtotal',
24
    'CaseRunsTotalCount',
25
    'CompletedPercentage',
26
    'FailurePercentage',
27
    'SuccessPercentage'])
28
29
30
class TestRun(TCMSActionModel):
31
    history = KiwiHistoricalRecords()
32
33
    run_id = models.AutoField(primary_key=True)
34
35
    # todo: this field should be removed in favor of plan.product_version
36
    # no longer shown in edit forms
37
    product_version = models.ForeignKey('management.Version', related_name='version_run',
38
                                        on_delete=models.CASCADE)
39
40
    start_date = models.DateTimeField(auto_now_add=True, db_index=True)
41
    stop_date = models.DateTimeField(null=True, blank=True, db_index=True)
42
    summary = models.TextField()
43
    notes = models.TextField(blank=True)
44
45
    plan = models.ForeignKey('testplans.TestPlan', related_name='run',
46
                             on_delete=models.CASCADE)
47
    build = models.ForeignKey('management.Build', related_name='build_run',
48
                              on_delete=models.CASCADE)
49
    manager = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='manager',
50
                                on_delete=models.CASCADE)
51
    default_tester = models.ForeignKey(settings.AUTH_USER_MODEL,
52
                                       null=True, blank=True,
53
                                       related_name='default_tester',
54
                                       on_delete=models.CASCADE)
55
56
    tag = models.ManyToManyField('management.Tag',
57
                                 through='testruns.TestRunTag',
58
                                 related_name='run')
59
60
    cc = models.ManyToManyField(settings.AUTH_USER_MODEL, through='testruns.TestRunCC')
61
62
    class Meta:
63
        unique_together = ('run_id', 'product_version')
64
65
    def __str__(self):
66
        return self.summary
67
68
    @classmethod
69
    def to_xmlrpc(cls, query=None):
70
        _query = query or {}
71
        qs = distinct_filter(TestRun, _query).order_by('pk')
72
        serializer = TestRunXMLRPCSerializer(model_class=cls, queryset=qs)
73
        return serializer.serialize_queryset()
74
75
    def _get_absolute_url(self):
76
        return reverse('testruns-get', args=[self.pk, ])
77
78
    def get_notify_addrs(self):
79
        """
80
        Get the all related mails from the run
81
        """
82
        send_to = [self.manager.email]
83
        send_to.extend(self.cc.values_list('email', flat=True))
84
        if self.default_tester_id:
85
            send_to.append(self.default_tester.email)
86
87
        for tcr in self.case_run.select_related('assignee').all():
88
            if tcr.assignee_id:
89
                send_to.append(tcr.assignee.email)
90
91
        send_to = set(send_to)
92
        # don't email author of last change
93
        send_to.discard(getattr(self.history.latest().history_user,  # pylint: disable=no-member
94
                                'email', ''))
95
        return list(send_to)
96
97
    # FIXME: rewrite to use multiple values INSERT statement
98
    def add_case_run(self, case, status=1, assignee=None,
99
                     case_text_version=None, build=None,
100
                     sortkey=0):
101
        _case_text_version = case_text_version
102
        if not _case_text_version:
103
            _case_text_version = case.history.latest().history_id
104
105
        _assignee = assignee \
106
            or (case.default_tester_id and case.default_tester) \
107
            or (self.default_tester_id and self.default_tester)
108
109
        _status = TestExecutionStatus.objects.get(id=status) \
110
            if isinstance(status, int) else status
111
112
        return self.case_run.create(case=case,
113
                                    assignee=_assignee,
114
                                    tested_by=None,
115
                                    status=_status,
116
                                    case_text_version=_case_text_version,
117
                                    build=build or self.build,
118
                                    sortkey=sortkey,
119
                                    close_date=None)
120
121
    def add_tag(self, tag):
122
        return TestRunTag.objects.get_or_create(
123
            run=self,
124
            tag=tag
125
        )
126
127
    def add_cc(self, user):
128
        return TestRunCC.objects.get_or_create(
129
            run=self,
130
            user=user,
131
        )
132
133
    def remove_tag(self, tag):
134
        TestRunTag.objects.filter(run=self, tag=tag).delete()
135
136
    def remove_cc(self, user):
137
        TestRunCC.objects.filter(run=self, user=user).delete()
138
139
    def get_bug_count(self):
140
        """
141
            Return the count of distinct bug numbers recorded for
142
            this particular TestRun.
143
        """
144
        # note fom Django docs: A count() call performs a SELECT COUNT(*)
145
        # behind the scenes !!!
146
        return Bug.objects.filter(
147
            case_run__run=self.pk
148
        ).values('bug_id').distinct().count()
149
150
    def get_percentage(self, count):
151
        case_run_count = self.total_num_caseruns
152
        if case_run_count == 0:
153
            return 0
154
        percent = float(count) / case_run_count * 100
155
        percent = round(percent, 2)
156
        return percent
157
158
    def _get_completed_case_run_percentage(self):
159
        ids = TestExecutionStatus.objects.filter(
160
            name__in=TestExecutionStatus.complete_status_names).values_list('pk', flat=True)
161
162
        completed_caserun = self.case_run.filter(
163
            status__in=ids)
164
165
        return self.get_percentage(completed_caserun.count())
166
167
    completed_case_run_percent = property(_get_completed_case_run_percentage)
168
169
    def _get_total_case_run_num(self):
170
        return self.case_run.count()
171
172
    total_num_caseruns = property(_get_total_case_run_num)
173
174
    def update_completion_status(self, is_finished):
175
        if is_finished:
176
            self.stop_date = datetime.datetime.now()
177
        else:
178
            self.stop_date = None
179
180
    @override('en')
181
    def stats_caseruns_status(self, statuses=None):
182
        """
183
            Get statistics based on case runs' status
184
185
            :param statuses: iterable object containing TestCaseRunStatus
186
                             objects representing PASS, FAIL, WAIVED, etc.
187
            :type statuses: iterable
188
            :return: the statistics including the number of each status mapping,
189
                     total number of case runs, complete percent, and failure percent.
190
            :rtype: namedtuple
191
        """
192
        if statuses is None:
193
            statuses = TestExecutionStatus.objects.only('pk', 'name').order_by('pk')
194
195
        rows = TestExecution.objects.filter(
196
            run=self.pk
197
        ).values(
198
            'status'
199
        ).annotate(status_count=Count('status'))
200
201
        caserun_statuses_subtotal = dict((status.pk, [0, status])
202
                                         for status in statuses)
203
204
        for row in rows:
205
            caserun_statuses_subtotal[row['status']][0] = row['status_count']
206
207
        complete_count = 0
208
        failure_count = 0
209
        caseruns_total_count = 0
210
211
        for _status_pk, total_info in caserun_statuses_subtotal.items():
212
            status_caseruns_count, caserun_status = total_info
213
            status_name = caserun_status.name
214
215
            caseruns_total_count += status_caseruns_count
216
217
            if status_name in TestExecutionStatus.complete_status_names:
218
                complete_count += status_caseruns_count
219
            if status_name in TestExecutionStatus.failure_status_names:
220
                failure_count += status_caseruns_count
221
222
        # Final calculation
223
        complete_percent = .0
224
        if caseruns_total_count:
225
            complete_percent = complete_count * 100.0 / caseruns_total_count
226
        failure_percent = .0
227
        if complete_count:
228
            failure_percent = failure_count * 100.0 / caseruns_total_count
229
230
        return TestExecutionStatusSubtotal(caserun_statuses_subtotal,
231
                                           caseruns_total_count,
232
                                           complete_percent,
233
                                           failure_percent,
234
                                           complete_percent - failure_percent)
235
236
237
class TestExecutionStatus(TCMSActionModel):
238
    FAILED = 'FAILED'
239
    BLOCKED = 'BLOCKED'
240
    PASSED = 'PASSED'
241
    IDLE = 'IDLE'
242
243
    _icons = {
244
        'IDLE': 'fa fa-question-circle-o',
245
        'RUNNING': 'fa fa-play-circle-o',
246
        'PAUSED': 'fa fa-pause-circle-o',
247
        PASSED: 'fa fa-check-circle-o',
248
        FAILED: 'fa fa-times-circle-o',
249
        BLOCKED: 'fa fa-stop-circle-o',
250
        'ERROR': 'fa fa-minus-circle',
251
        'WAIVED': 'fa fa-commenting-o',
252
    }
253
254
    complete_status_names = (PASSED, 'ERROR', FAILED, 'WAIVED')
255
    failure_status_names = ('ERROR', FAILED)
256
    idle_status_names = (IDLE,)
257
258
    id = models.AutoField(db_column='case_run_status_id', primary_key=True)
259
    name = models.CharField(max_length=60, blank=True, unique=True)
260
261
    def __str__(self):
262
        return self.name
263
264
    @classmethod
265
    def get_names(cls):
266
        """ Get all status names in mapping between id and name """
267
        return dict(cls.objects.values_list('pk', 'name'))
268
269
    @classmethod
270
    def get_names_ids(cls):
271
        """ Get all status names in reverse mapping between name and id """
272
        return dict((name, _id) for _id, name in cls.get_names().items())
273
274
    def icon(self):
275
        with override('en'):
276
            return self._icons[self.name]
277
278
279
# register model for DB translations
280
vinaigrette.register(TestExecutionStatus, ['name'])
281
282
283
class TestExecution(TCMSActionModel):
284
    history = KiwiHistoricalRecords()
285
286
    case_run_id = models.AutoField(primary_key=True)
287
    assignee = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
288
                                 related_name='case_run_assignee',
289
                                 on_delete=models.CASCADE)
290
    tested_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
291
                                  related_name='case_run_tester',
292
                                  on_delete=models.CASCADE)
293
    case_text_version = models.IntegerField()
294
    close_date = models.DateTimeField(null=True, blank=True)
295
    sortkey = models.IntegerField(null=True, blank=True)
296
297
    run = models.ForeignKey(TestRun, related_name='case_run', on_delete=models.CASCADE)
298
    case = models.ForeignKey('testcases.TestCase', related_name='case_run',
299
                             on_delete=models.CASCADE)
300
    status = models.ForeignKey(TestExecutionStatus, on_delete=models.CASCADE)
301
    build = models.ForeignKey('management.Build', on_delete=models.CASCADE)
302
303
    class Meta:
304
        unique_together = ('case', 'run', 'case_text_version')
305
306
    def links(self):
307
        """
308
            Returns all links attached to this object!
309
        """
310
        return LinkReference.objects.filter(test_case_run=self.pk)
311
312
    def __str__(self):
313
        return '%s: %s' % (self.pk, self.case_id)
314
315
    @classmethod
316
    def to_xmlrpc(cls, query: dict = None):
317
        if query is None:
318
            query = {}
319
        query_set = distinct_filter(TestExecution, query).order_by('pk')
320
        serializer = TestExecutionXMLRPCSerializer(model_class=cls, queryset=query_set)
321
        return serializer.serialize_queryset()
322
323
    def add_bug(self, bug_id, bug_system_id,
324
                summary=None, description=None, bz_external_track=False):
325
        return self.case.add_bug(
326
            bug_id=bug_id,
327
            bug_system_id=bug_system_id,
328
            summary=summary,
329
            description=description,
330
            case_run=self,
331
            bz_external_track=bz_external_track
332
        )
333
334
    def remove_bug(self, bug_id, run_id=None):
335
        self.case.remove_bug(bug_id=bug_id, run_id=run_id)
336
337
    def get_bugs(self):
338
        return Bug.objects.filter(
339
            case_run__case_run_id=self.case_run_id)
340
341
    def get_bugs_count(self):
342
        return self.get_bugs().count()
343
344
    def _get_absolute_url(self):
345
        # NOTE: this returns the URL to the TestRun containing this TestCaseRun!
346
        return reverse('testruns-get', args=[self.run_id])
347
348
349
class TestRunTag(models.Model):
350
    tag = models.ForeignKey('management.Tag', on_delete=models.CASCADE)
351
    run = models.ForeignKey(TestRun, related_name='tags', on_delete=models.CASCADE)
352
353
354
class TestRunCC(models.Model):
355
    run = models.ForeignKey(TestRun, related_name='cc_list', on_delete=models.CASCADE)
356
    user = models.ForeignKey(settings.AUTH_USER_MODEL, db_column='who', on_delete=models.CASCADE)
357
358
    class Meta:
359
        unique_together = ('run', 'user')
360