blob: 961cc19cfaeaebc7f567d514234f8624e1e0424e [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 Nikolaus3a09c6e2018-09-04 12:17:45 +02004import unittest
5
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +02006import pytz
Dirk Vogt62ff7f22017-05-04 16:07:21 +02007
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +04008from django.core.management import call_command
9from django.test import TestCase
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020010from django.urls import reverse
11from django.utils.http import urlencode
12
13from rest_framework import status
14from rest_framework.test import APITestCase, APIClient
15
16from crashreport_stats.models import (
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020017 Version,
18 VersionDaily,
19 RadioVersion,
20 RadioVersionDaily,
21 StatsMetadata,
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020022)
23
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040024from crashreports.models import User, Device, Crashreport, HeartBeat
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020025
26
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020027class Dummy:
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020028 """Class for creating dummy instances for testing."""
29
30 # Valid unique entries
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020031 BUILD_FINGERPRINTS = [
32 (
Mitja Nikolaus19cf9a92018-08-23 18:15:01 +020033 "Fairphone/FP2/FP2:5.1/FP2/r4275.1_FP2_gms76_1.13.0"
34 ":user/release-keys"
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020035 ),
36 (
37 "Fairphone/FP2/FP2:5.1.1/FP2-gms75.1.13.0/FP2-gms75.1.13.0"
38 ":user/release-keys"
39 ),
40 (
41 "Fairphone/FP2/FP2:6.0.1/FP2-gms-18.04.1/FP2-gms-18.04.1"
42 ":user/release-keys"
43 ),
Mitja Nikolaus19cf9a92018-08-23 18:15:01 +020044 ("Fairphone/FP2/FP2:7.1.2/18.07.2/gms-7480c31d:user/release-keys"),
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020045 ]
46 RADIO_VERSIONS = [
47 "4437.1-FP2-0-07",
48 "4437.1-FP2-0-08",
49 "4437.1-FP2-0-09",
50 "4437.1-FP2-0-10",
51 ]
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020052
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +020053 USERNAMES = ["testuser1", "testuser2"]
54
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020055 DATES = [date(2018, 3, 19), date(2018, 3, 26), date(2018, 5, 1)]
56
57 DEFAULT_DUMMY_VERSION_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020058 "build_fingerprint": BUILD_FINGERPRINTS[0],
59 "first_seen_on": DATES[1],
60 "released_on": DATES[0],
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020061 }
62
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020063 DEFAULT_DUMMY_VERSION_DAILY_VALUES = {"date": DATES[1]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020064
65 DEFAULT_DUMMY_RADIO_VERSION_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020066 "radio_version": RADIO_VERSIONS[0],
67 "first_seen_on": DATES[1],
68 "released_on": DATES[0],
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020069 }
70
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020071 DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES = {"date": DATES[1]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020072
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +020073 DEFAULT_DUMMY_STATSMETADATA_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020074 "updated_at": datetime(2018, 6, 15, 2, 12, 24, tzinfo=pytz.utc)
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +020075 }
76
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020077 DEFAULT_DUMMY_DEVICE_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020078 "board_date": datetime(2015, 12, 15, 1, 23, 45, tzinfo=pytz.utc),
79 "chipset": "Qualcomm MSM8974PRO-AA",
80 "token": "64111c62d521fb4724454ca6dea27e18f93ef56e",
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020081 }
82
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +020083 DEFAULT_DUMMY_USER_VALUES = {"username": USERNAMES[0]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020084
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040085 DEFAULT_DUMMY_HEARTBEAT_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020086 "app_version": 10100,
87 "uptime": (
88 "up time: 16 days, 21:49:56, idle time: 5 days, 20:55:04, "
89 "sleep time: 10 days, 20:46:27"
90 ),
91 "build_fingerprint": BUILD_FINGERPRINTS[0],
92 "radio_version": RADIO_VERSIONS[0],
93 "date": datetime(2018, 3, 19, tzinfo=pytz.utc),
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040094 }
95
96 DEFAULT_DUMMY_CRASHREPORT_VALUES = DEFAULT_DUMMY_HEARTBEAT_VALUES.copy()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020097 DEFAULT_DUMMY_CRASHREPORT_VALUES.update(
98 {
99 "is_fake_report": 0,
100 "boot_reason": Crashreport.BOOT_REASON_UNKOWN,
101 "power_on_reason": "it was powered on",
102 "power_off_reason": "something happened and it went off",
103 }
104 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400105
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200106 @staticmethod
107 def update_copy(original, update):
108 """Merge fields of update into a copy of original."""
109 data = original.copy()
110 data.update(update)
111 return data
112
113 @staticmethod
114 def create_dummy_user(**kwargs):
115 """Create a dummy user instance.
116
117 The dummy instance is created and saved to the database.
118 Args:
119 **kwargs:
120 Optional arguments to extend/overwrite the default values.
121
122 Returns: The created user instance.
123
124 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200125 entity = User(
126 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_USER_VALUES, kwargs)
127 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200128 entity.save()
129 return entity
130
131 @staticmethod
132 def create_dummy_device(user, **kwargs):
133 """Create a dummy device instance.
134
135 The dummy instance is created and saved to the database.
136 Args:
137 user: The user instance that the device should relate to
138 **kwargs:
139 Optional arguments to extend/overwrite the default values.
140
141 Returns: The created device instance.
142
143 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200144 entity = Device(
145 user=user,
146 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_DEVICE_VALUES, kwargs)
147 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200148 entity.save()
149 return entity
150
151 @staticmethod
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400152 def create_dummy_report(report_type, device, **kwargs):
153 """Create a dummy report instance of the given report class type.
154
155 The dummy instance is created and saved to the database.
156 Args:
157 report_type: The class of the report type to be created.
158 user: The device instance that the heartbeat should relate to
159 **kwargs:
160 Optional arguments to extend/overwrite the default values.
161
162 Returns: The created report instance.
163
164 """
165 if report_type == HeartBeat:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200166 entity = HeartBeat(
167 device=device,
168 **Dummy.update_copy(
169 Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs
170 )
171 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400172 elif report_type == Crashreport:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200173 entity = Crashreport(
174 device=device,
175 **Dummy.update_copy(
176 Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES, kwargs
177 )
178 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400179 else:
180 raise RuntimeError(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200181 "No dummy report instance can be created for {}".format(
182 report_type.__name__
183 )
184 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400185 entity.save()
186 return entity
187
188 @staticmethod
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200189 def create_dummy_version(**kwargs):
190 """Create a dummy version instance.
191
192 The dummy instance is created and saved to the database.
193 Args:
194 **kwargs:
195 Optional arguments to extend/overwrite the default values.
196
197 Returns: The created version instance.
198
199 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200200 entity = Version(
201 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_VERSION_VALUES, kwargs)
202 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200203 entity.save()
204 return entity
205
206 @staticmethod
207 def create_dummy_radio_version(**kwargs):
208 """Create a dummy radio version instance.
209
210 The dummy instance is created and saved to the database.
211 Args:
212 **kwargs:
213 Optional arguments to extend/overwrite the default values.
214
215 Returns: The created radio version instance.
216
217 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200218 entity = RadioVersion(
219 **Dummy.update_copy(
220 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs
221 )
222 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200223 entity.save()
224 return entity
225
226 @staticmethod
227 def create_dummy_daily_version(version, **kwargs):
228 """Create a dummy daily version instance.
229
230 The dummy instance is created and saved to the database.
231 Args:
232 **kwargs:
233 Optional arguments to extend/overwrite the default values.
234
235 Returns: The created daily version instance.
236
237 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200238 entity = VersionDaily(
239 version=version,
240 **Dummy.update_copy(
241 Dummy.DEFAULT_DUMMY_VERSION_DAILY_VALUES, kwargs
242 )
243 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200244 entity.save()
245 return entity
246
247 @staticmethod
248 def create_dummy_daily_radio_version(version, **kwargs):
249 """Create a dummy daily radio version instance.
250
251 The dummy instance is created and saved to the database.
252 Args:
253 **kwargs:
254 Optional arguments to extend/overwrite the default values.
255
256 Returns: The created daily radio version instance.
257
258 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200259 entity = RadioVersionDaily(
260 version=version,
261 **Dummy.update_copy(
262 Dummy.DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES, kwargs
263 )
264 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200265 entity.save()
266 return entity
267
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200268 @staticmethod
269 def create_dummy_stats_metadata(**kwargs):
270 """Create a dummy stats metadata instance.
271
272 The dummy instance is created and saved to the database.
273 Args:
274 **kwargs:
275 Optional arguments to extend/overwrite the default values.
276
277 Returns: The created stats metadata instance.
278
279 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200280 entity = StatsMetadata(
281 **Dummy.update_copy(
282 Dummy.DEFAULT_DUMMY_STATSMETADATA_VALUES, kwargs
283 )
284 )
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200285 entity.save()
286 return entity
287
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200288
289class _VersionTestCase(APITestCase):
290 """Abstract class for version-related test cases to inherit from."""
291
292 # The attribute name characterising the unicity of a stats entry (the
293 # named identifier)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200294 unique_entry_name = "build_fingerprint"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200295 # The collection of unique entries to post
296 unique_entries = Dummy.BUILD_FINGERPRINTS
297 # The URL to retrieve the stats entries from
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200298 endpoint_url = reverse("hiccup_stats_api_v1_versions")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200299
300 @classmethod
301 def setUpTestData(cls): # noqa: N802
302 """Create an admin user for accessing the API.
303
304 The APIClient that can be used to make authenticated requests to the
305 server is stored in self.admin.
306 """
307 admin_user = User.objects.create_superuser(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200308 "somebody", "somebody@example.com", "thepassword"
309 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200310 cls.admin = APIClient()
311 cls.admin.force_authenticate(admin_user)
312
313 @staticmethod
314 def _create_dummy_version(**kwargs):
315 return Dummy.create_dummy_version(**kwargs)
316
317 def _get_with_params(self, url, params):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200318 return self.admin.get("{}?{}".format(url, urlencode(params)))
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200319
320 def _assert_result_length_is(self, response, count):
321 self.assertEqual(response.status_code, status.HTTP_200_OK)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200322 self.assertIn("results", response.data)
323 self.assertIn("count", response.data)
324 self.assertEqual(response.data["count"], count)
325 self.assertEqual(len(response.data["results"]), count)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200326
327 def _assert_device_owner_has_no_get_access(self, entries_url):
328 # Create a user and device
329 user = Dummy.create_dummy_user()
330 device = Dummy.create_dummy_device(user=user)
331
332 # Create authenticated client
333 user = APIClient()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200334 user.credentials(HTTP_AUTHORIZATION="Token " + device.token)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200335
336 # Try getting entries using the client
337 response = user.get(entries_url)
338 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
339
340 def _assert_filter_result_matches(self, filter_params, expected_result):
341 # List entities with filter
342 response = self._get_with_params(self.endpoint_url, filter_params)
343
344 # Expect only the single matching result to be returned
345 self._assert_result_length_is(response, 1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200346 self.assertEqual(
347 response.data["results"][0][self.unique_entry_name],
348 getattr(expected_result, self.unique_entry_name),
349 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200350
351
352class VersionTestCase(_VersionTestCase):
353 """Test the Version and REST endpoint."""
354
355 def _create_version_entities(self):
356 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200357 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200358 for unique_entry in self.unique_entries
359 ]
360 return versions
361
362 def test_list_versions_without_authentication(self):
363 """Test listing of versions without authentication."""
364 response = self.client.get(self.endpoint_url)
365 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
366
367 def test_list_versions_as_device_owner(self):
368 """Test listing of versions as device owner."""
369 self._assert_device_owner_has_no_get_access(self.endpoint_url)
370
371 def test_list_versions_empty_database(self):
372 """Test listing of versions on an empty database."""
373 response = self.admin.get(self.endpoint_url)
374 self._assert_result_length_is(response, 0)
375
376 def test_list_versions(self):
377 """Test listing versions."""
378 versions = self._create_version_entities()
379 response = self.admin.get(self.endpoint_url)
380 self._assert_result_length_is(response, len(versions))
381
382 def test_filter_versions_by_unique_entry_name(self):
383 """Test filtering versions by their unique entry name."""
384 versions = self._create_version_entities()
385 response = self.admin.get(self.endpoint_url)
386
387 # Listing all entities should return the correct result length
388 self._assert_result_length_is(response, len(versions))
389
390 # List entities with filter
391 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200392 self.unique_entry_name: getattr(versions[0], self.unique_entry_name)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200393 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200394 self._assert_filter_result_matches(
395 filter_params, expected_result=versions[0]
396 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200397
398 def test_filter_versions_by_release_type(self):
399 """Test filtering versions by release type."""
400 # Create versions for all combinations of release types
401 versions = []
402 i = 0
403 for is_official_release in True, False:
404 for is_beta_release in True, False:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200405 versions.append(
406 self._create_dummy_version(
407 **{
408 "is_official_release": is_official_release,
409 "is_beta_release": is_beta_release,
410 self.unique_entry_name: self.unique_entries[i],
411 }
412 )
413 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200414 i += 1
415
416 # # Listing all entities should return the correct result length
417 response = self.admin.get(self.endpoint_url)
418 self._assert_result_length_is(response, len(versions))
419
420 # List each of the entities with the matching filter params
421 for version in versions:
422 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200423 "is_official_release": version.is_official_release,
424 "is_beta_release": version.is_beta_release,
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200425 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200426 self._assert_filter_result_matches(
427 filter_params, expected_result=version
428 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200429
430 def test_filter_versions_by_first_seen_date(self):
431 """Test filtering versions by first seen date."""
432 versions = self._create_version_entities()
433
434 # Set the first seen date of an entity
435 versions[0].first_seen_on = Dummy.DATES[2]
436 versions[0].save()
437
438 # Listing all entities should return the correct result length
439 response = self.admin.get(self.endpoint_url)
440 self._assert_result_length_is(response, len(versions))
441
442 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200443 filter_params = {"first_seen_after": Dummy.DATES[2]}
444 self._assert_filter_result_matches(
445 filter_params, expected_result=versions[0]
446 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200447
448
449# pylint: disable=too-many-ancestors
450class RadioVersionTestCase(VersionTestCase):
451 """Test the RadioVersion REST endpoint."""
452
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200453 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200454 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200455 endpoint_url = reverse("hiccup_stats_api_v1_radio_versions")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200456
457 @staticmethod
458 def _create_dummy_version(**kwargs):
459 return Dummy.create_dummy_radio_version(**kwargs)
460
461
462class VersionDailyTestCase(_VersionTestCase):
463 """Test the VersionDaily REST endpoint."""
464
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200465 endpoint_url = reverse("hiccup_stats_api_v1_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200466
467 @staticmethod
468 def _create_dummy_daily_version(version, **kwargs):
469 return Dummy.create_dummy_daily_version(version, **kwargs)
470
471 def _create_version_entities(self):
472 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200473 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200474 for unique_entry in self.unique_entries
475 ]
476 versions_daily = [
477 self._create_dummy_daily_version(version=version)
478 for version in versions
479 ]
480 return versions_daily
481
482 def test_list_daily_versions_without_authentication(self):
483 """Test listing of daily versions without authentication."""
484 response = self.client.get(self.endpoint_url)
485 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
486
487 def test_list_daily_versions_as_device_owner(self):
488 """Test listing of daily versions as device owner."""
489 self._assert_device_owner_has_no_get_access(self.endpoint_url)
490
491 def test_list_daily_versions_empty_database(self):
492 """Test listing of daily versions on an empty database."""
493 response = self.admin.get(self.endpoint_url)
494 self._assert_result_length_is(response, 0)
495
496 def test_list_daily_versions(self):
497 """Test listing daily versions."""
498 versions_daily = self._create_version_entities()
499 response = self.admin.get(self.endpoint_url)
500 self._assert_result_length_is(response, len(versions_daily))
501
502 def test_filter_daily_versions_by_version(self):
503 """Test filtering versions by the version they relate to."""
504 # Create VersionDaily entities
505 versions = self._create_version_entities()
506
507 # Listing all entities should return the correct result length
508 response = self.admin.get(self.endpoint_url)
509 self._assert_result_length_is(response, len(versions))
510
511 # List entities with filter
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200512 param_name = "version__" + self.unique_entry_name
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200513 filter_params = {
514 param_name: getattr(versions[0].version, self.unique_entry_name)
515 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200516 self._assert_filter_result_matches(
517 filter_params, expected_result=versions[0].version
518 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200519
520 def test_filter_daily_versions_by_date(self):
521 """Test filtering daily versions by date."""
522 # Create Version and VersionDaily entities
523 versions = self._create_version_entities()
524
525 # Update the date
526 versions[0].date = Dummy.DATES[2]
527 versions[0].save()
528
529 # Listing all entities should return the correct result length
530 response = self.admin.get(self.endpoint_url)
531 self._assert_result_length_is(response, len(versions))
532
533 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200534 filter_params = {"date": versions[0].date}
535 self._assert_filter_result_matches(
536 filter_params, expected_result=versions[0].version
537 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200538
539
540class RadioVersionDailyTestCase(VersionDailyTestCase):
541 """Test the RadioVersionDaily REST endpoint."""
542
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200543 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200544 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200545 endpoint_url = reverse("hiccup_stats_api_v1_radio_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200546
547 @staticmethod
548 def _create_dummy_version(**kwargs):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200549 entity = RadioVersion(
550 **Dummy.update_copy(
551 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs
552 )
553 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200554 entity.save()
555 return entity
556
557 @staticmethod
558 def _create_dummy_daily_version(version, **kwargs):
559 return Dummy.create_dummy_daily_radio_version(version, **kwargs)
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400560
561
562class StatsCommandVersionsTestCase(TestCase):
563 """Test the generation of Version stats with the stats command."""
564
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400565 # The class of the version type to be tested
566 version_class = Version
567 # The attribute name characterising the unicity of a stats entry (the
568 # named identifier)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200569 unique_entry_name = "build_fingerprint"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400570 # The collection of unique entries to post
571 unique_entries = Dummy.BUILD_FINGERPRINTS
572
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200573 def _create_reports(
574 self, report_type, unique_entry_name, device, number, **kwargs
575 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400576 # Create reports with distinct timestamps
577 now = datetime.now(pytz.utc)
578 for i in range(number):
579 report_date = now - timedelta(milliseconds=i)
580 report_attributes = {
581 self.unique_entry_name: unique_entry_name,
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200582 "device": device,
583 "date": report_date,
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400584 }
585 report_attributes.update(**kwargs)
586 Dummy.create_dummy_report(report_type, **report_attributes)
587
588 def test_stats_calculation(self):
589 """Test generation of a Version instance."""
590 user = Dummy.create_dummy_user()
591 device = Dummy.create_dummy_device(user=user)
592 heartbeat = Dummy.create_dummy_report(HeartBeat, device=device)
593
594 # Expect that we do not have the Version before updating the stats
595 get_params = {
596 self.unique_entry_name: getattr(heartbeat, self.unique_entry_name)
597 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200598 self.assertRaises(
599 self.version_class.DoesNotExist,
600 self.version_class.objects.get,
601 **get_params
602 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400603
604 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200605 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400606
607 # Assume that a corresponding Version instance has been created
608 version = self.version_class.objects.get(**get_params)
609 self.assertIsNotNone(version)
610
611 def _assert_older_report_updates_version_date(self, report_type):
612 """Validate that older reports sent later affect the version date."""
613 user = Dummy.create_dummy_user()
614 device = Dummy.create_dummy_device(user=user)
615 report = Dummy.create_dummy_report(report_type, device=device)
616
617 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200618 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400619
620 get_params = {
621 self.unique_entry_name: getattr(report, self.unique_entry_name)
622 }
623 version = self.version_class.objects.get(**get_params)
624
625 self.assertEqual(report.date.date(), version.first_seen_on)
626
627 # Create a new report from an earlier point in time
628 report_time_2 = report.date - timedelta(weeks=1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200629 Dummy.create_dummy_report(
630 report_type, device=device, date=report_time_2
631 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400632
633 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200634 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400635
636 # Get the same version object from before
637 version = self.version_class.objects.get(**get_params)
638
639 # Validate that the date matches the report recently sent
640 self.assertEqual(report_time_2.date(), version.first_seen_on)
641
642 def test_older_heartbeat_updates_version_date(self):
643 """Validate updating version date with older heartbeats."""
644 self._assert_older_report_updates_version_date(HeartBeat)
645
646 def test_older_crash_report_updates_version_date(self):
647 """Validate updating version date with older crash reports."""
648 self._assert_older_report_updates_version_date(Crashreport)
649
650 def test_entries_are_unique(self):
651 """Validate the entries' unicity and value."""
652 # Create some reports
653 user = Dummy.create_dummy_user()
654 device = Dummy.create_dummy_device(user=user)
655 for unique_entry in self.unique_entries:
656 self._create_reports(HeartBeat, unique_entry, device, 10)
657
658 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200659 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400660
661 # Check whether the correct amount of distinct versions have been
662 # created
663 versions = self.version_class.objects.all()
664 for version in versions:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200665 self.assertIn(
666 getattr(version, self.unique_entry_name), self.unique_entries
667 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400668 self.assertEqual(len(versions), len(self.unique_entries))
669
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200670 def _assert_counter_distribution_is_correct(
671 self, report_type, numbers, counter_attribute_name, **kwargs
672 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400673 """Validate a counter distribution in the database."""
674 if len(numbers) != len(self.unique_entries):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200675 raise ValueError(
676 "The length of the numbers list must match the "
677 "length of self.unique_entries in the test class"
678 "({} != {})".format(len(numbers), len(self.unique_entries))
679 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400680 # Create some reports
681 user = Dummy.create_dummy_user()
682 device = Dummy.create_dummy_device(user=user)
683 for unique_entry, num in zip(self.unique_entries, numbers):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200684 self._create_reports(
685 report_type, unique_entry, device, num, **kwargs
686 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400687
688 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200689 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400690
691 # Check whether the numbers of reports match
692 for version in self.version_class.objects.all():
693 unique_entry_name = getattr(version, self.unique_entry_name)
694 num = numbers[self.unique_entries.index(unique_entry_name)]
695 self.assertEqual(num, getattr(version, counter_attribute_name))
696
697 def test_heartbeats_counter(self):
698 """Test the calculation of the heartbeats counter."""
699 numbers = [10, 7, 8, 5]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200700 counter_attribute_name = "heartbeats"
701 self._assert_counter_distribution_is_correct(
702 HeartBeat, numbers, counter_attribute_name
703 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400704
705 def test_crash_reports_counter(self):
706 """Test the calculation of the crashreports counter."""
707 numbers = [2, 5, 0, 3]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200708 counter_attribute_name = "prob_crashes"
709 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_UNKOWN}
710 self._assert_counter_distribution_is_correct(
711 Crashreport, numbers, counter_attribute_name, **boot_reason_param
712 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400713
714 def test_smpl_reports_counter(self):
715 """Test the calculation of the smpl reports counter."""
716 numbers = [1, 3, 4, 0]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200717 counter_attribute_name = "smpl"
718 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_RTC_ALARM}
719 self._assert_counter_distribution_is_correct(
720 Crashreport, numbers, counter_attribute_name, **boot_reason_param
721 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400722
723 def test_other_reports_counter(self):
724 """Test the calculation of the other reports counter."""
725 numbers = [0, 2, 1, 2]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200726 counter_attribute_name = "other"
727 boot_reason_param = {"boot_reason": "random boot reason"}
728 self._assert_counter_distribution_is_correct(
729 Crashreport, numbers, counter_attribute_name, **boot_reason_param
730 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400731
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +0200732 def _assert_reports_with_same_timestamp_are_counted(
733 self, report_type, counter_attribute_name, **kwargs
734 ):
735 """Validate that reports with the same timestamp are counted.
736
737 Reports from different devices but the same timestamp should be
738 counted as independent reports.
739 """
740 # Create a report
741 device1 = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
742 report1 = Dummy.create_dummy_report(
743 report_type, device=device1, **kwargs
744 )
745
746 # Create a second report with the same timestamp but from another device
747 device2 = Dummy.create_dummy_device(
748 user=Dummy.create_dummy_user(username=Dummy.USERNAMES[1])
749 )
750 Dummy.create_dummy_report(
751 report_type, device=device2, date=report1.date, **kwargs
752 )
753
754 # Run the command to update the database
755 call_command("stats", "update")
756
757 # Get the corresponding version instance from the database
758 get_params = {
759 self.unique_entry_name: getattr(report1, self.unique_entry_name)
760 }
761 version = self.version_class.objects.get(**get_params)
762
763 # Assert that both reports are counted
764 self.assertEqual(getattr(version, counter_attribute_name), 2)
765
766 @unittest.skip(
767 "Duplicates are dropped based on their timestamp at the moment. This is"
768 "to be adapted so that they are dropped taking into account the device"
769 "UUID as well."
770 )
771 def test_heartbeats_with_same_timestamp_are_counted(self):
772 """Validate that heartbeats with same timestamp are counted."""
773 counter_attribute_name = "heartbeats"
774 self._assert_reports_with_same_timestamp_are_counted(
775 HeartBeat, counter_attribute_name
776 )
777
778 @unittest.skip(
779 "Duplicates are dropped based on their timestamp at the moment. This is"
780 "to be adapted so that they are dropped taking into account the device"
781 "UUID as well."
782 )
783 def test_crash_reports_with_same_timestamp_are_counted(self):
784 """Validate that crash report duplicates are ignored."""
785 counter_attribute_name = "prob_crashes"
786 for unique_entry, boot_reason in zip(
787 self.unique_entries, Crashreport.CRASH_BOOT_REASONS
788 ):
789 params = {
790 "boot_reason": boot_reason,
791 self.unique_entry_name: unique_entry,
792 }
793 self._assert_reports_with_same_timestamp_are_counted(
794 Crashreport, counter_attribute_name, **params
795 )
796
797 @unittest.skip(
798 "Duplicates are dropped based on their timestamp at the moment. This is"
799 "to be adapted so that they are dropped taking into account the device"
800 "UUID as well."
801 )
802 def test_smpl_reports_with_same_timestamp_are_counted(self):
803 """Validate that smpl report duplicates are ignored."""
804 counter_attribute_name = "smpl"
805 for unique_entry, boot_reason in zip(
806 self.unique_entries, Crashreport.SMPL_BOOT_REASONS
807 ):
808 params = {
809 "boot_reason": boot_reason,
810 self.unique_entry_name: unique_entry,
811 }
812 self._assert_reports_with_same_timestamp_are_counted(
813 Crashreport, counter_attribute_name, **params
814 )
815
816 @unittest.skip(
817 "Duplicates are dropped based on their timestamp at the moment. This is"
818 "to be adapted so that they are dropped taking into account the device"
819 "UUID as well."
820 )
821 def test_other_reports_with_same_timestamp_are_counted(self):
822 """Validate that other report duplicates are ignored."""
823 counter_attribute_name = "other"
824 params = {"boot_reason": "random boot reason"}
825 self._assert_reports_with_same_timestamp_are_counted(
826 Crashreport, counter_attribute_name, **params
827 )
828
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200829 def _assert_duplicates_are_ignored(
830 self, report_type, device, counter_attribute_name, **kwargs
831 ):
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400832 """Validate that reports with duplicate timestamps are ignored."""
833 # Create a report
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200834 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400835
836 # Create a second report with the same timestamp
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200837 Dummy.create_dummy_report(
838 report_type, device=device, date=report.date, **kwargs
839 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400840
841 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200842 call_command("stats", "update")
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400843
844 # Get the corresponding version instance from the database
845 get_params = {
846 self.unique_entry_name: getattr(report, self.unique_entry_name)
847 }
848 version = self.version_class.objects.get(**get_params)
849
850 # Assert that the report with the duplicate timestamp is not
851 # counted, i.e. only 1 report is counted.
852 self.assertEqual(getattr(version, counter_attribute_name), 1)
853
854 def test_heartbeat_duplicates_are_ignored(self):
855 """Validate that heartbeat duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200856 counter_attribute_name = "heartbeats"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400857 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200858 self._assert_duplicates_are_ignored(
859 HeartBeat, device, counter_attribute_name
860 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400861
862 def test_crash_report_duplicates_are_ignored(self):
863 """Validate that crash report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200864 counter_attribute_name = "prob_crashes"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400865 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
866 for i, boot_reason in enumerate(Crashreport.CRASH_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200867 params = {
868 "boot_reason": boot_reason,
869 self.unique_entry_name: self.unique_entries[i],
870 }
871 self._assert_duplicates_are_ignored(
872 Crashreport, device, counter_attribute_name, **params
873 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400874
875 def test_smpl_report_duplicates_are_ignored(self):
876 """Validate that smpl report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200877 counter_attribute_name = "smpl"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400878 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
879 for i, boot_reason in enumerate(Crashreport.SMPL_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200880 params = {
881 "boot_reason": boot_reason,
882 self.unique_entry_name: self.unique_entries[i],
883 }
884 self._assert_duplicates_are_ignored(
885 Crashreport, device, counter_attribute_name, **params
886 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400887
888 def test_other_report_duplicates_are_ignored(self):
889 """Validate that other report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200890 counter_attribute_name = "other"
891 params = {"boot_reason": "random boot reason"}
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400892 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200893 self._assert_duplicates_are_ignored(
894 Crashreport, device, counter_attribute_name, **params
895 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400896
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400897
898# pylint: disable=too-many-ancestors
899class StatsCommandRadioVersionsTestCase(StatsCommandVersionsTestCase):
900 """Test the generation of RadioVersion stats with the stats command."""
901
902 version_class = RadioVersion
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200903 unique_entry_name = "radio_version"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400904 unique_entries = Dummy.RADIO_VERSIONS
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400905
906
907class CommandDebugOutputTestCase(TestCase):
908 """Test the reset and update commands debug output."""
909
910 # Additional positional arguments to pass to the commands
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200911 _CMD_ARGS = ["--no-color", "-v 2"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400912
913 # The stats models
914 _STATS_MODELS = [Version, VersionDaily, RadioVersion, RadioVersionDaily]
915 # The models that will generate an output
916 _ALL_MODELS = _STATS_MODELS + [StatsMetadata]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200917 _COUNTER_NAMES = ["heartbeats", "crashes", "smpl", "other"]
918 _COUNTER_ACTIONS = ["created", "updated"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400919
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200920 def _assert_command_output_matches(self, command, number, facts, models):
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400921 """Validate the debug output of a command.
922
923 The debug output is matched against the facts and models given in
924 the parameters.
925 """
926 buffer = StringIO()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200927 call_command("stats", command, *self._CMD_ARGS, stdout=buffer)
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400928 output = buffer.getvalue().splitlines()
929
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200930 expected_output = "{number} {model} {fact}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400931 for model in models:
932 for fact in facts:
933 self.assertIn(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200934 expected_output.format(
935 number=number, model=model.__name__, fact=fact
936 ),
937 output,
938 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400939
940 def test_reset_command_on_empty_db(self):
941 """Test the reset command on an empty database.
942
943 The reset command should yield nothing on an empty database.
944 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200945 self._assert_command_output_matches(
946 "reset", 0, ["deleted"], self._ALL_MODELS
947 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400948
949 def test_update_command_on_empty_db(self):
950 """Test the update command on an empty database.
951
952 The update command should yield nothing on an empty database.
953 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200954 pattern = "{action} for counter {counter}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400955 facts = [
956 pattern.format(action=counter_action, counter=counter_name)
957 for counter_action in self._COUNTER_ACTIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200958 for counter_name in self._COUNTER_NAMES
959 ]
960 self._assert_command_output_matches(
961 "update", 0, facts, self._STATS_MODELS
962 )
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200963
964 def test_reset_command_deletion_of_instances(self):
965 """Test the deletion of stats model instances with the reset command.
966
967 This test validates that model instances get deleted when the
968 reset command is called on a database that only contains a single
969 model instance for each class.
970 """
971 # Create dummy version instances
972 version = Dummy.create_dummy_version()
973 radio_version = Dummy.create_dummy_radio_version()
974 Dummy.create_dummy_daily_version(version)
975 Dummy.create_dummy_daily_radio_version(radio_version)
976 Dummy.create_dummy_stats_metadata()
977
978 # We expect that the model instances get deleted
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200979 self._assert_command_output_matches(
980 "reset", 1, ["deleted"], self._ALL_MODELS
981 )