tcms.testruns.models   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 335
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 36
eloc 218
dl 0
loc 335
rs 9.52
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A TestRun.add_cc() 0 4 1
A TestRun.__str__() 0 2 1
A TestExecutionStatus.__str__() 0 2 1
A TestRun.get_absolute_url() 0 2 1
A Environment.get_absolute_url() 0 2 1
B TestRun.property_matrix() 0 25 6
A TestExecution.__str__() 0 2 1
A TestExecution.properties() 0 2 1
A TestExecution.actual_duration() 0 5 3
A TestRun.remove_tag() 0 2 1
A TestExecution.links() 0 2 1
A TestRun.get_notify_addrs() 0 23 4
A TestRun._create_single_execution() 0 12 1
A TestRun.add_tag() 0 2 1
A TestRun._get_absolute_url() 0 5 1
A TestRun.create_execution() 0 37 4
A Environment.__str__() 0 2 1
A TestExecution.get_bugs() 0 2 1
A TestExecution._get_absolute_url() 0 3 1
A TestRun.remove_cc() 0 2 1
A TestRun.stats_executions_status() 0 24 2
A Environment._get_absolute_url() 0 5 1
1
# -*- coding: utf-8 -*-
2
import itertools
3
from collections import OrderedDict, namedtuple
4
5
import vinaigrette
6
from allpairspy import AllPairs
7
from colorfield.fields import ColorField
8
from django.conf import settings
9
from django.db import models
10
from django.urls import reverse
11
from django.utils.translation import gettext_lazy as _
12
from django.utils.translation import override
13
14
from tcms.core.contrib.linkreference.models import LinkReference
15
from tcms.core.history import KiwiHistoricalRecords
16
from tcms.core.models import abstract
17
from tcms.core.models.base import UrlMixin
18
19
TestExecutionStatusSubtotal = namedtuple(
20
    "TestExecutionStatusSubtotal",
21
    [
22
        "CompletedPercentage",
23
        "FailurePercentage",
24
        "SuccessPercentage",
25
    ],
26
)
27
28
29
class TestRun(models.Model, UrlMixin):
30
    history = KiwiHistoricalRecords()
31
32
    start_date = models.DateTimeField(db_index=True, null=True, blank=True)
33
    stop_date = models.DateTimeField(null=True, blank=True, db_index=True)
34
    planned_start = models.DateTimeField(db_index=True, null=True, blank=True)
35
    planned_stop = models.DateTimeField(db_index=True, null=True, blank=True)
36
37
    summary = models.TextField()
38
    notes = models.TextField(blank=True)
39
40
    plan = models.ForeignKey(
41
        "testplans.TestPlan", related_name="run", on_delete=models.CASCADE
42
    )
43
    build = models.ForeignKey(
44
        "management.Build", related_name="build_run", on_delete=models.CASCADE
45
    )
46
    manager = models.ForeignKey(
47
        settings.AUTH_USER_MODEL, related_name="manager", on_delete=models.CASCADE
48
    )
49
    default_tester = models.ForeignKey(
50
        settings.AUTH_USER_MODEL,
51
        null=True,
52
        blank=True,
53
        related_name="default_tester",
54
        on_delete=models.CASCADE,
55
    )
56
57
    tag = models.ManyToManyField(
58
        "management.Tag", through="testruns.TestRunTag", related_name="run"
59
    )
60
61
    cc = models.ManyToManyField(settings.AUTH_USER_MODEL, through="testruns.TestRunCC")
62
63
    def __str__(self):
64
        return self.summary
65
66
    def _get_absolute_url(self):
67
        return reverse(
68
            "testruns-get",
69
            args=[
70
                self.pk,
71
            ],
72
        )
73
74
    def get_absolute_url(self):
75
        return self._get_absolute_url()
76
77
    def get_notify_addrs(self):
78
        """
79
        Get the all related mails from the run
80
        """
81
        send_to = [self.manager.email]
82
        send_to.extend(self.cc.values_list("email", flat=True))
83
        if self.default_tester_id:
84
            send_to.append(self.default_tester.email)
85
86
        for execution in self.executions.select_related("assignee").all():
