Coverage for CIResults / tests / test_models.py: 100%
973 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-04 08:33 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-04 08:33 +0000
1from unittest.mock import patch, MagicMock, PropertyMock
2from django.test import TestCase
3from django.utils import timezone
4from django.core.exceptions import ValidationError
5from django.contrib.auth import get_user_model
7from CIResults.models import BugTracker, Bug, BugTrackerSLA, Person, BugTrackerAccount, BugComment
8from CIResults.models import Test, Machine, RunConfigTag, RunConfig, TestSuite, TextStatus
9from CIResults.models import TestResult, IssueFilter, IssueFilterAssociated, Issue, RunFilterStatistic
10from CIResults.models import MachineTag, TestsuiteRun, UnknownFailure, KnownFailure, Component, Build
11from CIResults.models import Rate, ReplicationScript, script_validator
13from model_bakery import baker
15import datetime
16import pytz
19class BugTrackerSLATests(TestCase):
20 def test___str__(self):
21 tracker = BugTracker(name="BugTracker")
22 entry = BugTrackerSLA(tracker=tracker, priority="high", SLA=datetime.timedelta(weeks=10, seconds=23))
23 self.assertEqual(str(entry), "BugTracker: high -> 70 days, 0:00:23")
26class PersonTests(TestCase):
27 def test_full(self):
28 p = Person(full_name="John Doe", email="john.doe@isp.earth")
29 self.assertEqual(str(p), "John Doe <john.doe@isp.earth>")
31 def test_full_name_only_only(self):
32 p = Person(full_name="John Doe")
33 self.assertEqual(str(p), "John Doe")
35 def test_email_only(self):
36 p = Person(email="john.doe@isp.earth")
37 self.assertEqual(str(p), "john.doe@isp.earth")
39 def test_no_information(self):
40 p = Person()
41 self.assertEqual(str(p), "(No name or email)")
44class TestBugTrackerAccount(TestCase):
45 def test_str(self):
46 person = Person(full_name="John Doe", email="john.doe@isp.earth")
47 self.assertEqual(str(BugTrackerAccount(person=person)), str(person))
50class BugTrackerTests(TestCase):
51 def test_str(self):
52 tracker = BugTracker(name="Freedesktop.org", short_name="fdo",
53 separator="#", public=True,
54 url="https://bugs.freedesktop.org/",
55 bug_base_url="https://bugs.freedesktop.org/show_bug.cgi?id=")
56 self.assertEqual(str(tracker), "Freedesktop.org")
58 @patch('CIResults.models.BugTrackerSLA.objects.filter',
59 return_value=[MagicMock(priority="P1", SLA=datetime.timedelta(seconds=1)),
60 MagicMock(priority="P2", SLA=datetime.timedelta(seconds=3)),
61 MagicMock(priority="P3", SLA=datetime.timedelta(seconds=2))])
62 def test_SLAs_cached(self, filter_mocked):
63 tracker = BugTracker(name="Tracker1", public=True)
64 slas = tracker.SLAs_cached
66 filter_mocked.assert_called_with(tracker=tracker)
67 self.assertEqual(slas, {"p1": filter_mocked.return_value[0].SLA,
68 "p2": filter_mocked.return_value[1].SLA,
69 "p3": filter_mocked.return_value[2].SLA})
71 @patch('CIResults.models.BugTracker.tracker')
72 def test_poll(self, tracker_mock):
73 bug = MagicMock()
74 bug.save = MagicMock()
75 tracker = BugTracker()
76 tracker.tracker._get_tracker_time = MagicMock(return_value=timezone.now())
77 tracker.poll(bug)
78 tracker_mock.poll.assert_called_with(bug, False)
79 bug.save.assert_not_called()
80 self.assertLess(timezone.now() - bug.polled, datetime.timedelta(seconds=.1))
82 @patch('CIResults.bugtrackers.Bugzilla')
83 def test_tracker__bugzilla(self, bugzilla_mock):
84 self.assertEqual(BugTracker(tracker_type="bugzilla").tracker, bugzilla_mock.return_value)
86 @patch('CIResults.bugtrackers.Jira')
87 def test_tracker__jira(self, jira_mock):
88 self.assertEqual(BugTracker(tracker_type="jira").tracker, jira_mock.return_value)
90 @patch('CIResults.bugtrackers.GitLab')
91 def test_tracker__gitlab(self, gitlab_mock):
92 self.assertEqual(BugTracker(tracker_type="gitlab").tracker, gitlab_mock.return_value)
94 @patch('CIResults.bugtrackers.Untracked')
95 def test_tracker__jira_untracked(self, untracked_mock):
96 self.assertEqual(BugTracker(tracker_type="jira_untracked").tracker, untracked_mock.return_value)
98 def test_tracker__invalid_name(self):
99 tracker = BugTracker(tracker_type="invalid_name")
100 self.assertRaisesMessage(ValueError, "The bugtracker type 'invalid_name' is unknown",
101 getattr, tracker, "tracker")
103 @patch.object(BugTracker, 'tracker', PropertyMock())
104 def test_open_statuses(self):
105 tracker = BugTracker(tracker_type="tracker")
106 self.assertEqual(tracker.open_statuses, BugTracker.tracker.open_statuses)
108 @patch('CIResults.models.Bug.objects.filter', return_value=[MagicMock(), MagicMock(), MagicMock()])
109 @patch('CIResults.models.BugTracker.poll')
110 def test_poll_all(self, poll_mocked, bugs_mocked):
111 tracker = BugTracker(tracker_type="untracked", public=True)
112 tracker.poll_all()
114 # Check that filter got called with the right argument
115 bugs_mocked.assert_called_with(tracker=tracker)
117 # Check that every bug we set up got polled
118 self.assertEqual(poll_mocked.call_count, 3)
119 for i in range(3):
120 poll_mocked.assert_any_call(bugs_mocked.return_value[i])
122 self.assertLess(abs((timezone.now() - tracker.polled).total_seconds()), .1)
124 @patch('CIResults.models.BugTracker.poll')
125 def test_poll_all__custom_list(self, poll_mocked):
126 bugs = [MagicMock(), MagicMock(), MagicMock()]
128 tracker = BugTracker(tracker_type="untracked", public=True)
129 tracker.poll_all(bugs=bugs)
131 # Check that every bug we set up got polled
132 self.assertEqual(poll_mocked.call_count, 3)
133 for i in range(3):
134 poll_mocked.assert_any_call(bugs[i])
136 self.assertLess(abs((timezone.now() - tracker.polled).total_seconds()), 1)
138 @patch('CIResults.models.Bug.objects.filter', return_value=[MagicMock(), MagicMock(), MagicMock()])
139 @patch('CIResults.models.BugTracker.poll')
140 def test_poll_all_interrupt(self, poll_mocked, bugs_mocked):
141 tracker = BugTracker()
143 stop_event = MagicMock(is_set=MagicMock(side_effect=[False, False, True]))
144 tracker.poll_all(stop_event)
146 # Check that filter got called with the right argument
147 bugs_mocked.assert_called_with(tracker=tracker)
149 # Check that every bug we set up got polled
150 self.assertEqual(poll_mocked.call_count, 2)
151 for i in range(2):
152 poll_mocked.assert_any_call(bugs_mocked.return_value[i])
154 def test_components_followed_list(self):
155 tracker = BugTracker(public=True)
156 self.assertEqual(tracker.components_followed_list, [])
158 tracker.components_followed = "COMPONENT1,COMPONENT2, COMPONENT3"
159 self.assertEqual(tracker.components_followed_list, ["COMPONENT1", "COMPONENT2", "COMPONENT3"])
161 def test_get_or_create_bugs(self):
162 tracker = BugTracker.objects.create(name='tracker', tracker_type='jira_untracked', public=True)
163 Bug.objects.create(tracker=tracker, bug_id='1')
164 Bug.objects.create(tracker=tracker, bug_id='3')
166 self.assertEqual(len(Bug.objects.all()), 2)
167 tracker.get_or_create_bugs(set(['1', '2', '3']))
168 self.assertEqual(len(Bug.objects.all()), 3)
170 @patch('CIResults.models.BugTracker.open_statuses', new_callable=PropertyMock)
171 def test_is_bug_open(self, open_statuses_mock):
172 open_statuses_mock.return_value = ["status1", "status3"]
173 tracker = BugTracker(public=True)
174 bug_status1 = Bug(tracker=tracker, status="status1")
175 bug_status2 = Bug(tracker=tracker, status="status2")
176 bug_status3 = Bug(tracker=tracker, status="status3")
177 bug_no_status = Bug(tracker=tracker, status=None)
179 for bug, is_open in [(bug_status1, True), (bug_status2, False), (bug_status3, True),
180 (bug_no_status, False)]:
181 self.assertEqual(tracker.is_bug_open(bug), is_open,
182 "{}: Should be opened? {}".format(bug.status, is_open))
184 @patch('CIResults.models.Bug.objects.filter', return_value=set([MagicMock(spec=Bug, bug_id='existing1'),
185 MagicMock(spec=Bug, bug_id='existing2')]))
186 def test_open_bugs__without_followed_list(self, bugs_filter_mocked):
187 tracker = BugTracker.objects.create(name='tracker', tracker_type='jira_untracked', public=True)
189 self.assertEqual(tracker.open_bugs(), bugs_filter_mocked.return_value)
190 bugs_filter_mocked.assert_called_with(tracker=tracker, status__in=tracker.open_statuses)
192 @patch('CIResults.models.BugTracker.get_or_create_bugs', return_value=set([MagicMock(spec=Bug,
193 bug_id='searched_bug')]))
194 @patch('CIResults.bugtrackers.Untracked.search_bugs_ids')
195 @patch('CIResults.models.Bug.objects.filter', return_value=set([MagicMock(spec=Bug, bug_id='existing1'),
196 MagicMock(spec=Bug, bug_id='existing2')]))
197 def test_open_bugs__with_followed_list(self, bugs_filter_mocked, search_bugs_ids_mocked,
198 get_or_create_bugs_mocked):
199 tracker = BugTracker.objects.create(name='tracker', tracker_type='jira_untracked', public=True,
200 components_followed="product1,product2")
202 self.assertEqual(tracker.open_bugs(), bugs_filter_mocked.return_value | get_or_create_bugs_mocked.return_value)
203 bugs_filter_mocked.assert_called_with(tracker=tracker, status__in=tracker.open_statuses)
204 search_bugs_ids_mocked.assert_called_with(components=["product1", "product2"], status=tracker.open_statuses)
206 @patch('CIResults.models.BugTracker.open_bugs')
207 def test_followed_bugs(self, open_bugs_mock):
208 tracker1 = BugTracker.objects.create(name='tracker1', tracker_type='jira_untracked', public=True)
209 tracker2 = BugTracker.objects.create(name='tracker2', tracker_type='jira_untracked', public=True)
211 # Create the "open bugs"
212 open_bugs_mock.return_value = set([Bug.objects.create(bug_id='1234', tracker=tracker1),
213 Bug.objects.create(bug_id='1235', tracker=tracker1)])
215 # Create the bugs associated to the issues
216 bug1 = Bug.objects.create(bug_id='1', tracker=tracker1)
217 bug2 = Bug.objects.create(bug_id='2', tracker=tracker2)
218 bug_old_issue = Bug.objects.create(bug_id='OLD_ISSUE', tracker=tracker1)
219 Bug.objects.create(bug_id='UNREFERENCED', tracker=tracker2)
221 active = Issue.objects.create()
222 active.bugs.add(bug1, bug2)
224 archived = Issue.objects.create(archived_on=timezone.now())
225 archived.bugs.add(bug2, bug_old_issue)
227 # Make sure that only the expected bugs are returned for each tracker
228 self.assertEqual(tracker1.followed_bugs(), set([bug1]) | open_bugs_mock.return_value)
229 self.assertEqual(tracker2.followed_bugs(), set([bug2]) | open_bugs_mock.return_value)
231 @patch('CIResults.models.BugTracker.get_or_create_bugs')
232 @patch('CIResults.bugtrackers.Untracked.search_bugs_ids')
233 @patch('CIResults.models.BugTracker.bugs_in_issues')
234 @patch('CIResults.models.timezone')
235 def test_updated_bugs(self, tz_mock, bugs_in_issues_mock, search_bugs_mock, get_or_create_mock):
236 tracker1 = BugTracker.objects.create(name='tracker1', tracker_type='jira_untracked',
237 public=True, components_followed="FOO")
238 tracker1.tracker.open_statuses = ["Open"]
239 tracker1.polled = datetime.datetime.fromtimestamp(0, pytz.utc)
240 tracker1.tracker._get_tracker_time = MagicMock(return_value=tracker1.polled + datetime.timedelta(seconds=20))
241 tz_mock.now.return_value = tracker1.polled + datetime.timedelta(seconds=10)
243 all_upd_bugs_ids = set(["bug2", "bug3", "bug4"])
244 open_bugs_ids = set(["bug1", "bug3"])
246 issue_bug1 = Bug.objects.create(bug_id='bug4', status="Closed", tracker=tracker1)
247 issue_bug2 = Bug.objects.create(bug_id='bug5', status="Closed", tracker=tracker1)
249 Bug.objects.create(bug_id='bug1', status="Open", tracker=tracker1)
250 Bug.objects.create(bug_id='bug2', status="Closed", tracker=tracker1)
251 Bug.objects.create(bug_id='bug3', status="Open", tracker=tracker1)
253 search_bugs_mock.side_effect = [all_upd_bugs_ids,
254 open_bugs_ids]
256 bugs_in_issues_mock.return_value = set([issue_bug1, issue_bug2])
257 tracker1.updated_bugs()
258 self.assertEqual(datetime.datetime.fromtimestamp(0, pytz.utc) + datetime.timedelta(seconds=10),
259 search_bugs_mock.call_args_list[0][1]['updated_since'])
261 """
262 bug1 - (not returned) open but not updated
263 bug2 - (not returned) updated but not open
264 bug3 - (returned) updated and open
265 bug4 - (returned) not open, but updated and associated to issue
266 bug5 - (not returned) not open or updated
267 """
268 get_or_create_mock.assert_called_with(set(["bug3", "bug4"]))
270 @patch('CIResults.models.BugTracker.get_or_create_bugs')
271 @patch('CIResults.bugtrackers.Untracked.search_bugs_ids')
272 @patch('CIResults.models.BugTracker.bugs_in_issues')
273 @patch('CIResults.models.timezone')
274 def test_unreplicated_bugs(self, tz_mock, bugs_in_issues_mock, search_bugs_mock, get_or_create_mock):
275 tracker1 = BugTracker.objects.create(name='tracker1', tracker_type='jira_untracked',
276 public=True, components_followed="FOO")
277 tracker1.tracker.open_statuses = ["Open"]
279 issue_bug1 = Bug.objects.create(bug_id='bug1', status="Open", tracker=tracker1)
280 issue_bug2 = Bug.objects.create(bug_id='bug2', status="Closed", tracker=tracker1)
282 Bug.objects.create(bug_id='bug3', status="Open", tracker=tracker1, parent=issue_bug1)
283 Bug.objects.create(bug_id='bug4', status="Closed", tracker=tracker1, parent=issue_bug2)
284 Bug.objects.create(bug_id='bug5', status="Open", tracker=tracker1)
286 tracker1.unreplicated_bugs()
287 get_or_create_mock.assert_called_with(set(["bug5"]))
290class BugTests(TestCase):
291 Model = Bug
293 def setUp(self):
294 self.tracker = BugTracker(name="Freedesktop.org", short_name="fdo",
295 separator="#", public=True,
296 url="https://bugs.freedesktop.org/",
297 bug_base_url="https://bugs.freedesktop.org/show_bug.cgi?id=")
298 self.bug = Bug(tracker=self.tracker, bug_id="1234", title="random title",
299 created=timezone.now() - datetime.timedelta(days=4))
301 def test_short_name(self):
302 self.assertEqual(self.bug.short_name, "fdo#1234")
304 def test_url(self):
305 self.assertEqual(self.bug.url,
306 "https://bugs.freedesktop.org/show_bug.cgi?id=1234")
308 def test_features_list(self):
309 self.assertEqual(self.bug.features_list, [])
310 self.bug.features = "feature1,feature2 , feature3"
311 self.assertEqual(self.bug.features_list, ["feature1", "feature2", "feature3"])
313 def test_platforms_list(self):
314 self.assertEqual(self.bug.platforms_list, [])
315 self.bug.platforms = "platform1,platform2 , platform3"
316 self.assertEqual(self.bug.platforms_list, ["platform1", "platform2", "platform3"])
318 def test_tags_list(self):
319 self.assertEqual(self.bug.tags_list, [])
320 self.bug.tags = "tag1,tag 2 , tag3"
321 self.assertEqual(self.bug.tags_list, ["tag1", "tag 2", "tag3"])
323 def test_has_new_comments(self):
324 self.bug.updated = timezone.now()
326 # comments_polled = None
327 self.assertTrue(self.bug.has_new_comments)
329 # comments_polled < updated
330 self.bug.comments_polled = self.bug.updated - datetime.timedelta(seconds=1)
331 self.assertTrue(self.bug.has_new_comments)
333 # comments_polled > updated
334 self.bug.comments_polled = self.bug.updated + datetime.timedelta(seconds=1)
335 self.assertFalse(self.bug.has_new_comments)
337 def test_add_first_seen_in(self):
338 runconfig1 = RunConfig.objects.create(name="run1", temporary=True)
339 runconfig2 = RunConfig.objects.create(name="run2", temporary=False)
340 testsuite = TestSuite.objects.create(name="testsuite1", public=True)
341 machine = Machine.objects.create(name="machine", public=True)
342 ts_run = TestsuiteRun.objects.create(
343 testsuite=testsuite,
344 runconfig=runconfig1,
345 machine=machine,
346 run_id=0,
347 start="2023-12-29 12:00",
348 duration=datetime.timedelta(days=1),
349 )
350 test = Test.objects.create(name="test", testsuite=testsuite, public=True)
351 status = TextStatus.objects.create(name="status", testsuite=testsuite)
352 test_result = TestResult.objects.create(
353 test=test,
354 ts_run=ts_run,
355 status=status,
356 stdout="h\n{} stdout1234\nYoo!".format(status.name),
357 stderr="h\n{} stderr1234\nasdf".format(status.name),
358 dmesg="h\n{} dmesg1234\nqwer".format(status.name),
359 start="2023-12-29 12:00",
360 duration=datetime.timedelta(days=1),
361 )
362 issue = Issue.objects.create()
363 filter = IssueFilter.objects.create(description="abc")
364 ifa = IssueFilterAssociated.objects.create(issue=issue, filter=filter)
365 KnownFailure.objects.create(result=test_result, matched_ifa=ifa)
366 self.bug.add_first_seen_in(issue, False)
367 self.assertEqual(self.bug.first_seen_in, runconfig1)
369 ts_run2 = TestsuiteRun.objects.create(
370 testsuite=testsuite,
371 runconfig=runconfig2,
372 machine=machine,
373 run_id=0,
374 start="2023-12-29 12:00",
375 duration=datetime.timedelta(days=1),
376 )
377 test2 = Test.objects.create(name="test2", testsuite=testsuite, public=True)
378 test_result2 = TestResult.objects.create(
379 test=test2,
380 ts_run=ts_run2,
381 status=status,
382 stdout="h\n{} stdout1234\nYoo!".format(status.name),
383 stderr="h\n{} stderr1234\nasdf".format(status.name),
384 dmesg="h\n{} dmesg1234\nqwer".format(status.name),
385 start="2023-12-29 12:00",
386 duration=datetime.timedelta(days=1),
387 )
388 issue2 = Issue.objects.create()
389 filter2 = IssueFilter.objects.create(description="abc")
390 ifa2 = IssueFilterAssociated.objects.create(issue=issue2, filter=filter2)
391 KnownFailure.objects.create(result=test_result2, matched_ifa=ifa2)
392 self.bug.add_first_seen_in(issue2, False)
393 self.assertEqual(self.bug.first_seen_in, runconfig1)
395 @patch('CIResults.models.BugComment.objects.filter')
396 def test_comments_cached(self, filter_mock):
397 self.assertEqual(self.bug.comments_cached, filter_mock.return_value.prefetch_related())
398 filter_mock.assert_called_with(bug=self.bug)
400 def test_SLA(self):
401 tracker = BugTracker.objects.create(name="BugTracker", public=True)
402 sla_high = BugTrackerSLA.objects.create(tracker=tracker, priority="HIGH", SLA=datetime.timedelta(seconds=23))
403 sla_low = BugTrackerSLA.objects.create(tracker=tracker, priority="low", SLA=datetime.timedelta(seconds=30))
405 self.assertEqual(tracker.SLAs_cached, {"low": sla_low.SLA, "high": sla_high.SLA})
407 bug = Bug(tracker=tracker, priority="low")
408 self.assertEqual(bug.SLA, sla_low.SLA)
410 bug = Bug(tracker=tracker, priority="LOW")
411 self.assertEqual(bug.SLA, sla_low.SLA)
413 bug = Bug(tracker=tracker, priority="invalid")
414 self.assertEqual(bug.SLA, datetime.timedelta.max)
416 def test_SLA_deadline__triage_needed(self):
417 # Check that if no developer has updated the bug, our deadline is set to the tracker's first_response_SLA
418 self.tracker.save()
419 self.bug.save()
420 self.tracker.first_response_SLA = datetime.timedelta(days=2.1)
421 self.assertEqual(self.bug.SLA_deadline, self.bug.created + self.tracker.first_response_SLA)
423 def test_SLA_deadline__normal_SLA(self):
424 # Check that when we have a developer comment, we follow the SLA
425 self.bug.SLA = datetime.timedelta(days=1)
426 self.bug.last_updated_by_developer = timezone.now()
427 self.assertAlmostEqual(self.bug.SLA_deadline.timestamp(),
428 (self.bug.last_updated_by_developer + self.bug.SLA).timestamp(),
429 places=1)
431 def test_SLA_deadline__infinite_SLA(self):
432 # In the event where the SLA is infine, verify that we always set the deadline a year in advance
433 self.bug.SLA = datetime.timedelta.max
434 self.bug.last_updated_by_developer = timezone.now() - datetime.timedelta(days=30)
435 self.assertAlmostEqual(self.bug.SLA_deadline.timestamp(),
436 (timezone.now() + datetime.timedelta(days=365, seconds=1)).timestamp(),
437 places=1)
439 def test_SLA_remaining_time__one_day_left(self):
440 self.bug.SLA_deadline = timezone.now() - datetime.timedelta(days=1)
441 self.assertAlmostEqual(self.bug.SLA_remaining_time.total_seconds(),
442 datetime.timedelta(days=-1).total_seconds(), places=1)
444 def test_SLA_remaining_time__one_day_over(self):
445 self.bug.SLA_deadline = timezone.now() + datetime.timedelta(days=1)
446 self.assertLessEqual(abs(self.bug.SLA_remaining_time.total_seconds() -
447 datetime.timedelta(days=1).total_seconds()), 1)
449 def test_SLA_remaining_str__one_day_over(self):
450 self.bug.SLA_remaining_time = datetime.timedelta(days=-1)
451 exp = "1 day, 0:00:00 ago"
452 self.assertEqual(exp, self.bug.SLA_remaining_str)
454 def test_SLA_remaining_str__one_day_left(self):
455 self.bug.SLA_remaining_time = datetime.timedelta(days=1)
456 exp = "in 1 day, 0:00:00"
457 self.assertEqual(exp, self.bug.SLA_remaining_str)
459 def test_effective_priority(self):
460 self.bug.SLA_remaining_time = datetime.timedelta(days=3)
461 self.bug.SLA = datetime.timedelta(hours=2.5)
462 self.assertAlmostEqual(self.bug.effective_priority, -self.bug.SLA_remaining_time / self.bug.SLA)
464 def test_is_being_updated__never_flagged(self):
465 self.assertFalse(self.bug.is_being_updated)
467 def test_is_being_updated__not_expired(self):
468 self.bug.flagged_as_update_pending_on = timezone.now()
469 self.assertTrue(self.bug.is_being_updated)
471 def test_is_being_updated__expired(self):
472 self.bug.flagged_as_update_pending_on = timezone.now() - self.bug.UPDATE_PENDING_TIMEOUT
473 self.assertFalse(self.bug.is_being_updated)
475 def test_update_pending_expires_in__never_flagged(self):
476 self.assertEqual(self.bug.update_pending_expires_in, None)
478 @patch('django.utils.timezone.now',
479 return_value=datetime.datetime.strptime('2019-01-01T00:00:05', "%Y-%m-%dT%H:%M:%S"))
480 def test_update_pending_expires_in__not_expired(self, now_mocked):
481 self.bug.flagged_as_update_pending_on = timezone.now()
482 self.assertEqual(self.bug.update_pending_expires_in, self.bug.UPDATE_PENDING_TIMEOUT)
484 @patch('django.utils.timezone.now',
485 return_value=datetime.datetime.strptime('2019-01-01T00:00:05', "%Y-%m-%dT%H:%M:%S"))
486 def test_update_pending_expires_in__expired(self, now_mocked):
487 self.bug.flagged_as_update_pending_on = timezone.now() - 2 * self.bug.UPDATE_PENDING_TIMEOUT
488 self.assertEqual(self.bug.update_pending_expires_in, -self.bug.UPDATE_PENDING_TIMEOUT)
490 @patch('CIResults.models.BugTracker.poll')
491 def test_poll(self, poll_mock):
492 self.bug.poll()
493 poll_mock.assert_called_with(self.bug, False)
495 def test_create(self):
496 tracker = BugTracker.objects.create(name="Tracker2", tracker_type="jira", url="http://foo",
497 project="TEST2", public=True)
498 bug = Bug(tracker=tracker, title="random title")
499 bug.tracker.tracker.create_bug = MagicMock(return_value=1)
500 bug.create()
501 self.assertEqual(bug.bug_id, 1)
503 def test_create_error(self):
504 tracker = BugTracker.objects.create(name="Tracker2", tracker_type="jira", url="http://foo",
505 project="TEST2", public=True)
506 bug = Bug(tracker=tracker, title="random title")
507 bug.tracker.tracker.create_bug = MagicMock(side_effect=ValueError)
508 bug.create()
509 self.assertNotEqual(bug.bug_id, 1)
511 def test_save_with_dict_in_custom_field(self):
512 tracker = BugTracker.objects.create(name="BugTracker", public=True)
513 with self.assertRaisesMessage(ValueError,
514 'Values stored in custom_fields cannot be tuples, lists, dictionaries'):
515 Bug.objects.create(tracker=tracker, custom_fields={"field": {"toto": "gaga"}})
517 def test_save_with_list_in_custom_field(self):
518 tracker = BugTracker.objects.create(name="BugTracker", public=True)
519 with self.assertRaisesMessage(ValueError,
520 'Values stored in custom_fields cannot be tuples, lists, dictionaries'):
521 Bug.objects.create(tracker=tracker, custom_fields={"field": [0, 1, 2, 3]})
523 def test_save_with_tuples_in_custom_field(self):
524 tracker = BugTracker.objects.create(name="BugTracker", public=True)
525 with self.assertRaisesMessage(ValueError,
526 'Values stored in custom_fields cannot be tuples, lists, dictionaries'):
527 Bug.objects.create(tracker=tracker, custom_fields={"field": (0, 1, 2, 3)})
529 def test_update_from_dict(self):
530 upd_dict = {'severity': 'High', 'priority': 'Medium', 'non-existant': 42,
531 'id': 42, 'bug_id': 42, 'tracker_id': 42, 'tracker': 42,
532 'parent_id': 42, 'parent': 42}
533 self.bug.update_from_dict(upd_dict)
535 self.assertEqual(self.bug.severity, 'High')
536 self.assertEqual(self.bug.priority, 'Medium')
537 self.assertNotIn(42, self.bug.__dict__.values())
539 def test_str(self):
540 self.assertEqual(str(self.bug), "fdo#1234 - random title")
543class TestReplicationScript(TestCase):
544 def test_str(self):
545 tracker = BugTracker.objects.create(name="Freedesktop.org", short_name="fdo", separator="#", public=False)
546 tracker2 = BugTracker.objects.create(name="JIRA", short_name="jira", separator="#", public=False)
547 rep_script = ReplicationScript.objects.create(name="My Script", source_tracker=tracker,
548 destination_tracker=tracker2)
549 self.assertEqual(str(rep_script), "<replication script 'My Script'>")
551 def test_script_validator(self):
552 val_script = "def foo(): pass"
553 res = script_validator(val_script)
554 self.assertEqual(res, val_script)
556 def test_script_validator_error(self):
557 inval_script = "def foo("
558 with self.assertRaises(ValidationError):
559 script_validator(inval_script)
562class TestBugComment(TestCase):
563 def test_str(self):
564 account = BugTrackerAccount(person=Person(full_name="John Doe", email="john.doe@isp.earth"))
566 tracker = BugTracker(name="Freedesktop.org", short_name="fdo", separator="#")
567 bug = Bug(tracker=tracker, bug_id="1234", title="random title",
568 created=timezone.now() - datetime.timedelta(days=4))
570 comment = BugComment(bug=bug, account=account)
571 self.assertEqual(str(comment), "{}'s comment by {}".format(bug, account))
574class TestBuild(TestCase):
575 def setUp(self):
576 self.component = Component.objects.create(name='component', public=True)
578 def test_url(self):
579 self.assertEqual(Build(component=self.component, name='build').url,
580 '')
581 self.assertEqual(Build(component=self.component, name='build', version='version').url,
582 'version')
583 self.assertEqual(Build(component=self.component, name='build', repo='git://repo.com/repo',
584 version='version').url,
585 'version @ git://repo.com/repo')
586 self.assertEqual(Build(component=self.component, name='build', upstream_url='https://repo.com/commit/version',
587 repo='git://repo.com/repo', version='version').url,
588 'https://repo.com/commit/version')
591class VettableObjectMixin:
592 def setUpVettableObject(self, vettable_object):
593 self.vettable_object = vettable_object
595 def test_vet(self):
596 self.assertFalse(self.vettable_object.vetted)
597 self.vettable_object.vet()
598 self.assertTrue(self.vettable_object.vetted)
600 self.assertRaisesMessage(ValueError, 'The object is already vetted',
601 self.vettable_object.vet)
603 def test_suppress(self):
604 self.vettable_object.vetted_on = timezone.now()
606 self.assertTrue(self.vettable_object.vetted)
607 self.vettable_object.suppress()
608 self.assertFalse(self.vettable_object.vetted)
610 self.assertRaisesMessage(ValueError, 'The object is already suppressed',
611 self.vettable_object.suppress)
614class TestTests(TestCase, VettableObjectMixin):
615 def setUp(self):
616 self.ts = TestSuite.objects.create(name="testsuite", description="nothing", public=True)
617 self.test = Test.objects.create(testsuite=self.ts, name="test", public=True)
619 self.setUpVettableObject(self.test)
621 def test__str__(self):
622 self.assertEqual(str(self.test), "{}: {}".format(self.ts.name, self.test.name))
624 @patch('CIResults.models.IssueFilterAssociated.objects.filter')
625 def test_in_active_ifas(self, mock_filter):
626 self.test.in_active_ifas
627 mock_filter.assert_called_with(deleted_on=None, filter__tests__in=[self.test])
629 @patch.object(Test, 'in_active_ifas', [MagicMock(), MagicMock()])
630 def test_rename_public_test_to_existing_private_test(self):
631 self.test.vetted_on = timezone.now()
632 self.test.first_runconfig = RunConfig(name="test_runcfg")
634 new_test = Test.objects.create(testsuite=self.ts, name="test2", public=False, vetted_on=timezone.now(),
635 first_runconfig=RunConfig.objects.create(name="test2_runcfg", temporary=False))
637 self.test.rename(new_test.name)
639 # Fetch again the new test and check it got updated
640 new_test = Test.objects.get(name="test2")
641 self.assertTrue(new_test.public)
642 self.assertEqual(new_test.testsuite, self.test.testsuite)
643 self.assertEqual(new_test.first_runconfig.name, "test2_runcfg") # This field should not have changed
644 self.assertEqual(new_test.vetted_on, self.test.vetted_on)
646 self.assertEqual(len(self.test.in_active_ifas), 2)
647 for ifa in self.test.in_active_ifas:
648 ifa.filter.tests.add.assert_called_with(new_test)
650 @patch.object(Test, 'in_active_ifas', [MagicMock(), MagicMock()])
651 def test_rename_test_to_new_bug(self):
652 self.test.public = False
653 self.test.vetted_on = timezone.now()
654 self.test.first_runconfig = RunConfig(name="test_runcfg")
656 self.test.rename('test2')
658 # Fetchthe new test and check the fields got copied
659 new_test = Test.objects.get(name="test2")
660 self.assertFalse(new_test.public)
661 self.assertEqual(new_test.testsuite, self.ts)
662 self.assertEqual(new_test.first_runconfig, None)
663 self.assertEqual(new_test.vetted_on, self.test.vetted_on)
665 self.assertEqual(len(self.test.in_active_ifas), 2)
666 for ifa in self.test.in_active_ifas:
667 ifa.filter.tests.add.assert_called_with(new_test)
670class MachineTagTests(TestCase):
671 def test_machines(self):
672 tag = MachineTag.objects.create(name='tag', public=True)
673 machine1 = Machine.objects.create(name='machine1', public=True)
674 machine1.tags.add(tag)
676 machine2 = Machine.objects.create(name='machine2', public=True)
677 machine2.tags.add(tag)
679 Machine.objects.create(name='machine3', public=True)
681 self.assertEqual(tag.machines, [machine1, machine2])
683 def test_str(self):
684 self.assertEqual(str(MachineTag(name='tag')), "tag")
687class MachineTests(TestCase, VettableObjectMixin):
688 def setUp(self):
689 self.machine = Machine.objects.create(name="machine", public=True)
690 self.setUpVettableObject(self.machine)
692 @patch('CIResults.models.Machine.tags')
693 def test_tags_cached(self, tags_cached_mocked):
694 self.assertEqual(self.machine.tags_cached, tags_cached_mocked.all.return_value)
696 def test__str__(self):
697 self.assertEqual(str(self.machine), self.machine.name)
700class RunConfigTests(TestCase):
701 def setUp(self):
702 self.testsuite = TestSuite.objects.create(name="testsuite", public=True)
703 self.s_pass = TextStatus.objects.create(name="pass", testsuite=self.testsuite)
704 self.s_fail = TextStatus.objects.create(name="fail", testsuite=self.testsuite)
705 self.s_broken = TextStatus.objects.create(name="broken", testsuite=self.testsuite)
706 self.testsuite.acceptable_statuses.add(self.s_pass)
708 self.runconfig = RunConfig.objects.create(name="run", temporary=True)
710 def test_public_no_tags(self):
711 self.assertTrue(self.runconfig.public)
713 def test_public_all_public(self):
714 public_tag1 = RunConfigTag.objects.create(name="public_tag1", public=True)
715 public_tag2 = RunConfigTag.objects.create(name="public_tag2", public=True)
717 self.runconfig.tags.add(public_tag1)
718 self.runconfig.tags.add(public_tag2)
719 self.assertTrue(self.runconfig.public)
721 def test_public_when_one_tag_is_private(self):
722 public_tag1 = RunConfigTag.objects.create(name="public_tag1", public=True)
723 public_tag2 = RunConfigTag.objects.create(name="public_tag2", public=True)
724 private_tag = RunConfigTag.objects.create(name="private_tag", public=False)
726 self.runconfig.tags.add(public_tag1)
727 self.runconfig.tags.add(public_tag2)
728 self.runconfig.tags.add(private_tag)
729 self.assertFalse(self.runconfig.public)
731 def test_update_statistics(self):
732 self.machine = Machine.objects.create(name="machine", public=True)
733 self.ts_run = TestsuiteRun.objects.create(testsuite=self.testsuite, runconfig=self.runconfig,
734 machine=self.machine, run_id=0,
735 start=timezone.now(), duration=datetime.timedelta(hours=1))
736 self.tests = []
737 self.testresults = []
738 for i in range(4):
739 self.tests.append(Test.objects.create(name="test{}".format(i),
740 testsuite=self.testsuite,
741 public=True))
743 status = self.s_pass if i < 2 else self.s_fail
744 self.testresults.append(TestResult.objects.create(test=self.tests[i],
745 ts_run=self.ts_run,
746 status=status,
747 start=timezone.now(),
748 duration=datetime.timedelta(seconds=3)))
750 self.issue = Issue.objects.create(description="Issue", filer="me@me.de")
752 # Create a filter that should not cover anything, and other that cover
753 # and match a different count
754 self.filters = []
755 f = IssueFilter.objects.create(description="Covers nothing")
756 f.statuses.add(self.s_broken)
757 for i in range(len(self.tests)):
758 f = IssueFilter.objects.create(description="Filter{}".format(i))
759 f.tests.add(self.tests[i])
760 if i + 1 < len(self.tests):
761 f.tests.add(self.tests[i + 1])
762 IssueFilterAssociated.objects.create(filter=f, issue=self.issue)
764 # Try computing statistics as a temporary run first
765 stats = self.runconfig.update_statistics()
766 self.assertEqual(len(stats), 0)
768 # Now check if the stastistics match with a non-temporary run
769 self.runconfig.temporary = False
770 stats = self.runconfig.update_statistics()
771 stats.sort(key=lambda x: x.filter.id)
773 expected_results = [(0, 2), (1, 2), (2, 2), (1, 1)]
774 for i, stat in enumerate(stats):
775 self.assertEqual(stat.matched_count, expected_results[i][0])
776 self.assertEqual(stat.covered_count, expected_results[i][1])
778 # TODO: Test runcfg_history and runcfg_history_offset
781class TestSuiteTests(TestCase):
782 def setUp(self):
783 self.testsuite = TestSuite(name="testsuite1", public=True)
784 self.testsuite.save()
786 def test_str(self):
787 self.assertEqual(str(self.testsuite), "testsuite1")
789 def test_is_failure(self):
790 statuses = []
791 for i in range(4):
792 status = TextStatus(name="status{}".format(i),
793 testsuite=self.testsuite)
794 status.save()
795 statuses.append(status)
797 self.testsuite.acceptable_statuses.add(statuses[2])
798 self.testsuite.acceptable_statuses.add(statuses[3])
800 for i in range(0, 2):
801 self.assertTrue(self.testsuite.is_failure(statuses[i]))
802 for i in range(2, 4):
803 self.assertFalse(self.testsuite.is_failure(statuses[i]))
806class IssueTests(TestCase):
807 Model = Issue
809 def setUp(self):
810 self.user = get_user_model().objects.create(username='blabla')
812 created_on = timezone.now() - datetime.timedelta(seconds=1)
813 self.issue = Issue.objects.create(description="blabla",
814 filer="test@test.de",
815 added_on=timezone.now())
817 self.filters = []
818 self.filtersAssoc = []
819 for i in range(4):
820 # Create the a filter and add it to the issue
821 filter = IssueFilter.objects.create(description="Filter {}".format(i),
822 added_on=created_on)
823 self.filters.append(filter)
825 # Create the association between the filter and the issue
826 # WARNING: if the index is 2, close it immediately
827 deleted_on = None if i != 2 else timezone.now()
828 assoc = IssueFilterAssociated.objects.create(filter=filter, issue=self.issue,
829 added_on=created_on, added_by=self.user,
830 deleted_on=deleted_on)
831 self.filtersAssoc.append(assoc)
833 # Create multiple runconfigs
834 self.runconfigs = []
835 for i in range(5):
836 r = RunConfig.objects.create(name="Runconfig {}".format(i),
837 temporary=False,
838 added_on=created_on + (i - 2) * datetime.timedelta(seconds=1))
839 self.runconfigs.append(r)
841 for filter in self.filters:
842 RunFilterStatistic.objects.create(runconfig=r, filter=filter,
843 covered_count=0 if i < 2 else 10,
844 matched_count=0 if i < 3 else 3)
846 self.issue.update_statistics()
848 def test_active_filters(self):
849 # Check that all the current filters are here (excluding the filter2
850 # because it is got deleted already
851 filters = set([self.filtersAssoc[0], self.filtersAssoc[1], self.filtersAssoc[3]])
852 self.assertEqual(filters, set(self.issue.active_filters))
854 # Now check that archiving does not change the result
855 self.issue.archive(self.user)
856 self.assertEqual(filters, set(self.issue.active_filters))
858 def test_runconfigs_covered(self):
859 runconfigs = set([self.runconfigs[2], self.runconfigs[3], self.runconfigs[4]])
860 self.assertEqual(runconfigs, self.issue.runconfigs_covered)
862 # Now check that archiving does not change the result
863 self.issue.archive(self.user)
864 self.assertEqual(runconfigs, self.issue.runconfigs_covered)
866 def test_runconfigs_affected(self):
867 runconfigs = set([self.runconfigs[3], self.runconfigs[4]])
868 self.assertEqual(runconfigs, self.issue.runconfigs_affected)
870 # Now check that archiving does not change the result
871 self.issue.archive(self.user)
872 self.assertEqual(runconfigs, self.issue.runconfigs_affected)
874 def test_last_seen(self):
875 self.assertEqual(self.issue.last_seen, self.runconfigs[-1].added_on)
877 def test_failure_rate(self):
878 failure_rate = self.issue.failure_rate
879 self.assertAlmostEqual(2 / 3, failure_rate.rate)
880 self.assertEqual("2 / 3 runs (66.7%)", str(failure_rate))
882 def test_matches(self):
883 # Intercept the calls to CIResults.models.IssueFilter.matches
884 patcher_api_call = patch('CIResults.models.IssueFilter.matches')
885 mock_matches = patcher_api_call.start()
886 mock_matches.return_value = False
887 self.addCleanup(patcher_api_call.stop)
889 self.issue.matches(None)
890 self.assertEqual(mock_matches.call_count, 3)
892 # Now archive the issue, and check matches returns False without
893 # calling $filter.matches()
894 mock_matches.reset_mock()
895 self.issue.archive(self.user)
896 self.assertFalse(self.issue.matches(None))
897 self.assertEqual(mock_matches.call_count, 0)
899 def __count_active_filters__(self):
900 count = 0
901 for e in IssueFilterAssociated.objects.filter(issue=self.issue):
902 if e.deleted_on is None:
903 count += 1
904 else:
905 self.assertLess(timezone.now() - e.deleted_on,
906 datetime.timedelta(seconds=10),
907 "The deleted_on time is incorrect")
908 self.assertEqual(e.added_by, self.user)
909 return count
911 def __check_comment_posted(self, render_and_leave_comment_on_all_bugs_mock, substring):
912 render_and_leave_comment_on_all_bugs_mock.assert_called_once()
913 args, kwargs = render_and_leave_comment_on_all_bugs_mock.call_args_list[0]
914 self.assertIn(substring, args[0])
916 @patch('CIResults.models.Issue.render_and_leave_comment_on_all_bugs')
917 def test_archive(self, render_and_leave_comment_on_all_bugs_mock):
918 # Check the default state
919 self.assertFalse(self.issue.archived)
920 self.assertEqual(self.issue.archived_on, None)
921 self.assertEqual(self.__count_active_filters__(), 3)
923 # Archive the issue
924 self.issue.archive(self.user)
926 # Check that the filters' association has been updated
927 self.assertEqual(self.__count_active_filters__(), 0)
929 # Check that archived_on has been updated
930 self.assertTrue(self.issue.archived)
931 self.assertNotEqual(self.issue.archived_on, None)
932 self.assertLess(timezone.now() - self.issue.archived_on,
933 datetime.timedelta(seconds=1),
934 "The archived_on time is incorrect")
935 self.assertEqual(self.issue.archived_by, self.user)
937 # Check that we posted a comment on the bugs associated
938 self.__check_comment_posted(render_and_leave_comment_on_all_bugs_mock, "archived")
940 # Check that archiving the issue again generates an error
941 self.assertRaisesMessage(ValueError, "The issue is already archived",
942 self.issue.archive, self.user)
944 @patch('CIResults.models.Issue.render_and_leave_comment_on_all_bugs')
945 def test_restore(self, render_and_leave_comment_on_all_bugs_mock):
946 # Archive the issue
947 # TODO: Load a fixture with an archived issue and an unknown failure
948 self.issue.archived_on = timezone.now()
950 # Restore the issue before checking the restoration process
951 self.issue.restore()
953 # Check that the filters' association has been updated
954 self.assertEqual(self.__count_active_filters__(), 3)
956 # Check that archived_on has been updated
957 self.assertFalse(self.issue.archived)
958 self.assertEqual(self.issue.archived_on, None)
959 self.assertEqual(self.issue.archived_by, None)
961 # TODO: Make sure the unknown failure became a known failure, associated
962 # to this issue. To be done after migrating to the fixture
964 # Check that we posted a comment on the bugs associated
965 self.__check_comment_posted(render_and_leave_comment_on_all_bugs_mock, "restored")
967 # Check that restoring the issue again generates an error
968 self.assertRaisesMessage(ValueError, "The issue is not currently archived",
969 self.issue.restore)
971 def test_set_bugs(self):
972 # Create some bugs
973 bugs = []
974 tracker = BugTracker.objects.create(name="Tracker", tracker_type="jira_untracked", public=True)
975 for i in range(5):
976 bugs.append(Bug(tracker=tracker, bug_id=str(i), title="bug {}".format(i)))
978 # Add some bugs
979 self.issue.set_bugs(bugs[2:4])
980 self.assertEqual(set(self.issue.bugs_cached), set(bugs[2:4]))
982 # Now try to update the bugs
983 self.issue.set_bugs(bugs[0:3])
984 self.assertEqual(set(self.issue.bugs_cached), set(bugs[0:3]))
986 # Archive the issue, and see if updating the bug generates an assert
987 self.issue.archive(self.user)
988 self.assertRaisesMessage(ValueError, "The issue is archived, and thus read-only",
989 self.issue.set_bugs, [])
991 def test___filter_add__(self):
992 filter = baker.make(IssueFilter)
993 self.issue.__filter_add__(filter, self.user)
994 self.assertEqual(
995 IssueFilterAssociated.objects.filter(filter=filter, issue=self.issue, added_by=self.user).count(), 1
996 )
998 @patch("CIResults.models.timezone.now")
999 def test__assign_to_known_failures(self, timezone_now_mock):
1000 date_reported = timezone.make_aware(datetime.datetime(2024, 1, 1), timezone.get_default_timezone())
1001 timezone_now_mock.return_value = date_reported
1003 unknown_failures = baker.make(UnknownFailure, _quantity=3)
1004 ifa = baker.make(IssueFilterAssociated)
1006 date_now = timezone.make_aware(datetime.datetime(2024, 1, 2), timezone.get_default_timezone())
1007 timezone_now_mock.return_value = date_now
1008 known_failures = self.issue._assign_to_known_failures(unknown_failures, ifa)
1010 self.assertEqual(len(known_failures), 3)
1011 self.assertEqual(UnknownFailure.objects.count(), 0)
1012 for failure in known_failures:
1013 self.assertEqual(failure.manually_associated_on, date_now)
1014 self.assertEqual(failure.filing_delay, date_now - date_reported)
1016 def test_replace_filter(self):
1017 old_filter = IssueFilter.objects.create(description="old filter")
1018 new_filter = IssueFilter.objects.create(description="new filter")
1020 # Add the old filter once before deleting it
1021 self.issue.set_filters([old_filter], self.user)
1022 self.issue.set_filters([], self.user)
1024 self.assertEqual(IssueFilterAssociated.objects.exclude(deleted_on=None).filter(filter=old_filter).count(), 1)
1026 # Re-add the old filter, then replace it with the new one
1027 self.issue.set_filters([old_filter], self.user)
1028 self.issue.replace_filter(old_filter, new_filter, self.user)
1030 self.assertEqual(IssueFilterAssociated.objects.exclude(deleted_on=None).filter(filter=old_filter).count(), 2)
1031 self.assertEqual(IssueFilterAssociated.objects.filter(deleted_on=None, filter=new_filter).count(), 1)
1033 # Archive the issue, and see if replacing the filter generates an assert
1034 self.issue.archive(self.user)
1035 self.assertRaisesMessage(ValueError, "The issue is archived, and thus read-only",
1036 self.issue.replace_filter, new_filter, old_filter, self.user)
1038 def test_set_filters(self):
1039 # Check the current amount of filters
1040 self.assertEqual(set([e.filter for e in self.issue.active_filters]),
1041 set([self.filters[0], self.filters[1], self.filters[3]]))
1042 self.assertEqual(set([e.filter for e in self.issue.all_filters]), set(self.filters))
1044 # Now try to update the filters
1045 self.issue.set_filters(self.filters[0:3], self.user)
1046 self.assertEqual(set([e.filter for e in self.issue.active_filters]), set(self.filters[0:3]))
1047 self.assertEqual(set([e.filter for e in self.issue.all_filters]), set(self.filters))
1049 # Check that the deleted_on field has been updated for the filter 3
1050 expected_len = [1, 1, 2, 1]
1051 expected_deleted_none = [True, True, False, False]
1052 for i in range(len(self.filters)):
1053 db_assocs = IssueFilterAssociated.objects.filter(filter=self.filters[i], issue=self.issue)
1054 self.assertEqual(len(db_assocs), expected_len[i], i)
1055 if len(db_assocs) > 0:
1056 self.assertEqual(db_assocs[0].deleted_on is None, expected_deleted_none[i], i)
1058 # Archive the issue, and see if updating the filter generates an assert
1059 self.issue.archive(self.user)
1060 self.assertRaisesMessage(ValueError, "The issue is archived, and thus read-only",
1061 self.issue.set_filters, [], self.user)
1063 def test_add_filters(self):
1064 # Check the current amount of filters
1065 filters = set(self.filters)
1066 self.assertEqual({e.filter for e in self.issue.all_filters}, filters)
1068 # Now try to add some filters
1069 f1 = baker.make(IssueFilter)
1070 f2 = baker.make(IssueFilter)
1071 filters.update((f1, f2))
1072 self.issue.add_filters([self.filters[-1], f1, f2], self.user)
1073 self.assertEqual({e.filter for e in self.issue.active_filters}, filters - {self.filters[2]})
1075 # "Uncache" all filters
1076 del self.issue.all_filters
1077 self.assertEqual({e.filter for e in self.issue.all_filters}, filters)
1079 def test_str(self):
1080 self.assertEqual(str(self.issue), "Issue: <empty>")
1082 tracker = BugTracker(name="Freedesktop.org", short_name="fdo",
1083 separator="#", public=True,
1084 url="https://bugs.freedesktop.org/",
1085 bug_base_url="https://bugs.freedesktop.org/show_bug.cgi?id=")
1086 tracker.save()
1087 bug1 = Bug.objects.create(tracker=tracker, bug_id="1234", title="random title")
1088 bug2 = Bug.objects.create(tracker=tracker, bug_id="1235", title="random title")
1090 self.issue.bugs.add(bug1)
1091 self.assertEqual(str(self.issue), "Issue: fdo#1234 - random title")
1093 self.issue.bugs.add(bug2)
1094 self.assertEqual(str(self.issue), "Issue: [fdo#1234, fdo#1235]")
1097class IssueFilterAssociatedTests(TestCase):
1098 def setUp(self):
1099 self.filter = IssueFilter.objects.create(description="Filter")
1100 self.issue = Issue.objects.create(filer="m@x.org")
1102 def test_delete(self):
1103 # Create an association and check that the field deleted_on is not set
1104 assoc = IssueFilterAssociated(filter=self.filter, issue=self.issue)
1105 self.assertEqual(assoc.deleted_on, None)
1106 self.assertEqual(assoc.id, None)
1108 # Delete the association, and verify that it actually got saved to the DB
1109 # by checking if the id has been set
1110 user = get_user_model().objects.create(username='blabla')
1111 assoc.delete(user, datetime.datetime.fromtimestamp(0, tz=pytz.utc))
1112 self.assertEqual(assoc.deleted_by, user)
1113 self.assertNotEqual(assoc.id, None)
1114 self.assertEqual(assoc.deleted_on, datetime.datetime.fromtimestamp(0, tz=pytz.utc))
1116 # Try deleting again with a new timestamp, and check that it did not change
1117 assoc.delete(datetime.datetime.fromtimestamp(1, tz=pytz.utc))
1118 self.assertEqual(assoc.deleted_on, datetime.datetime.fromtimestamp(0, tz=pytz.utc))
1120 # Test what happens when we omit the delete's now argument
1121 assoc = IssueFilterAssociated(filter=self.filter, issue=self.issue)
1122 assoc.delete(user)
1123 self.assertLess(abs(timezone.now() - assoc.deleted_on), datetime.timedelta(seconds=1))
1124 self.assertEqual(assoc.deleted_by, user)
1126 # TODO: Test all the statistics!
1129class TextStatusTests(TestCase, VettableObjectMixin):
1130 def setUp(self):
1131 self.testsuite = TestSuite.objects.create(name="testsuite", public=True)
1132 self.status = TextStatus.objects.create(testsuite=self.testsuite, name="status")
1133 self.setUpVettableObject(self.status)
1135 self.pass_status = TextStatus.objects.create(testsuite=self.testsuite, name="pass")
1136 self.testsuite.acceptable_statuses.add(self.pass_status)
1138 self.notrun = TextStatus.objects.create(testsuite=self.testsuite, name="notrun")
1139 self.testsuite.notrun_status = self.notrun
1141 def test_color__with_specified_color(self):
1142 self.assertEqual(TextStatus(color_hex="#123456").color, "#123456")
1144 def test_color__default(self):
1145 self.assertEqual(TextStatus().color, "#e9be2c")
1147 def test_is_failure(self):
1148 self.assertTrue(self.status.is_failure)
1149 self.assertFalse(self.pass_status.is_failure)
1151 def test_is_notrun(self):
1152 self.assertTrue(self.notrun.is_notrun)
1153 self.assertFalse(self.status.is_notrun)
1155 def test_actual_severity(self):
1156 self.assertEqual(TextStatus(severity=42).actual_severity, 42)
1158 self.assertEqual(self.notrun.actual_severity, 0)
1159 self.assertEqual(self.pass_status.actual_severity, 1)
1160 self.assertEqual(self.status.actual_severity, 2)
1162 def test_str(self):
1163 self.assertEqual(str(self.status), "testsuite: status")
1166class IssueFilterTests(TestCase):
1167 def setUp(self):
1168 self.tag = []
1169 self.runconfigs = []
1170 self.machine = []
1171 self.test = []
1172 self.status = []
1173 self.ts_run = []
1174 self.testresult = []
1176 self.testsuite = TestSuite(name="testsuite1", public=True)
1177 self.testsuite.save()
1179 # Create 4 instances of random objects
1180 for i in range(4):
1181 tag = RunConfigTag(public=True, name="tag{}".format(i))
1182 tag.save()
1183 self.tag.append(tag)
1185 machine = Machine(name="machine{}".format(i), public=True)
1186 machine.save()
1187 self.machine.append(machine)
1189 test = Test(name="test{}".format(i), testsuite=self.testsuite,
1190 public=True)
1191 test.save()
1192 self.test.append(test)
1194 status = TextStatus(name="status{}".format(i), testsuite=self.testsuite)
1195 status.save()
1196 self.status.append(status)
1198 # Tell which results are acceptable for the testsuite
1199 self.testsuite.acceptable_statuses.add(self.status[1])
1200 self.testsuite.acceptable_statuses.add(self.status[2])
1202 # Create the runconfig and ts_runs
1203 for i in range(3):
1204 runconfig = RunConfig(name="runconfig{}".format(i), temporary=False)
1205 runconfig.save()
1207 # Add $i tags
1208 if i > 0:
1209 runconfig.tags.add(self.tag[i - 1])
1211 # Create a testsuite run for all machines
1212 for machine in self.machine:
1213 self.ts_run.append(
1214 TestsuiteRun(
1215 testsuite=self.testsuite,
1216 runconfig=runconfig,
1217 machine=machine,
1218 run_id=0,
1219 start="2023-12-29 12:00",
1220 duration=datetime.timedelta(days=1),
1221 )
1222 )
1223 self.runconfigs.append(runconfig)
1225 # Create the test results
1226 for test in self.test:
1227 for ts_run in self.ts_run:
1228 ts_run.save()
1229 for status in self.status:
1230 test_result = TestResult(
1231 test=test,
1232 ts_run=ts_run,
1233 status=status,
1234 stdout="h\n{} stdout1234\nYoo!".format(status.name),
1235 stderr="h\n{} stderr1234\nasdf".format(status.name),
1236 dmesg="h\n{} dmesg1234\nqwer".format(status.name),
1237 start="2023-12-29 12:00",
1238 duration=datetime.timedelta(days=1),
1239 )
1240 test_result.save()
1241 self.testresult.append(test_result)
1243 self.filter = IssueFilter()
1244 self.filter.save()
1246 def test_empty(self):
1247 for testresult in self.testresult:
1248 self.assertTrue(self.filter.covers(testresult))
1249 self.assertTrue(self.filter.matches(testresult))
1251 # Test the conversion to user filters
1252 expected = ""
1253 self.assertEqual(self.filter.equivalent_user_query, expected)
1254 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), expected)
1255 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), expected)
1257 def test_runconfig_tag_only(self):
1258 self.filter.tags.add(self.tag[0])
1259 self.filter.tags.add(self.tag[2])
1261 filter_tags = set([self.tag[0].id, self.tag[2].id])
1262 for testresult in self.testresult:
1263 tags = set([t.id for t in testresult.ts_run.runconfig.tags.all()])
1264 should_match = not filter_tags.isdisjoint(tags)
1265 self.assertTrue(self.filter.covers(testresult) == should_match)
1266 self.assertTrue(self.filter.matches(testresult) == should_match)
1268 # Test the conversion to user filters
1269 expected = 'runconfig_tag IS IN ["tag0", "tag2"]'
1270 self.assertEqual(self.filter.equivalent_user_query, expected)
1271 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), expected)
1272 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), "")
1274 def test_machine_and_machine_tags(self):
1275 tag1 = MachineTag.objects.create(name="Tag1", public=True)
1276 self.machine[0].tags.add(tag1)
1277 self.machine[2].tags.add(tag1)
1279 self.filter.machine_tags.add(tag1)
1281 for testresult in self.testresult:
1282 machine = testresult.ts_run.machine
1283 should_match = (machine == self.machine[0] or machine == self.machine[2])
1284 self.assertTrue(self.filter.covers(testresult) == should_match)
1285 self.assertTrue(self.filter.matches(testresult) == should_match)
1287 # Test the conversion to user filters
1288 expected = 'machine_tag IS IN ["Tag1"]'
1289 self.assertEqual(self.filter.equivalent_user_query, expected)
1290 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), expected)
1291 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), "")
1293 def test_machine_tag_only(self):
1294 # Test that if a user asks for a tag that contains no machines, we do not match anything
1295 tag1 = MachineTag.objects.create(name="Tag1", public=True)
1296 self.filter.machine_tags.add(tag1)
1298 for testresult in self.testresult:
1299 self.assertFalse(self.filter.covers(testresult))
1300 self.assertFalse(self.filter.matches(testresult))
1302 # Test the conversion to user filters
1303 expected = 'machine_tag IS IN ["Tag1"]'
1304 self.assertEqual(self.filter.equivalent_user_query, expected)
1305 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), expected)
1306 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), "")
1308 def test_machine_only(self):
1309 self.filter.machines.add(self.machine[0])
1310 self.filter.machines.add(self.machine[2])
1312 for testresult in self.testresult:
1313 machine = testresult.ts_run.machine
1314 should_match = (machine == self.machine[0] or machine == self.machine[2])
1315 self.assertTrue(self.filter.covers(testresult) == should_match)
1316 self.assertTrue(self.filter.matches(testresult) == should_match)
1318 # Test the conversion to user filters
1319 machine_names = [mach.name for mach in self.filter.machines_cached]
1320 expected = 'machine_name IS IN ["{}", "{}"]'.format(machine_names[0], machine_names[1])
1321 self.assertEqual(self.filter.equivalent_user_query, expected)
1322 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), expected)
1323 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), "")
1325 def test_test_only(self):
1326 # Create another status from another testsuite
1327 testsuite2 = TestSuite.objects.create(name="testsuite2", public=True)
1328 test_ts2 = Test.objects.create(name="test-ts2", testsuite=testsuite2, public=True)
1330 self.filter.tests.add(self.test[0], self.test[2], test_ts2)
1332 for testresult in self.testresult:
1333 test = testresult.test
1334 should_match = (test == self.test[0] or test == self.test[2])
1335 self.assertTrue(self.filter.covers(testresult) == should_match)
1336 self.assertTrue(self.filter.matches(testresult) == should_match)
1338 # Generate all the valid queries that could be generated
1339 query_opts = []
1340 q1 = '(testsuite_name = "testsuite1" AND test_name IS IN ["{}", "{}"])'
1341 q2 = '(testsuite_name = "testsuite2" AND test_name IS IN ["test-ts2"])'
1342 query_pattern = '({} OR {})'
1343 query_opts.append(query_pattern.format(q1.format("test0", "test2"), q2))
1344 query_opts.append(query_pattern.format(q1.format("test2", "test0"), q2))
1345 query_opts.append(query_pattern.format(q2, q1.format("test2", "test0")))
1346 query_opts.append(query_pattern.format(q2, q1.format("test0", "test2")))
1348 # Test the conversion to user filters
1349 self.assertIn(self.filter.equivalent_user_query, query_opts)
1350 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), "")
1351 self.assertIn(self.filter._to_user_query(covers=True, matches=False), query_opts)
1353 def test_results_only(self):
1354 # Create another status from another testsuite
1355 testsuite2 = TestSuite.objects.create(name="testsuite2", public=True)
1356 status_ts2 = TextStatus.objects.create(name="status-ts2", testsuite=testsuite2)
1358 self.filter.statuses.add(self.status[0], self.status[2], status_ts2)
1360 for testresult in self.testresult:
1361 status = testresult.status
1362 should_match = (status == self.status[0] or status == self.status[2])
1363 self.assertTrue(self.filter.covers(testresult))
1364 self.assertTrue(self.filter.matches(testresult) == should_match, testresult)
1366 # Generate all the valid queries that could be generated
1367 query_opts = []
1368 q1 = '(testsuite_name = "testsuite1" AND status_name IS IN ["{}", "{}"])'
1369 q2 = '(testsuite_name = "testsuite2" AND status_name IS IN ["status-ts2"])'
1370 query_pattern = '({} OR {})'
1371 query_opts.append(query_pattern.format(q1.format("status0", "status2"), q2))
1372 query_opts.append(query_pattern.format(q1.format("status2", "status0"), q2))
1373 query_opts.append(query_pattern.format(q2, q1.format("status2", "status0")))
1374 query_opts.append(query_pattern.format(q2, q1.format("status0", "status2")))
1376 # Test the conversion to user filters
1377 self.assertIn(self.filter.equivalent_user_query, query_opts)
1378 self.assertIn(self.filter._to_user_query(covers=False, matches=True), query_opts)
1379 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), "")
1381 def test_escaping_of_single_quote(self):
1382 self.filter.stdout_regex = r"test's log"
1384 # Test the conversion to user filters and ensure that the single quote is escaped
1385 expected = r"stdout ~= 'test\'s log'"
1386 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), expected)
1388 def test_stdout_only(self):
1389 self.filter.stdout_regex = r"result[12] stdout\d+"
1390 for testresult in self.testresult:
1391 stdout_line = testresult.stdout.split('\n')[1]
1392 should_match = (stdout_line == "result1 stdout1234" or stdout_line == "result2 stdout1234")
1393 self.assertTrue(self.filter.covers(testresult))
1394 self.assertTrue(self.filter.matches(testresult) == should_match, stdout_line)
1396 # Test the conversion to user filters
1397 expected = r"stdout ~= 'result[12] stdout\d+'"
1398 self.assertEqual(self.filter.equivalent_user_query, expected)
1399 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), "")
1400 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), expected)
1402 def test_stderr_only(self):
1403 self.filter.stderr_regex = r"result[12] stderr\d+"
1404 for testresult in self.testresult:
1405 stderr_line = testresult.stderr.split('\n')[1]
1406 should_match = (stderr_line == "result1 stderr1234" or stderr_line == "result2 stderr1234")
1407 self.assertTrue(self.filter.covers(testresult))
1408 self.assertTrue(self.filter.matches(testresult) == should_match, stderr_line)
1410 # Test the conversion to user filters
1411 expected = r"stderr ~= 'result[12] stderr\d+'"
1412 self.assertEqual(self.filter.equivalent_user_query, expected)
1413 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), "")
1414 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), expected)
1416 def test_dmesg_only(self):
1417 self.filter.dmesg_regex = r"result[12] dmesg\d+"
1418 for testresult in self.testresult:
1419 dmesg_line = testresult.dmesg.split('\n')[1]
1420 should_match = (dmesg_line == "result1 dmesg1234" or dmesg_line == "result2 dmesg1234")
1421 self.assertTrue(self.filter.covers(testresult))
1422 self.assertTrue(self.filter.matches(testresult) == should_match, dmesg_line)
1424 # Test the conversion to user filters
1425 expected = r"dmesg ~= 'result[12] dmesg\d+'"
1426 self.assertEqual(self.filter.equivalent_user_query, expected)
1427 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), "")
1428 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), expected)
1430 def test_user_query_filter(self):
1431 self.filter.user_query = f'machine_name IS IN ["{self.machine[0]}", "{self.machine[2]}"]'
1433 for testresult in self.testresult:
1434 machine = testresult.ts_run.machine
1435 should_match = (machine == self.machine[0] or machine == self.machine[2])
1436 self.assertTrue(self.filter.covers(testresult) == should_match)
1437 self.assertTrue(self.filter.matches(testresult) == should_match)
1439 def test_replace(self):
1440 user = get_user_model().objects.create(username='blabla')
1442 issues = []
1443 for i in range(3):
1444 issue = Issue.objects.create(description="")
1445 issue.set_filters([self.filter], user)
1446 issues.append(issue)
1447 if i == 1:
1448 issue.archive(user)
1450 new_filter = IssueFilter.objects.create(description="new filter")
1452 self.assertEqual(IssueFilterAssociated.objects.filter(filter=self.filter).count(), 3)
1453 self.assertEqual(IssueFilterAssociated.objects.filter(filter=self.filter, deleted_by=user).count(), 1)
1454 self.assertEqual(IssueFilterAssociated.objects.filter(deleted_on=None, filter=self.filter).count(), 2)
1455 self.assertEqual(IssueFilterAssociated.objects.filter(filter=new_filter).count(), 0)
1456 self.assertFalse(self.filter.hidden)
1458 self.filter.replace(new_filter, user)
1460 self.assertEqual(IssueFilterAssociated.objects.filter(filter=self.filter).count(), 3)
1461 self.assertEqual(IssueFilterAssociated.objects.filter(deleted_on=None, filter=self.filter).count(), 0)
1462 self.assertEqual(IssueFilterAssociated.objects.filter(filter=new_filter).count(), 2)
1463 self.assertEqual(IssueFilterAssociated.objects.filter(filter=self.filter, deleted_by=user).count(), 3)
1464 self.assertTrue(self.filter.hidden)
1466 # Verify that the all the machines specified by tags are listed when calling test_machines_cached
1467 def test_machines_cached(self):
1468 # Create a machine tag and tag machine1 with it
1469 tag1 = MachineTag.objects.create(name="TAG1", public=True)
1470 self.machine[1].tags.add(tag1)
1471 self.machine[3].tags.add(tag1)
1473 # Now add the tag1 to the list of machines tags, and machine 0 as a machine
1474 self.filter.machine_tags.add(tag1)
1475 self.filter.machines.add(self.machine[0])
1477 # Now check that the filter lists the machines 1 and 2 when calling test_machines_cached
1478 self.assertEqual(self.filter.machines_cached, set([self.machine[0], self.machine[1], self.machine[3]]))
1481class RateTests(TestCase):
1482 def test_rate(self):
1483 self.assertAlmostEqual(Rate('', 1, 2).rate, 0.5)
1484 self.assertEqual(Rate('', 0, 0).rate, 0)
1486 def test_str(self):
1487 self.assertEqual(str(Rate('tests', 1, 2)), '1 / 2 tests (50.0%)')
1490class KnownFailureTests(TestCase):
1491 def test_covered_runconfigs_since(self):
1492 # Create a list of runconfigs that will be used by the "runconfig_covered"
1493 runconfigs = list()
1494 for i in range(6):
1495 runconfigs.append(RunConfig.objects.create(name="Runconfig{}".format(i), temporary=True))
1497 # Create a failure linked to an Issue with all the runconfigs as covered, and the ifa's
1498 # covered runconfigs are all but the last one. Associate the runconfig index 1 as the runconfig
1499 # where the failure happened
1500 failure = KnownFailure()
1501 failure.result = TestResult(ts_run=TestsuiteRun(runconfig=runconfigs[1]))
1502 failure.matched_ifa = IssueFilterAssociated(issue=Issue())
1503 failure.matched_ifa.runconfigs_covered = runconfigs[:-1]
1504 failure.matched_ifa.issue.runconfigs_covered = runconfigs
1506 # Check that when the runconfig is not found, we return None
1507 self.assertEqual(KnownFailure._runconfig_index(runconfigs, RunConfig()), None)
1509 # Check that the number of runconfigs since the failure happened is
1510 self.assertEqual(failure.covered_runconfigs_since_for_issue, 4) # len(issue.runconfig_covered) - 2
1511 self.assertEqual(failure.covered_runconfigs_since_for_filter, 3) # len(ifa.runconfig_covered) - 2
1514class UnknownFailureTests(TestCase):
1515 @patch('CIResults.models.UnknownFailure.matched_archived_ifas')
1516 def test_matched_archived_ifas_cached(self, matched_archived_ifas_mocked):
1517 self.assertEqual(UnknownFailure().matched_archived_ifas_cached, matched_archived_ifas_mocked.all.return_value)
1519 def test_matched_issues(self):
1520 failure = UnknownFailure()
1521 failure.matched_archived_ifas_cached = [MagicMock(spec=IssueFilterAssociated),
1522 MagicMock(spec=IssueFilterAssociated)]
1523 self.assertEqual(failure.matched_issues,
1524 set([e.issue for e in failure.matched_archived_ifas_cached]))
1526 @patch('CIResults.models.UnknownFailure.result')
1527 def test_str(self, results_mocked):
1528 failure = UnknownFailure()
1529 self.assertEqual(str(failure), str(failure.result))
1532class RunFilterStatisticTests(TestCase):
1533 def test_str(self):
1534 f = IssueFilter(description="My filter")
1535 runconfig = RunConfig(name='RunConfig')
1537 self.assertEqual(str(RunFilterStatistic(runconfig=runconfig, filter=f, covered_count=0, matched_count=0)),
1538 'My filter on RunConfig: match rate 0/0 (0.00%)')
1539 self.assertEqual(str(RunFilterStatistic(runconfig=runconfig, filter=f, covered_count=10, matched_count=5)),
1540 'My filter on RunConfig: match rate 5/10 (50.00%)')