blob: e8527004862ffab5a6a8cad953ec39964ce15472 [file] [log] [blame]
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +02001"""Test crashreport_stats models and the 'stats' command."""
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04002from io import StringIO
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +04003from datetime import datetime, date, timedelta
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +02004import pytz
Dirk Vogt62ff7f22017-05-04 16:07:21 +02005
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +04006from django.core.management import call_command
7from django.test import TestCase
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +02008from django.urls import reverse
9from django.utils.http import urlencode
10
11from rest_framework import status
12from rest_framework.test import APITestCase, APIClient
13
14from crashreport_stats.models import (
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +040015 Version, VersionDaily, RadioVersion, RadioVersionDaily, StatsMetadata
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020016)
17
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040018from crashreports.models import User, Device, Crashreport, HeartBeat
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020019
20
21class Dummy():
22 """Class for creating dummy instances for testing."""
23
24 # Valid unique entries
25 BUILD_FINGERPRINTS = [(
26 'Fairphone/FP2/FP2:5.1/FP2/r4275.1_FP2_gms76_1.13.0:user/release-keys'
27 ), (
28 'Fairphone/FP2/FP2:5.1.1/FP2-gms75.1.13.0/FP2-gms75.1.13.0'
29 ':user/release-keys'
30 ), (
31 'Fairphone/FP2/FP2:6.0.1/FP2-gms-18.04.1/FP2-gms-18.04.1'
32 ':user/release-keys'
33 ), (
34 'Fairphone/FP2/FP2:7.1.2/18.07.2/gms-7480c31d'
35 ':user/release-keys'
36 )]
37 RADIO_VERSIONS = ['4437.1-FP2-0-07', '4437.1-FP2-0-08',
38 '4437.1-FP2-0-09', '4437.1-FP2-0-10']
39
40 DATES = [date(2018, 3, 19), date(2018, 3, 26), date(2018, 5, 1)]
41
42 DEFAULT_DUMMY_VERSION_VALUES = {
43 'build_fingerprint': BUILD_FINGERPRINTS[0],
44 'first_seen_on': DATES[1],
45 'released_on': DATES[0]
46 }
47
48 DEFAULT_DUMMY_VERSION_DAILY_VALUES = {
49 'date': DATES[1]
50 }
51
52 DEFAULT_DUMMY_RADIO_VERSION_VALUES = {
53 'radio_version': RADIO_VERSIONS[0],
54 'first_seen_on': DATES[1],
55 'released_on': DATES[0]
56 }
57
58 DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES = {
59 'date': DATES[1]
60 }
61
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +020062 DEFAULT_DUMMY_STATSMETADATA_VALUES = {
63 'updated_at': datetime(2018, 6, 15, 2, 12, 24, tzinfo=pytz.utc),
64 }
65
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020066 DEFAULT_DUMMY_DEVICE_VALUES = {
67 'board_date': datetime(2015, 12, 15, 1, 23, 45, tzinfo=pytz.utc),
68 'chipset': 'Qualcomm MSM8974PRO-AA',
69 'token': '64111c62d521fb4724454ca6dea27e18f93ef56e'
70 }
71
72 DEFAULT_DUMMY_USER_VALUES = {
73 'username': 'testuser'
74 }
75
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040076 DEFAULT_DUMMY_HEARTBEAT_VALUES = {
77 'app_version': 10100,
78 'uptime': (
79 'up time: 16 days, 21:49:56, idle time: 5 days, 20:55:04, '
80 'sleep time: 10 days, 20:46:27'),
81 'build_fingerprint': BUILD_FINGERPRINTS[0],
82 'radio_version': RADIO_VERSIONS[0],
83 'date': datetime(2018, 3, 19, tzinfo=pytz.utc),
84 }
85
86 DEFAULT_DUMMY_CRASHREPORT_VALUES = DEFAULT_DUMMY_HEARTBEAT_VALUES.copy()
87 DEFAULT_DUMMY_CRASHREPORT_VALUES.update({
88 'is_fake_report': 0,
89 'boot_reason': Crashreport.BOOT_REASON_UNKOWN,
90 'power_on_reason': 'it was powered on',
91 'power_off_reason': 'something happened and it went off',
92 })
93
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020094 @staticmethod
95 def update_copy(original, update):
96 """Merge fields of update into a copy of original."""
97 data = original.copy()
98 data.update(update)
99 return data
100
101 @staticmethod
102 def create_dummy_user(**kwargs):
103 """Create a dummy user instance.
104
105 The dummy instance is created and saved to the database.
106 Args:
107 **kwargs:
108 Optional arguments to extend/overwrite the default values.
109
110 Returns: The created user instance.
111
112 """
113 entity = User(**Dummy.update_copy(
114 Dummy.DEFAULT_DUMMY_USER_VALUES, kwargs))
115 entity.save()
116 return entity
117
118 @staticmethod
119 def create_dummy_device(user, **kwargs):
120 """Create a dummy device instance.
121
122 The dummy instance is created and saved to the database.
123 Args:
124 user: The user instance that the device should relate to
125 **kwargs:
126 Optional arguments to extend/overwrite the default values.
127
128 Returns: The created device instance.
129
130 """
131 entity = Device(user=user, **Dummy.update_copy(
132 Dummy.DEFAULT_DUMMY_DEVICE_VALUES, kwargs))
133 entity.save()
134 return entity
135
136 @staticmethod
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400137 def create_dummy_report(report_type, device, **kwargs):
138 """Create a dummy report instance of the given report class type.
139
140 The dummy instance is created and saved to the database.
141 Args:
142 report_type: The class of the report type to be created.
143 user: The device instance that the heartbeat should relate to
144 **kwargs:
145 Optional arguments to extend/overwrite the default values.
146
147 Returns: The created report instance.
148
149 """
150 if report_type == HeartBeat:
151 entity = HeartBeat(device=device, **Dummy.update_copy(
152 Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs))
153 elif report_type == Crashreport:
154 entity = Crashreport(device=device, **Dummy.update_copy(
155 Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES, kwargs))
156 else:
157 raise RuntimeError(
158 'No dummy report instance can be created for {}'.format(
159 report_type.__name__))
160 entity.save()
161 return entity
162
163 @staticmethod
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200164 def create_dummy_version(**kwargs):
165 """Create a dummy version instance.
166
167 The dummy instance is created and saved to the database.
168 Args:
169 **kwargs:
170 Optional arguments to extend/overwrite the default values.
171
172 Returns: The created version instance.
173
174 """
175 entity = Version(**Dummy.update_copy(
176 Dummy.DEFAULT_DUMMY_VERSION_VALUES, kwargs))
177 entity.save()
178 return entity
179
180 @staticmethod
181 def create_dummy_radio_version(**kwargs):
182 """Create a dummy radio version instance.
183
184 The dummy instance is created and saved to the database.
185 Args:
186 **kwargs:
187 Optional arguments to extend/overwrite the default values.
188
189 Returns: The created radio version instance.
190
191 """
192 entity = RadioVersion(**Dummy.update_copy(
193 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs))
194 entity.save()
195 return entity
196
197 @staticmethod
198 def create_dummy_daily_version(version, **kwargs):
199 """Create a dummy daily version instance.
200
201 The dummy instance is created and saved to the database.
202 Args:
203 **kwargs:
204 Optional arguments to extend/overwrite the default values.
205
206 Returns: The created daily version instance.
207
208 """
209 entity = VersionDaily(version=version, **Dummy.update_copy(
210 Dummy.DEFAULT_DUMMY_VERSION_DAILY_VALUES, kwargs))
211 entity.save()
212 return entity
213
214 @staticmethod
215 def create_dummy_daily_radio_version(version, **kwargs):
216 """Create a dummy daily radio version instance.
217
218 The dummy instance is created and saved to the database.
219 Args:
220 **kwargs:
221 Optional arguments to extend/overwrite the default values.
222
223 Returns: The created daily radio version instance.
224
225 """
226 entity = RadioVersionDaily(version=version, **Dummy.update_copy(
227 Dummy.DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES, kwargs))
228 entity.save()
229 return entity
230
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200231 @staticmethod
232 def create_dummy_stats_metadata(**kwargs):
233 """Create a dummy stats metadata instance.
234
235 The dummy instance is created and saved to the database.
236 Args:
237 **kwargs:
238 Optional arguments to extend/overwrite the default values.
239
240 Returns: The created stats metadata instance.
241
242 """
243 entity = StatsMetadata(**Dummy.update_copy(
244 Dummy.DEFAULT_DUMMY_STATSMETADATA_VALUES, kwargs))
245 entity.save()
246 return entity
247
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200248
249class _VersionTestCase(APITestCase):
250 """Abstract class for version-related test cases to inherit from."""
251
252 # The attribute name characterising the unicity of a stats entry (the
253 # named identifier)
254 unique_entry_name = 'build_fingerprint'
255 # The collection of unique entries to post
256 unique_entries = Dummy.BUILD_FINGERPRINTS
257 # The URL to retrieve the stats entries from
258 endpoint_url = reverse('hiccup_stats_api_v1_versions')
259
260 @classmethod
261 def setUpTestData(cls): # noqa: N802
262 """Create an admin user for accessing the API.
263
264 The APIClient that can be used to make authenticated requests to the
265 server is stored in self.admin.
266 """
267 admin_user = User.objects.create_superuser(
268 'somebody', 'somebody@example.com', 'thepassword')
269 cls.admin = APIClient()
270 cls.admin.force_authenticate(admin_user)
271
272 @staticmethod
273 def _create_dummy_version(**kwargs):
274 return Dummy.create_dummy_version(**kwargs)
275
276 def _get_with_params(self, url, params):
277 return self.admin.get('{}?{}'.format(url, urlencode(params)))
278
279 def _assert_result_length_is(self, response, count):
280 self.assertEqual(response.status_code, status.HTTP_200_OK)
281 self.assertIn('results', response.data)
282 self.assertIn('count', response.data)
283 self.assertEqual(response.data['count'], count)
284 self.assertEqual(len(response.data['results']), count)
285
286 def _assert_device_owner_has_no_get_access(self, entries_url):
287 # Create a user and device
288 user = Dummy.create_dummy_user()
289 device = Dummy.create_dummy_device(user=user)
290
291 # Create authenticated client
292 user = APIClient()
293 user.credentials(HTTP_AUTHORIZATION='Token ' + device.token)
294
295 # Try getting entries using the client
296 response = user.get(entries_url)
297 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
298
299 def _assert_filter_result_matches(self, filter_params, expected_result):
300 # List entities with filter
301 response = self._get_with_params(self.endpoint_url, filter_params)
302
303 # Expect only the single matching result to be returned
304 self._assert_result_length_is(response, 1)
305 self.assertEqual(response.data['results'][0][self.unique_entry_name],
306 getattr(expected_result, self.unique_entry_name))
307
308
309class VersionTestCase(_VersionTestCase):
310 """Test the Version and REST endpoint."""
311
312 def _create_version_entities(self):
313 versions = [
314 self._create_dummy_version(
315 **{self.unique_entry_name: unique_entry}
316 )
317 for unique_entry in self.unique_entries
318 ]
319 return versions
320
321 def test_list_versions_without_authentication(self):
322 """Test listing of versions without authentication."""
323 response = self.client.get(self.endpoint_url)
324 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
325
326 def test_list_versions_as_device_owner(self):
327 """Test listing of versions as device owner."""
328 self._assert_device_owner_has_no_get_access(self.endpoint_url)
329
330 def test_list_versions_empty_database(self):
331 """Test listing of versions on an empty database."""
332 response = self.admin.get(self.endpoint_url)
333 self._assert_result_length_is(response, 0)
334
335 def test_list_versions(self):
336 """Test listing versions."""
337 versions = self._create_version_entities()
338 response = self.admin.get(self.endpoint_url)
339 self._assert_result_length_is(response, len(versions))
340
341 def test_filter_versions_by_unique_entry_name(self):
342 """Test filtering versions by their unique entry name."""
343 versions = self._create_version_entities()
344 response = self.admin.get(self.endpoint_url)
345
346 # Listing all entities should return the correct result length
347 self._assert_result_length_is(response, len(versions))
348
349 # List entities with filter
350 filter_params = {
351 self.unique_entry_name: getattr(versions[0],
352 self.unique_entry_name)
353 }
354 self._assert_filter_result_matches(filter_params,
355 expected_result=versions[0])
356
357 def test_filter_versions_by_release_type(self):
358 """Test filtering versions by release type."""
359 # Create versions for all combinations of release types
360 versions = []
361 i = 0
362 for is_official_release in True, False:
363 for is_beta_release in True, False:
364 versions.append(self._create_dummy_version(**{
365 'is_official_release': is_official_release,
366 'is_beta_release': is_beta_release,
367 self.unique_entry_name: self.unique_entries[i]
368 }))
369 i += 1
370
371 # # Listing all entities should return the correct result length
372 response = self.admin.get(self.endpoint_url)
373 self._assert_result_length_is(response, len(versions))
374
375 # List each of the entities with the matching filter params
376 for version in versions:
377 filter_params = {
378 'is_official_release': version.is_official_release,
379 'is_beta_release': version.is_beta_release
380 }
381 self._assert_filter_result_matches(filter_params,
382 expected_result=version)
383
384 def test_filter_versions_by_first_seen_date(self):
385 """Test filtering versions by first seen date."""
386 versions = self._create_version_entities()
387
388 # Set the first seen date of an entity
389 versions[0].first_seen_on = Dummy.DATES[2]
390 versions[0].save()
391
392 # Listing all entities should return the correct result length
393 response = self.admin.get(self.endpoint_url)
394 self._assert_result_length_is(response, len(versions))
395
396 # Expect the single matching result to be returned
397 filter_params = {'first_seen_after': Dummy.DATES[2]}
398 self._assert_filter_result_matches(filter_params,
399 expected_result=versions[0])
400
401
402# pylint: disable=too-many-ancestors
403class RadioVersionTestCase(VersionTestCase):
404 """Test the RadioVersion REST endpoint."""
405
406 unique_entry_name = 'radio_version'
407 unique_entries = Dummy.RADIO_VERSIONS
408 endpoint_url = reverse('hiccup_stats_api_v1_radio_versions')
409
410 @staticmethod
411 def _create_dummy_version(**kwargs):
412 return Dummy.create_dummy_radio_version(**kwargs)
413
414
415class VersionDailyTestCase(_VersionTestCase):
416 """Test the VersionDaily REST endpoint."""
417
418 endpoint_url = reverse('hiccup_stats_api_v1_version_daily')
419
420 @staticmethod
421 def _create_dummy_daily_version(version, **kwargs):
422 return Dummy.create_dummy_daily_version(version, **kwargs)
423
424 def _create_version_entities(self):
425 versions = [
426 self._create_dummy_version(
427 **{self.unique_entry_name: unique_entry}
428 )
429 for unique_entry in self.unique_entries
430 ]
431 versions_daily = [
432 self._create_dummy_daily_version(version=version)
433 for version in versions
434 ]
435 return versions_daily
436
437 def test_list_daily_versions_without_authentication(self):
438 """Test listing of daily versions without authentication."""
439 response = self.client.get(self.endpoint_url)
440 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
441
442 def test_list_daily_versions_as_device_owner(self):
443 """Test listing of daily versions as device owner."""
444 self._assert_device_owner_has_no_get_access(self.endpoint_url)
445
446 def test_list_daily_versions_empty_database(self):
447 """Test listing of daily versions on an empty database."""
448 response = self.admin.get(self.endpoint_url)
449 self._assert_result_length_is(response, 0)
450
451 def test_list_daily_versions(self):
452 """Test listing daily versions."""
453 versions_daily = self._create_version_entities()
454 response = self.admin.get(self.endpoint_url)
455 self._assert_result_length_is(response, len(versions_daily))
456
457 def test_filter_daily_versions_by_version(self):
458 """Test filtering versions by the version they relate to."""
459 # Create VersionDaily entities
460 versions = self._create_version_entities()
461
462 # Listing all entities should return the correct result length
463 response = self.admin.get(self.endpoint_url)
464 self._assert_result_length_is(response, len(versions))
465
466 # List entities with filter
467 param_name = 'version__' + self.unique_entry_name
468 filter_params = {
469 param_name: getattr(versions[0].version, self.unique_entry_name)
470 }
471 self._assert_filter_result_matches(filter_params,
472 expected_result=versions[0].version)
473
474 def test_filter_daily_versions_by_date(self):
475 """Test filtering daily versions by date."""
476 # Create Version and VersionDaily entities
477 versions = self._create_version_entities()
478
479 # Update the date
480 versions[0].date = Dummy.DATES[2]
481 versions[0].save()
482
483 # Listing all entities should return the correct result length
484 response = self.admin.get(self.endpoint_url)
485 self._assert_result_length_is(response, len(versions))
486
487 # Expect the single matching result to be returned
488 filter_params = {'date': versions[0].date}
489 self._assert_filter_result_matches(filter_params,
490 expected_result=versions[0].version)
491
492
493class RadioVersionDailyTestCase(VersionDailyTestCase):
494 """Test the RadioVersionDaily REST endpoint."""
495
496 unique_entry_name = 'radio_version'
497 unique_entries = Dummy.RADIO_VERSIONS
498 endpoint_url = reverse('hiccup_stats_api_v1_radio_version_daily')
499
500 @staticmethod
501 def _create_dummy_version(**kwargs):
502 entity = RadioVersion(**Dummy.update_copy(
503 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs))
504 entity.save()
505 return entity
506
507 @staticmethod
508 def _create_dummy_daily_version(version, **kwargs):
509 return Dummy.create_dummy_daily_radio_version(version, **kwargs)
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400510
511
512class StatsCommandVersionsTestCase(TestCase):
513 """Test the generation of Version stats with the stats command."""
514
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400515 # FIXME: Test for false duplicates: same timestamps but different UUIDs
516 # FIXME: Test that the 'released_on' field changes or not once an older
517 # report has been sent depending on whether the field has been manually
518 # changed
519 # FIXME: Test that tests the daily version stats
520 # FIXME: Test creating stats from reports of different devices/users.
521
522 # The class of the version type to be tested
523 version_class = Version
524 # The attribute name characterising the unicity of a stats entry (the
525 # named identifier)
526 unique_entry_name = 'build_fingerprint'
527 # The collection of unique entries to post
528 unique_entries = Dummy.BUILD_FINGERPRINTS
529
530 def _create_reports(self, report_type, unique_entry_name, device,
531 number, **kwargs):
532 # Create reports with distinct timestamps
533 now = datetime.now(pytz.utc)
534 for i in range(number):
535 report_date = now - timedelta(milliseconds=i)
536 report_attributes = {
537 self.unique_entry_name: unique_entry_name,
538 'device': device,
539 'date': report_date
540 }
541 report_attributes.update(**kwargs)
542 Dummy.create_dummy_report(report_type, **report_attributes)
543
544 def test_stats_calculation(self):
545 """Test generation of a Version instance."""
546 user = Dummy.create_dummy_user()
547 device = Dummy.create_dummy_device(user=user)
548 heartbeat = Dummy.create_dummy_report(HeartBeat, device=device)
549
550 # Expect that we do not have the Version before updating the stats
551 get_params = {
552 self.unique_entry_name: getattr(heartbeat, self.unique_entry_name)
553 }
554 self.assertRaises(self.version_class.DoesNotExist,
555 self.version_class.objects.get, **get_params)
556
557 # Run the command to update the database
558 call_command('stats', 'update')
559
560 # Assume that a corresponding Version instance has been created
561 version = self.version_class.objects.get(**get_params)
562 self.assertIsNotNone(version)
563
564 def _assert_older_report_updates_version_date(self, report_type):
565 """Validate that older reports sent later affect the version date."""
566 user = Dummy.create_dummy_user()
567 device = Dummy.create_dummy_device(user=user)
568 report = Dummy.create_dummy_report(report_type, device=device)
569
570 # Run the command to update the database
571 call_command('stats', 'update')
572
573 get_params = {
574 self.unique_entry_name: getattr(report, self.unique_entry_name)
575 }
576 version = self.version_class.objects.get(**get_params)
577
578 self.assertEqual(report.date.date(), version.first_seen_on)
579
580 # Create a new report from an earlier point in time
581 report_time_2 = report.date - timedelta(weeks=1)
582 Dummy.create_dummy_report(report_type, device=device,
583 date=report_time_2)
584
585 # Run the command to update the database
586 call_command('stats', 'update')
587
588 # Get the same version object from before
589 version = self.version_class.objects.get(**get_params)
590
591 # Validate that the date matches the report recently sent
592 self.assertEqual(report_time_2.date(), version.first_seen_on)
593
594 def test_older_heartbeat_updates_version_date(self):
595 """Validate updating version date with older heartbeats."""
596 self._assert_older_report_updates_version_date(HeartBeat)
597
598 def test_older_crash_report_updates_version_date(self):
599 """Validate updating version date with older crash reports."""
600 self._assert_older_report_updates_version_date(Crashreport)
601
602 def test_entries_are_unique(self):
603 """Validate the entries' unicity and value."""
604 # Create some reports
605 user = Dummy.create_dummy_user()
606 device = Dummy.create_dummy_device(user=user)
607 for unique_entry in self.unique_entries:
608 self._create_reports(HeartBeat, unique_entry, device, 10)
609
610 # Run the command to update the database
611 call_command('stats', 'update')
612
613 # Check whether the correct amount of distinct versions have been
614 # created
615 versions = self.version_class.objects.all()
616 for version in versions:
617 self.assertIn(getattr(version, self.unique_entry_name),
618 self.unique_entries)
619 self.assertEqual(len(versions), len(self.unique_entries))
620
621 def _assert_counter_distribution_is_correct(self, report_type, numbers,
622 counter_attribute_name,
623 **kwargs):
624 """Validate a counter distribution in the database."""
625 if len(numbers) != len(self.unique_entries):
626 raise ValueError('The length of the numbers list must match the '
627 'length of self.unique_entries in the test class'
628 '({} != {})'.format(len(numbers),
629 len(self.unique_entries)))
630 # Create some reports
631 user = Dummy.create_dummy_user()
632 device = Dummy.create_dummy_device(user=user)
633 for unique_entry, num in zip(self.unique_entries, numbers):
634 self._create_reports(report_type, unique_entry, device, num,
635 **kwargs)
636
637 # Run the command to update the database
638 call_command('stats', 'update')
639
640 # Check whether the numbers of reports match
641 for version in self.version_class.objects.all():
642 unique_entry_name = getattr(version, self.unique_entry_name)
643 num = numbers[self.unique_entries.index(unique_entry_name)]
644 self.assertEqual(num, getattr(version, counter_attribute_name))
645
646 def test_heartbeats_counter(self):
647 """Test the calculation of the heartbeats counter."""
648 numbers = [10, 7, 8, 5]
649 counter_attribute_name = 'heartbeats'
650 self._assert_counter_distribution_is_correct(HeartBeat, numbers,
651 counter_attribute_name)
652
653 def test_crash_reports_counter(self):
654 """Test the calculation of the crashreports counter."""
655 numbers = [2, 5, 0, 3]
656 counter_attribute_name = 'prob_crashes'
657 boot_reason_param = {'boot_reason': Crashreport.BOOT_REASON_UNKOWN}
658 self._assert_counter_distribution_is_correct(Crashreport, numbers,
659 counter_attribute_name,
660 **boot_reason_param)
661
662 def test_smpl_reports_counter(self):
663 """Test the calculation of the smpl reports counter."""
664 numbers = [1, 3, 4, 0]
665 counter_attribute_name = 'smpl'
666 boot_reason_param = {'boot_reason': Crashreport.BOOT_REASON_RTC_ALARM}
667 self._assert_counter_distribution_is_correct(Crashreport, numbers,
668 counter_attribute_name,
669 **boot_reason_param)
670
671 def test_other_reports_counter(self):
672 """Test the calculation of the other reports counter."""
673 numbers = [0, 2, 1, 2]
674 counter_attribute_name = 'other'
675 boot_reason_param = {'boot_reason': "random boot reason"}
676 self._assert_counter_distribution_is_correct(Crashreport, numbers,
677 counter_attribute_name,
678 **boot_reason_param)
679
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400680 def _assert_duplicates_are_ignored(self, report_type, device,
681 counter_attribute_name, **kwargs):
682 """Validate that reports with duplicate timestamps are ignored."""
683 # Create a report
684 report = Dummy.create_dummy_report(report_type, device=device,
685 **kwargs)
686
687 # Create a second report with the same timestamp
688 Dummy.create_dummy_report(report_type, device=device,
689 date=report.date, **kwargs)
690
691 # Run the command to update the database
692 call_command('stats', 'update')
693
694 # Get the corresponding version instance from the database
695 get_params = {
696 self.unique_entry_name: getattr(report, self.unique_entry_name)
697 }
698 version = self.version_class.objects.get(**get_params)
699
700 # Assert that the report with the duplicate timestamp is not
701 # counted, i.e. only 1 report is counted.
702 self.assertEqual(getattr(version, counter_attribute_name), 1)
703
704 def test_heartbeat_duplicates_are_ignored(self):
705 """Validate that heartbeat duplicates are ignored."""
706 counter_attribute_name = 'heartbeats'
707 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
708 self._assert_duplicates_are_ignored(HeartBeat, device,
709 counter_attribute_name)
710
711 def test_crash_report_duplicates_are_ignored(self):
712 """Validate that crash report duplicates are ignored."""
713 counter_attribute_name = 'prob_crashes'
714 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
715 for i, boot_reason in enumerate(Crashreport.CRASH_BOOT_REASONS):
716 params = {'boot_reason': boot_reason,
717 self.unique_entry_name: self.unique_entries[i]}
718 self._assert_duplicates_are_ignored(Crashreport, device,
719 counter_attribute_name,
720 **params)
721
722 def test_smpl_report_duplicates_are_ignored(self):
723 """Validate that smpl report duplicates are ignored."""
724 counter_attribute_name = 'smpl'
725 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
726 for i, boot_reason in enumerate(Crashreport.SMPL_BOOT_REASONS):
727 params = {'boot_reason': boot_reason,
728 self.unique_entry_name: self.unique_entries[i]}
729 self._assert_duplicates_are_ignored(Crashreport, device,
730 counter_attribute_name,
731 **params)
732
733 def test_other_report_duplicates_are_ignored(self):
734 """Validate that other report duplicates are ignored."""
735 counter_attribute_name = 'other'
736 params = {'boot_reason': 'random boot reason'}
737 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
738 self._assert_duplicates_are_ignored(Crashreport, device,
739 counter_attribute_name,
740 **params)
741
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400742
743# pylint: disable=too-many-ancestors
744class StatsCommandRadioVersionsTestCase(StatsCommandVersionsTestCase):
745 """Test the generation of RadioVersion stats with the stats command."""
746
747 version_class = RadioVersion
748 unique_entry_name = 'radio_version'
749 unique_entries = Dummy.RADIO_VERSIONS
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400750
751
752class CommandDebugOutputTestCase(TestCase):
753 """Test the reset and update commands debug output."""
754
755 # Additional positional arguments to pass to the commands
756 _CMD_ARGS = [
757 '--no-color',
758 '-v 2',
759 ]
760
761 # The stats models
762 _STATS_MODELS = [Version, VersionDaily, RadioVersion, RadioVersionDaily]
763 # The models that will generate an output
764 _ALL_MODELS = _STATS_MODELS + [StatsMetadata]
765 _COUNTER_NAMES = ['heartbeats', 'crashes', 'smpl', 'other']
766 _COUNTER_ACTIONS = ['created', 'updated']
767
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200768 def _assert_command_output_matches(self, command, number, facts, models):
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400769 """Validate the debug output of a command.
770
771 The debug output is matched against the facts and models given in
772 the parameters.
773 """
774 buffer = StringIO()
775 call_command('stats', command, *self._CMD_ARGS, stdout=buffer)
776 output = buffer.getvalue().splitlines()
777
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200778 expected_output = '{number} {model} {fact}'
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400779 for model in models:
780 for fact in facts:
781 self.assertIn(
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200782 expected_output.format(number=number,
783 model=model.__name__,
784 fact=fact),
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400785 output)
786
787 def test_reset_command_on_empty_db(self):
788 """Test the reset command on an empty database.
789
790 The reset command should yield nothing on an empty database.
791 """
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200792 self._assert_command_output_matches('reset', 0, ['deleted'],
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400793 self._ALL_MODELS)
794
795 def test_update_command_on_empty_db(self):
796 """Test the update command on an empty database.
797
798 The update command should yield nothing on an empty database.
799 """
800 pattern = '{action} for counter {counter}'
801 facts = [
802 pattern.format(action=counter_action, counter=counter_name)
803 for counter_action in self._COUNTER_ACTIONS
804 for counter_name in self._COUNTER_NAMES]
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200805 self._assert_command_output_matches('update', 0, facts,
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400806 self._STATS_MODELS)
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200807
808 def test_reset_command_deletion_of_instances(self):
809 """Test the deletion of stats model instances with the reset command.
810
811 This test validates that model instances get deleted when the
812 reset command is called on a database that only contains a single
813 model instance for each class.
814 """
815 # Create dummy version instances
816 version = Dummy.create_dummy_version()
817 radio_version = Dummy.create_dummy_radio_version()
818 Dummy.create_dummy_daily_version(version)
819 Dummy.create_dummy_daily_radio_version(radio_version)
820 Dummy.create_dummy_stats_metadata()
821
822 # We expect that the model instances get deleted
823 self._assert_command_output_matches('reset', 1, ['deleted'],
824 self._ALL_MODELS)