87
            if execution.assignee_id:
88
                send_to.append(execution.assignee.email)
89
90
        send_to = set(send_to)
91
        # don't email author of last change
92
        send_to.discard(
93
            getattr(
94
                self.history.latest().history_user,  # pylint: disable=no-member
95
                "email",
96
                "",
97
            )
98
        )
99
        return list(send_to)
100
101
    def _create_single_execution(self, case, assignee, build, sortkey):
102
        return self.executions.create(
103
            case=case,
104
            assignee=assignee,
105
            tested_by=None,
106
            # usually IDLE but users can customize statuses
107
            status=TestExecutionStatus.objects.filter(weight=0).first(),
108
            case_text_version=case.history.latest().history_id,
109
            build=build or self.build,
110
            sortkey=sortkey,
111
            stop_date=None,
112
            start_date=None,
113
        )
114
115
    def create_execution(  # pylint: disable=too-many-arguments
116
        self,
117
        case,
118
        assignee=None,
119
        build=None,
120
        sortkey=0,
121
        matrix_type="full",
122
    ):
123
        # pylint: disable=import-outside-toplevel
124
        from tcms.testcases.models import Property as TestCaseProperty
125
126
        assignee = (
127
            assignee
128
            or (case.default_tester_id and case.default_tester)
129
            or (self.default_tester_id and self.default_tester)
130
        )
131
132
        executions = []
133
        properties = self.property_set.union(TestCaseProperty.objects.filter(case=case))
134
135
        if properties.count():
136
            for prop_tuple in self.property_matrix(properties, matrix_type):
137
                execution = self._create_single_execution(
138
                    case, assignee, build, sortkey
139
                )
140
                executions.append(execution)
141
142
                for prop in prop_tuple:
143
                    TestExecutionProperty.objects.create(
144
                        execution=execution, name=prop.name, value=prop.value
145
                    )
146
        else:
147
            executions.append(
148
                self._create_single_execution(case, assignee, build, sortkey)
149
            )
150
151
        return executions
152
153
    @staticmethod
154
    def property_matrix(properties, _type="full"):
155
        """
156
        Return a sequence of tuples representing the property matrix!
157
        """
158
        property_groups = OrderedDict()
159
        for prop in properties.order_by("name", "value"):
160
            if prop.name in property_groups:
161
                property_groups[prop.name].append(prop)
162
            else:
163
                property_groups[prop.name] = [prop]
164
165
        if _type == "full":
166
            return itertools.product(*property_groups.values())
167
168
        if _type == "pairwise":
169
            # AllPairs returns named tuples which require valid identifiers.
170
            # Rename all keys b/c we don't use them for storing data in DB anyway
171
            for _i, key in enumerate(property_groups.copy()):
172
                property_groups[f"key_{_i}"] = property_groups.pop(key)
173
174
            # Note: in Python 3.10 there is itertools.pairwise() function
175
            return AllPairs(property_groups)
176
177
        raise RuntimeError(f"Unknown matrix type '{_type}'")
178
179
    def add_tag(self, tag):
180
        return TestRunTag.objects.get_or_create(run=self, tag=tag)
181
182
    def add_cc(self, user):
183
        return TestRunCC.objects.get_or_create(
184
            run=self,
185
            user=user,
186
        )
187
188
    def remove_tag(self, tag):
189
        TestRunTag.objects.filter(run=self, tag=tag).delete()
190
191
    def remove_cc(self, user):
192
        TestRunCC.objects.filter(run=self, user=user).delete()
193
194
    @override("en")
195
    def stats_executions_status(self):
196
        """
197
        Get statistics based on executions' status
198
199
        :return: the statistics including the number of each status mapping,
200
                 total number of executions, complete percent, and failure percent.
201
        :rtype: namedtuple
202
        """
203
        total_count = self.executions.count()
204
        if total_count:
205
            complete_count = self.executions.exclude(status__weight=0).count()
206
            complete_percent = complete_count * 100.0 / total_count
207
208
            failing_count = self.executions.filter(status__weight__lt=0).count()
209
            failing_percent = failing_count * 100.0 / total_count
210
        else:
