Coverage for CIResults / tests / test_models.py: 100%
963 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-10 09:22 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-10 09:22 +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_str(self):
1064 self.assertEqual(str(self.issue), "Issue: <empty>")
1066 tracker = BugTracker(name="Freedesktop.org", short_name="fdo",
1067 separator="#", public=True,
1068 url="https://bugs.freedesktop.org/",
1069 bug_base_url="https://bugs.freedesktop.org/show_bug.cgi?id=")
1070 tracker.save()
1071 bug1 = Bug.objects.create(tracker=tracker, bug_id="1234", title="random title")
1072 bug2 = Bug.objects.create(tracker=tracker, bug_id="1235", title="random title")
1074 self.issue.bugs.add(bug1)
1075 self.assertEqual(str(self.issue), "Issue: fdo#1234 - random title")
1077 self.issue.bugs.add(bug2)
1078 self.assertEqual(str(self.issue), "Issue: [fdo#1234, fdo#1235]")
1081class IssueFilterAssociatedTests(TestCase):
1082 def setUp(self):
1083 self.filter = IssueFilter.objects.create(description="Filter")
1084 self.issue = Issue.objects.create(filer="m@x.org")
1086 def test_delete(self):
1087 # Create an association and check that the field deleted_on is not set
1088 assoc = IssueFilterAssociated(filter=self.filter, issue=self.issue)
1089 self.assertEqual(assoc.deleted_on, None)
1090 self.assertEqual(assoc.id, None)
1092 # Delete the association, and verify that it actually got saved to the DB
1093 # by checking if the id has been set
1094 user = get_user_model().objects.create(username='blabla')
1095 assoc.delete(user, datetime.datetime.fromtimestamp(0, tz=pytz.utc))
1096 self.assertEqual(assoc.deleted_by, user)
1097 self.assertNotEqual(assoc.id, None)
1098 self.assertEqual(assoc.deleted_on, datetime.datetime.fromtimestamp(0, tz=pytz.utc))
1100 # Try deleting again with a new timestamp, and check that it did not change
1101 assoc.delete(datetime.datetime.fromtimestamp(1, tz=pytz.utc))
1102 self.assertEqual(assoc.deleted_on, datetime.datetime.fromtimestamp(0, tz=pytz.utc))
1104 # Test what happens when we omit the delete's now argument
1105 assoc = IssueFilterAssociated(filter=self.filter, issue=self.issue)
1106 assoc.delete(user)
1107 self.assertLess(abs(timezone.now() - assoc.deleted_on), datetime.timedelta(seconds=1))
1108 self.assertEqual(assoc.deleted_by, user)
1110 # TODO: Test all the statistics!
1113class TextStatusTests(TestCase, VettableObjectMixin):
1114 def setUp(self):
1115 self.testsuite = TestSuite.objects.create(name="testsuite", public=True)
1116 self.status = TextStatus.objects.create(testsuite=self.testsuite, name="status")
1117 self.setUpVettableObject(self.status)
1119 self.pass_status = TextStatus.objects.create(testsuite=self.testsuite, name="pass")
1120 self.testsuite.acceptable_statuses.add(self.pass_status)
1122 self.notrun = TextStatus.objects.create(testsuite=self.testsuite, name="notrun")
1123 self.testsuite.notrun_status = self.notrun
1125 def test_color__with_specified_color(self):
1126 self.assertEqual(TextStatus(color_hex="#123456").color, "#123456")
1128 def test_color__default(self):
1129 self.assertEqual(TextStatus().color, "#e9be2c")
1131 def test_is_failure(self):
1132 self.assertTrue(self.status.is_failure)
1133 self.assertFalse(self.pass_status.is_failure)
1135 def test_is_notrun(self):
1136 self.assertTrue(self.notrun.is_notrun)
1137 self.assertFalse(self.status.is_notrun)
1139 def test_actual_severity(self):
1140 self.assertEqual(TextStatus(severity=42).actual_severity, 42)
1142 self.assertEqual(self.notrun.actual_severity, 0)
1143 self.assertEqual(self.pass_status.actual_severity, 1)
1144 self.assertEqual(self.status.actual_severity, 2)
1146 def test_str(self):
1147 self.assertEqual(str(self.status), "testsuite: status")
1150class IssueFilterTests(TestCase):
1151 def setUp(self):
1152 self.tag = []
1153 self.runconfigs = []
1154 self.machine = []
1155 self.test = []
1156 self.status = []
1157 self.ts_run = []
1158 self.testresult = []
1160 self.testsuite = TestSuite(name="testsuite1", public=True)
1161 self.testsuite.save()
1163 # Create 4 instances of random objects
1164 for i in range(4):
1165 tag = RunConfigTag(public=True, name="tag{}".format(i))
1166 tag.save()
1167 self.tag.append(tag)
1169 machine = Machine(name="machine{}".format(i), public=True)
1170 machine.save()
1171 self.machine.append(machine)
1173 test = Test(name="test{}".format(i), testsuite=self.testsuite,
1174 public=True)
1175 test.save()
1176 self.test.append(test)
1178 status = TextStatus(name="status{}".format(i), testsuite=self.testsuite)
1179 status.save()
1180 self.status.append(status)
1182 # Tell which results are acceptable for the testsuite
1183 self.testsuite.acceptable_statuses.add(self.status[1])
1184 self.testsuite.acceptable_statuses.add(self.status[2])
1186 # Create the runconfig and ts_runs
1187 for i in range(3):
1188 runconfig = RunConfig(name="runconfig{}".format(i), temporary=False)
1189 runconfig.save()
1191 # Add $i tags
1192 if i > 0:
1193 runconfig.tags.add(self.tag[i - 1])
1195 # Create a testsuite run for all machines
1196 for machine in self.machine:
1197 self.ts_run.append(
1198 TestsuiteRun(
1199 testsuite=self.testsuite,
1200 runconfig=runconfig,
1201 machine=machine,
1202 run_id=0,
1203 start="2023-12-29 12:00",
1204 duration=datetime.timedelta(days=1),
1205 )
1206 )
1207 self.runconfigs.append(runconfig)
1209 # Create the test results
1210 for test in self.test:
1211 for ts_run in self.ts_run:
1212 ts_run.save()
1213 for status in self.status:
1214 test_result = TestResult(
1215 test=test,
1216 ts_run=ts_run,
1217 status=status,
1218 stdout="h\n{} stdout1234\nYoo!".format(status.name),
1219 stderr="h\n{} stderr1234\nasdf".format(status.name),
1220 dmesg="h\n{} dmesg1234\nqwer".format(status.name),
1221 start="2023-12-29 12:00",
1222 duration=datetime.timedelta(days=1),
1223 )
1224 test_result.save()
1225 self.testresult.append(test_result)
1227 self.filter = IssueFilter()
1228 self.filter.save()
1230 def test_empty(self):
1231 for testresult in self.testresult:
1232 self.assertTrue(self.filter.covers(testresult))
1233 self.assertTrue(self.filter.matches(testresult))
1235 # Test the conversion to user filters
1236 expected = ""
1237 self.assertEqual(self.filter.equivalent_user_query, expected)
1238 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), expected)
1239 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), expected)
1241 def test_runconfig_tag_only(self):
1242 self.filter.tags.add(self.tag[0])
1243 self.filter.tags.add(self.tag[2])
1245 filter_tags = set([self.tag[0].id, self.tag[2].id])
1246 for testresult in self.testresult:
1247 tags = set([t.id for t in testresult.ts_run.runconfig.tags.all()])
1248 should_match = not filter_tags.isdisjoint(tags)
1249 self.assertTrue(self.filter.covers(testresult) == should_match)
1250 self.assertTrue(self.filter.matches(testresult) == should_match)
1252 # Test the conversion to user filters
1253 expected = 'runconfig_tag IS IN ["tag0", "tag2"]'
1254 self.assertEqual(self.filter.equivalent_user_query, expected)
1255 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), expected)
1256 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), "")
1258 def test_machine_and_machine_tags(self):
1259 tag1 = MachineTag.objects.create(name="Tag1", public=True)
1260 self.machine[0].tags.add(tag1)
1261 self.machine[2].tags.add(tag1)
1263 self.filter.machine_tags.add(tag1)
1265 for testresult in self.testresult:
1266 machine = testresult.ts_run.machine
1267 should_match = (machine == self.machine[0] or machine == self.machine[2])
1268 self.assertTrue(self.filter.covers(testresult) == should_match)
1269 self.assertTrue(self.filter.matches(testresult) == should_match)
1271 # Test the conversion to user filters
1272 expected = 'machine_tag IS IN ["Tag1"]'
1273 self.assertEqual(self.filter.equivalent_user_query, expected)
1274 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), expected)
1275 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), "")
1277 def test_machine_tag_only(self):
1278 # Test that if a user asks for a tag that contains no machines, we do not match anything
1279 tag1 = MachineTag.objects.create(name="Tag1", public=True)
1280 self.filter.machine_tags.add(tag1)
1282 for testresult in self.testresult:
1283 self.assertFalse(self.filter.covers(testresult))
1284 self.assertFalse(self.filter.matches(testresult))
1286 # Test the conversion to user filters
1287 expected = 'machine_tag IS IN ["Tag1"]'
1288 self.assertEqual(self.filter.equivalent_user_query, expected)
1289 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), expected)
1290 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), "")
1292 def test_machine_only(self):
1293 self.filter.machines.add(self.machine[0])
1294 self.filter.machines.add(self.machine[2])
1296 for testresult in self.testresult:
1297 machine = testresult.ts_run.machine
1298 should_match = (machine == self.machine[0] or machine == self.machine[2])
1299 self.assertTrue(self.filter.covers(testresult) == should_match)
1300 self.assertTrue(self.filter.matches(testresult) == should_match)
1302 # Test the conversion to user filters
1303 machine_names = [mach.name for mach in self.filter.machines_cached]
1304 expected = 'machine_name IS IN ["{}", "{}"]'.format(machine_names[0], machine_names[1])
1305 self.assertEqual(self.filter.equivalent_user_query, expected)
1306 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), expected)
1307 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), "")
1309 def test_test_only(self):
1310 # Create another status from another testsuite
1311 testsuite2 = TestSuite.objects.create(name="testsuite2", public=True)
1312 test_ts2 = Test.objects.create(name="test-ts2", testsuite=testsuite2, public=True)
1314 self.filter.tests.add(self.test[0], self.test[2], test_ts2)
1316 for testresult in self.testresult:
1317 test = testresult.test
1318 should_match = (test == self.test[0] or test == self.test[2])
1319 self.assertTrue(self.filter.covers(testresult) == should_match)
1320 self.assertTrue(self.filter.matches(testresult) == should_match)
1322 # Generate all the valid queries that could be generated
1323 query_opts = []
1324 q1 = '(testsuite_name = "testsuite1" AND test_name IS IN ["{}", "{}"])'
1325 q2 = '(testsuite_name = "testsuite2" AND test_name IS IN ["test-ts2"])'
1326 query_pattern = '({} OR {})'
1327 query_opts.append(query_pattern.format(q1.format("test0", "test2"), q2))
1328 query_opts.append(query_pattern.format(q1.format("test2", "test0"), q2))
1329 query_opts.append(query_pattern.format(q2, q1.format("test2", "test0")))
1330 query_opts.append(query_pattern.format(q2, q1.format("test0", "test2")))
1332 # Test the conversion to user filters
1333 self.assertIn(self.filter.equivalent_user_query, query_opts)
1334 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), "")
1335 self.assertIn(self.filter._to_user_query(covers=True, matches=False), query_opts)
1337 def test_results_only(self):
1338 # Create another status from another testsuite
1339 testsuite2 = TestSuite.objects.create(name="testsuite2", public=True)
1340 status_ts2 = TextStatus.objects.create(name="status-ts2", testsuite=testsuite2)
1342 self.filter.statuses.add(self.status[0], self.status[2], status_ts2)
1344 for testresult in self.testresult:
1345 status = testresult.status
1346 should_match = (status == self.status[0] or status == self.status[2])
1347 self.assertTrue(self.filter.covers(testresult))
1348 self.assertTrue(self.filter.matches(testresult) == should_match, testresult)
1350 # Generate all the valid queries that could be generated
1351 query_opts = []
1352 q1 = '(testsuite_name = "testsuite1" AND status_name IS IN ["{}", "{}"])'
1353 q2 = '(testsuite_name = "testsuite2" AND status_name IS IN ["status-ts2"])'
1354 query_pattern = '({} OR {})'
1355 query_opts.append(query_pattern.format(q1.format("status0", "status2"), q2))
1356 query_opts.append(query_pattern.format(q1.format("status2", "status0"), q2))
1357 query_opts.append(query_pattern.format(q2, q1.format("status2", "status0")))
1358 query_opts.append(query_pattern.format(q2, q1.format("status0", "status2")))
1360 # Test the conversion to user filters
1361 self.assertIn(self.filter.equivalent_user_query, query_opts)
1362 self.assertIn(self.filter._to_user_query(covers=False, matches=True), query_opts)
1363 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), "")
1365 def test_escaping_of_single_quote(self):
1366 self.filter.stdout_regex = r"test's log"
1368 # Test the conversion to user filters and ensure that the single quote is escaped
1369 expected = r"stdout ~= 'test\'s log'"
1370 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), expected)
1372 def test_stdout_only(self):
1373 self.filter.stdout_regex = r"result[12] stdout\d+"
1374 for testresult in self.testresult:
1375 stdout_line = testresult.stdout.split('\n')[1]
1376 should_match = (stdout_line == "result1 stdout1234" or stdout_line == "result2 stdout1234")
1377 self.assertTrue(self.filter.covers(testresult))
1378 self.assertTrue(self.filter.matches(testresult) == should_match, stdout_line)
1380 # Test the conversion to user filters
1381 expected = r"stdout ~= 'result[12] stdout\d+'"
1382 self.assertEqual(self.filter.equivalent_user_query, expected)
1383 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), "")
1384 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), expected)
1386 def test_stderr_only(self):
1387 self.filter.stderr_regex = r"result[12] stderr\d+"
1388 for testresult in self.testresult:
1389 stderr_line = testresult.stderr.split('\n')[1]
1390 should_match = (stderr_line == "result1 stderr1234" or stderr_line == "result2 stderr1234")
1391 self.assertTrue(self.filter.covers(testresult))
1392 self.assertTrue(self.filter.matches(testresult) == should_match, stderr_line)
1394 # Test the conversion to user filters
1395 expected = r"stderr ~= 'result[12] stderr\d+'"
1396 self.assertEqual(self.filter.equivalent_user_query, expected)
1397 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), "")
1398 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), expected)
1400 def test_dmesg_only(self):
1401 self.filter.dmesg_regex = r"result[12] dmesg\d+"
1402 for testresult in self.testresult:
1403 dmesg_line = testresult.dmesg.split('\n')[1]
1404 should_match = (dmesg_line == "result1 dmesg1234" or dmesg_line == "result2 dmesg1234")
1405 self.assertTrue(self.filter.covers(testresult))
1406 self.assertTrue(self.filter.matches(testresult) == should_match, dmesg_line)
1408 # Test the conversion to user filters
1409 expected = r"dmesg ~= 'result[12] dmesg\d+'"
1410 self.assertEqual(self.filter.equivalent_user_query, expected)
1411 self.assertEqual(self.filter._to_user_query(covers=True, matches=False), "")
1412 self.assertEqual(self.filter._to_user_query(covers=False, matches=True), expected)
1414 def test_user_query_filter(self):
1415 self.filter.user_query = f'machine_name IS IN ["{self.machine[0]}", "{self.machine[2]}"]'
1417 for testresult in self.testresult:
1418 machine = testresult.ts_run.machine
1419 should_match = (machine == self.machine[0] or machine == self.machine[2])
1420 self.assertTrue(self.filter.covers(testresult) == should_match)
1421 self.assertTrue(self.filter.matches(testresult) == should_match)
1423 def test_replace(self):
1424 user = get_user_model().objects.create(username='blabla')
1426 issues = []
1427 for i in range(3):
1428 issue = Issue.objects.create(description="")
1429 issue.set_filters([self.filter], user)
1430 issues.append(issue)
1431 if i == 1:
1432 issue.archive(user)
1434 new_filter = IssueFilter.objects.create(description="new filter")
1436 self.assertEqual(IssueFilterAssociated.objects.filter(filter=self.filter).count(), 3)
1437 self.assertEqual(IssueFilterAssociated.objects.filter(filter=self.filter, deleted_by=user).count(), 1)
1438 self.assertEqual(IssueFilterAssociated.objects.filter(deleted_on=None, filter=self.filter).count(), 2)
1439 self.assertEqual(IssueFilterAssociated.objects.filter(filter=new_filter).count(), 0)
1440 self.assertFalse(self.filter.hidden)
1442 self.filter.replace(new_filter, user)
1444 self.assertEqual(IssueFilterAssociated.objects.filter(filter=self.filter).count(), 3)
1445 self.assertEqual(IssueFilterAssociated.objects.filter(deleted_on=None, filter=self.filter).count(), 0)
1446 self.assertEqual(IssueFilterAssociated.objects.filter(filter=new_filter).count(), 2)
1447 self.assertEqual(IssueFilterAssociated.objects.filter(filter=self.filter, deleted_by=user).count(), 3)
1448 self.assertTrue(self.filter.hidden)
1450 # Verify that the all the machines specified by tags are listed when calling test_machines_cached
1451 def test_machines_cached(self):
1452 # Create a machine tag and tag machine1 with it
1453 tag1 = MachineTag.objects.create(name="TAG1", public=True)
1454 self.machine[1].tags.add(tag1)
1455 self.machine[3].tags.add(tag1)
1457 # Now add the tag1 to the list of machines tags, and machine 0 as a machine
1458 self.filter.machine_tags.add(tag1)
1459 self.filter.machines.add(self.machine[0])
1461 # Now check that the filter lists the machines 1 and 2 when calling test_machines_cached
1462 self.assertEqual(self.filter.machines_cached, set([self.machine[0], self.machine[1], self.machine[3]]))
1465class RateTests(TestCase):
1466 def test_rate(self):
1467 self.assertAlmostEqual(Rate('', 1, 2).rate, 0.5)
1468 self.assertEqual(Rate('', 0, 0).rate, 0)
1470 def test_str(self):
1471 self.assertEqual(str(Rate('tests', 1, 2)), '1 / 2 tests (50.0%)')
1474class KnownFailureTests(TestCase):
1475 def test_covered_runconfigs_since(self):
1476 # Create a list of runconfigs that will be used by the "runconfig_covered"
1477 runconfigs = list()
1478 for i in range(6):
1479 runconfigs.append(RunConfig.objects.create(name="Runconfig{}".format(i), temporary=True))
1481 # Create a failure linked to an Issue with all the runconfigs as covered, and the ifa's
1482 # covered runconfigs are all but the last one. Associate the runconfig index 1 as the runconfig
1483 # where the failure happened
1484 failure = KnownFailure()
1485 failure.result = TestResult(ts_run=TestsuiteRun(runconfig=runconfigs[1]))
1486 failure.matched_ifa = IssueFilterAssociated(issue=Issue())
1487 failure.matched_ifa.runconfigs_covered = runconfigs[:-1]
1488 failure.matched_ifa.issue.runconfigs_covered = runconfigs
1490 # Check that when the runconfig is not found, we return None
1491 self.assertEqual(KnownFailure._runconfig_index(runconfigs, RunConfig()), None)
1493 # Check that the number of runconfigs since the failure happened is
1494 self.assertEqual(failure.covered_runconfigs_since_for_issue, 4) # len(issue.runconfig_covered) - 2
1495 self.assertEqual(failure.covered_runconfigs_since_for_filter, 3) # len(ifa.runconfig_covered) - 2
1498class UnknownFailureTests(TestCase):
1499 @patch('CIResults.models.UnknownFailure.matched_archived_ifas')
1500 def test_matched_archived_ifas_cached(self, matched_archived_ifas_mocked):
1501 self.assertEqual(UnknownFailure().matched_archived_ifas_cached, matched_archived_ifas_mocked.all.return_value)
1503 def test_matched_issues(self):
1504 failure = UnknownFailure()
1505 failure.matched_archived_ifas_cached = [MagicMock(spec=IssueFilterAssociated),
1506 MagicMock(spec=IssueFilterAssociated)]
1507 self.assertEqual(failure.matched_issues,
1508 set([e.issue for e in failure.matched_archived_ifas_cached]))
1510 @patch('CIResults.models.UnknownFailure.result')
1511 def test_str(self, results_mocked):
1512 failure = UnknownFailure()
1513 self.assertEqual(str(failure), str(failure.result))
1516class RunFilterStatisticTests(TestCase):
1517 def test_str(self):
1518 f = IssueFilter(description="My filter")
1519 runconfig = RunConfig(name='RunConfig')
1521 self.assertEqual(str(RunFilterStatistic(runconfig=runconfig, filter=f, covered_count=0, matched_count=0)),
1522 'My filter on RunConfig: match rate 0/0 (0.00%)')
1523 self.assertEqual(str(RunFilterStatistic(runconfig=runconfig, filter=f, covered_count=10, matched_count=5)),
1524 'My filter on RunConfig: match rate 5/10 (50.00%)')