211
            complete_percent = 0.0
212
            failing_percent = 0.0
213
214
        return TestExecutionStatusSubtotal(
215
            complete_percent,
216
            failing_percent,
217
            complete_percent - failing_percent,
218
        )
219
220
221
class TestExecutionStatus(models.Model, UrlMixin):
222
    class Meta:
223
        # used in the admin view
224
        verbose_name_plural = _("Test execution statuses")
225
226
    name = models.CharField(max_length=60, blank=True, unique=True)
227
    weight = models.IntegerField(default=0)
228
    icon = models.CharField(max_length=64)
229
    color = ColorField()
230
231
    def __str__(self):
232
        return self.name
233
234
235
# register model for DB translations
236
vinaigrette.register(TestExecutionStatus, ["name"])
237
238
239
class TestExecution(models.Model, UrlMixin):
240
    history = KiwiHistoricalRecords()
241
242
    assignee = models.ForeignKey(
243
        settings.AUTH_USER_MODEL,
244
        blank=True,
245
        null=True,
246
        related_name="execution_assignee",
247
        on_delete=models.CASCADE,
248
    )
249
    tested_by = models.ForeignKey(
250
        settings.AUTH_USER_MODEL,
251
        blank=True,
252
        null=True,
253
        related_name="execution_tester",
254
        on_delete=models.CASCADE,
255
    )
256
    case_text_version = models.IntegerField()
257
    start_date = models.DateTimeField(null=True, blank=True, db_index=True)
258
    stop_date = models.DateTimeField(null=True, blank=True, db_index=True)
259
    sortkey = models.IntegerField(null=True, blank=True)
260
261
    run = models.ForeignKey(
262
        TestRun, related_name="executions", on_delete=models.CASCADE
263
    )
264
    case = models.ForeignKey(
265
        "testcases.TestCase", related_name="executions", on_delete=models.CASCADE
266
    )
267
    status = models.ForeignKey(TestExecutionStatus, on_delete=models.CASCADE)
268
    build = models.ForeignKey("management.Build", on_delete=models.CASCADE)
269
270
    def __str__(self):
271
        return f"{self.pk}: {self.case_id}"
272
273
    def links(self):
274
        return LinkReference.objects.filter(execution=self.pk)
275
276
    def get_bugs(self):
277
        return self.links().filter(is_defect=True)
278
279
    def _get_absolute_url(self):
280
        # NOTE: this returns the URL to the TestRun containing this TestExecution!
281
        return reverse("testruns-get", args=[self.run_id])
282
283
    @property
284
    def actual_duration(self):
285
        if self.stop_date is None or self.start_date is None:
286
            return None
287
        return self.stop_date - self.start_date
288
289
    def properties(self):
290
        return TestExecutionProperty.objects.filter(execution=self.pk)
291
292
293
class TestExecutionProperty(abstract.Property):
294
    execution = models.ForeignKey(TestExecution, on_delete=models.CASCADE)
295
296
297
class TestRunTag(models.Model):
298
    tag = models.ForeignKey("management.Tag", on_delete=models.CASCADE)
299
    run = models.ForeignKey(TestRun, related_name="tags", on_delete=models.CASCADE)
300
301
302
class TestRunCC(models.Model):
303
    run = models.ForeignKey(TestRun, related_name="cc_list", on_delete=models.CASCADE)
304
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
305
306
    class Meta:
307
        unique_together = ("run", "user")
308
309
310
class Environment(models.Model):
311
    name = models.CharField(unique=True, max_length=255)
312
    description = models.TextField(blank=True)
313
314
    def _get_absolute_url(self):
315
        return reverse(
316
            "testruns-environment",
317
            args=[
318
                self.pk,
319
            ],
320
        )
321
322
    def get_absolute_url(self):
323
        return self._get_absolute_url()
324
325
    def __str__(self):
326
        return f"{self.name}"
327
328
329
class EnvironmentProperty(abstract.Property):
330
    environment = models.ForeignKey(Environment, on_delete=models.CASCADE)
331
332
333
class Property(abstract.Property):
334
    run = models.ForeignKey(TestRun, on_delete=models.CASCADE)
335