blob: 0fd9fc94826857a9a42fe81815e78c50eaad7cee [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 (
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020015 Version,
16 VersionDaily,
17 RadioVersion,
18 RadioVersionDaily,
19 StatsMetadata,
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020020)
21
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040022from crashreports.models import User, Device, Crashreport, HeartBeat
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020023
24
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020025class Dummy:
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020026 """Class for creating dummy instances for testing."""
27
28 # Valid unique entries
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020029 BUILD_FINGERPRINTS = [
30 (
Mitja Nikolaus19cf9a92018-08-23 18:15:01 +020031 "Fairphone/FP2/FP2:5.1/FP2/r4275.1_FP2_gms76_1.13.0"
32 ":user/release-keys"
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020033 ),
34 (
35 "Fairphone/FP2/FP2:5.1.1/FP2-gms75.1.13.0/FP2-gms75.1.13.0"
36 ":user/release-keys"
37 ),
38 (
39 "Fairphone/FP2/FP2:6.0.1/FP2-gms-18.04.1/FP2-gms-18.04.1"
40 ":user/release-keys"
41 ),
Mitja Nikolaus19cf9a92018-08-23 18:15:01 +020042 ("Fairphone/FP2/FP2:7.1.2/18.07.2/gms-7480c31d:user/release-keys"),
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020043 ]
44 RADIO_VERSIONS = [
45 "4437.1-FP2-0-07",
46 "4437.1-FP2-0-08",
47 "4437.1-FP2-0-09",
48 "4437.1-FP2-0-10",
49 ]
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020050
51 DATES = [date(2018, 3, 19), date(2018, 3, 26), date(2018, 5, 1)]
52
53 DEFAULT_DUMMY_VERSION_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020054 "build_fingerprint": BUILD_FINGERPRINTS[0],
55 "first_seen_on": DATES[1],
56 "released_on": DATES[0],
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020057 }
58
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020059 DEFAULT_DUMMY_VERSION_DAILY_VALUES = {"date": DATES[1]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020060
61 DEFAULT_DUMMY_RADIO_VERSION_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020062 "radio_version": RADIO_VERSIONS[0],
63 "first_seen_on": DATES[1],
64 "released_on": DATES[0],
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020065 }
66
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020067 DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES = {"date": DATES[1]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020068
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +020069 DEFAULT_DUMMY_STATSMETADATA_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020070 "updated_at": datetime(2018, 6, 15, 2, 12, 24, tzinfo=pytz.utc)
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +020071 }
72
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020073 DEFAULT_DUMMY_DEVICE_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020074 "board_date": datetime(2015, 12, 15, 1, 23, 45, tzinfo=pytz.utc),
75 "chipset": "Qualcomm MSM8974PRO-AA",
76 "token": "64111c62d521fb4724454ca6dea27e18f93ef56e",
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020077 }
78
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020079 DEFAULT_DUMMY_USER_VALUES = {"username": "testuser"}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020080
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040081 DEFAULT_DUMMY_HEARTBEAT_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020082 "app_version": 10100,
83 "uptime": (
84 "up time: 16 days, 21:49:56, idle time: 5 days, 20:55:04, "
85 "sleep time: 10 days, 20:46:27"
86 ),
87 "build_fingerprint": BUILD_FINGERPRINTS[0],
88 "radio_version": RADIO_VERSIONS[0],
89 "date": datetime(2018, 3, 19, tzinfo=pytz.utc),
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040090 }
91
92 DEFAULT_DUMMY_CRASHREPORT_VALUES = DEFAULT_DUMMY_HEARTBEAT_VALUES.copy()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020093 DEFAULT_DUMMY_CRASHREPORT_VALUES.update(
94 {
95 "is_fake_report": 0,
96 "boot_reason": Crashreport.BOOT_REASON_UNKOWN,
97 "power_on_reason": "it was powered on",
98 "power_off_reason": "something happened and it went off",
99 }
100 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400101
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200102 @staticmethod
103 def update_copy(original, update):
104 """Merge fields of update into a copy of original."""
105 data = original.copy()
106 data.update(update)
107 return data
108
109 @staticmethod
110 def create_dummy_user(**kwargs):
111 """Create a dummy user instance.
112
113 The dummy instance is created and saved to the database.
114 Args:
115 **kwargs:
116 Optional arguments to extend/overwrite the default values.
117
118 Returns: The created user instance.
119
120 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200121 entity = User(
122 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_USER_VALUES, kwargs)
123 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200124 entity.save()
125 return entity
126
127 @staticmethod
128 def create_dummy_device(user, **kwargs):
129 """Create a dummy device instance.
130
131 The dummy instance is created and saved to the database.
132 Args:
133 user: The user instance that the device should relate to
134 **kwargs:
135 Optional arguments to extend/overwrite the default values.
136
137 Returns: The created device instance.
138
139 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200140 entity = Device(
141 user=user,
142 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_DEVICE_VALUES, kwargs)
143 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200144 entity.save()
145 return entity
146
147 @staticmethod
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400148 def create_dummy_report(report_type, device, **kwargs):
149 """Create a dummy report instance of the given report class type.
150
151 The dummy instance is created and saved to the database.
152 Args:
153 report_type: The class of the report type to be created.
154 user: The device instance that the heartbeat should relate to
155 **kwargs:
156 Optional arguments to extend/overwrite the default values.
157
158 Returns: The created report instance.
159
160 """
161 if report_type == HeartBeat:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200162 entity = HeartBeat(
163 device=device,
164 **Dummy.update_copy(
165 Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs
166 )
167 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400168 elif report_type == Crashreport:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200169 entity = Crashreport(
170 device=device,
171 **Dummy.update_copy(
172 Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES, kwargs
173 )
174 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400175 else:
176 raise RuntimeError(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200177 "No dummy report instance can be created for {}".format(
178 report_type.__name__
179 )
180 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400181 entity.save()
182 return entity
183
184 @staticmethod
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200185 def create_dummy_version(**kwargs):
186 """Create a dummy version instance.
187
188 The dummy instance is created and saved to the database.
189 Args:
190 **kwargs:
191 Optional arguments to extend/overwrite the default values.
192
193 Returns: The created version instance.
194
195 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200196 entity = Version(
197 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_VERSION_VALUES, kwargs)
198 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200199 entity.save()
200 return entity
201
202 @staticmethod
203 def create_dummy_radio_version(**kwargs):
204 """Create a dummy radio version instance.
205
206 The dummy instance is created and saved to the database.
207 Args:
208 **kwargs:
209 Optional arguments to extend/overwrite the default values.
210
211 Returns: The created radio version instance.
212
213 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200214 entity = RadioVersion(
215 **Dummy.update_copy(
216 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs
217 )
218 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200219 entity.save()
220 return entity
221
222 @staticmethod
223 def create_dummy_daily_version(version, **kwargs):
224 """Create a dummy daily version instance.
225
226 The dummy instance is created and saved to the database.
227 Args:
228 **kwargs:
229 Optional arguments to extend/overwrite the default values.
230
231 Returns: The created daily version instance.
232
233 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200234 entity = VersionDaily(
235 version=version,
236 **Dummy.update_copy(
237 Dummy.DEFAULT_DUMMY_VERSION_DAILY_VALUES, kwargs
238 )
239 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200240 entity.save()
241 return entity
242
243 @staticmethod
244 def create_dummy_daily_radio_version(version, **kwargs):
245 """Create a dummy daily radio version instance.
246
247 The dummy instance is created and saved to the database.
248 Args:
249 **kwargs:
250 Optional arguments to extend/overwrite the default values.
251
252 Returns: The created daily radio version instance.
253
254 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200255 entity = RadioVersionDaily(
256 version=version,
257 **Dummy.update_copy(
258 Dummy.DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES, kwargs
259 )
260 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200261 entity.save()
262 return entity
263
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200264 @staticmethod
265 def create_dummy_stats_metadata(**kwargs):
266 """Create a dummy stats metadata instance.
267
268 The dummy instance is created and saved to the database.
269 Args:
270 **kwargs:
271 Optional arguments to extend/overwrite the default values.
272
273 Returns: The created stats metadata instance.
274
275 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200276 entity = StatsMetadata(
277 **Dummy.update_copy(
278 Dummy.DEFAULT_DUMMY_STATSMETADATA_VALUES, kwargs
279 )
280 )
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200281 entity.save()
282 return entity
283
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200284
285class _VersionTestCase(APITestCase):
286 """Abstract class for version-related test cases to inherit from."""
287
288 # The attribute name characterising the unicity of a stats entry (the
289 # named identifier)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200290 unique_entry_name = "build_fingerprint"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200291 # The collection of unique entries to post
292 unique_entries = Dummy.BUILD_FINGERPRINTS
293 # The URL to retrieve the stats entries from
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200294 endpoint_url = reverse("hiccup_stats_api_v1_versions")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200295
296 @classmethod
297 def setUpTestData(cls): # noqa: N802
298 """Create an admin user for accessing the API.
299
300 The APIClient that can be used to make authenticated requests to the
301 server is stored in self.admin.
302 """
303 admin_user = User.objects.create_superuser(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200304 "somebody", "somebody@example.com", "thepassword"
305 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200306 cls.admin = APIClient()
307 cls.admin.force_authenticate(admin_user)
308
309 @staticmethod
310 def _create_dummy_version(**kwargs):
311 return Dummy.create_dummy_version(**kwargs)
312
313 def _get_with_params(self, url, params):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200314 return self.admin.get("{}?{}".format(url, urlencode(params)))
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200315
316 def _assert_result_length_is(self, response, count):
317 self.assertEqual(response.status_code, status.HTTP_200_OK)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200318 self.assertIn("results", response.data)
319 self.assertIn("count", response.data)
320 self.assertEqual(response.data["count"], count)
321 self.assertEqual(len(response.data["results"]), count)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200322
323 def _assert_device_owner_has_no_get_access(self, entries_url):
324 # Create a user and device
325 user = Dummy.create_dummy_user()
326 device = Dummy.create_dummy_device(user=user)
327
328 # Create authenticated client
329 user = APIClient()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200330 user.credentials(HTTP_AUTHORIZATION="Token " + device.token)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200331
332 # Try getting entries using the client
333 response = user.get(entries_url)
334 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
335
336 def _assert_filter_result_matches(self, filter_params, expected_result):
337 # List entities with filter
338 response = self._get_with_params(self.endpoint_url, filter_params)
339
340 # Expect only the single matching result to be returned
341 self._assert_result_length_is(response, 1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200342 self.assertEqual(
343 response.data["results"][0][self.unique_entry_name],
344 getattr(expected_result, self.unique_entry_name),
345 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200346
347
348class VersionTestCase(_VersionTestCase):
349 """Test the Version and REST endpoint."""
350
351 def _create_version_entities(self):
352 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200353 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200354 for unique_entry in self.unique_entries
355 ]
356 return versions
357
358 def test_list_versions_without_authentication(self):
359 """Test listing of versions without authentication."""
360 response = self.client.get(self.endpoint_url)
361 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
362
363 def test_list_versions_as_device_owner(self):
364 """Test listing of versions as device owner."""
365 self._assert_device_owner_has_no_get_access(self.endpoint_url)
366
367 def test_list_versions_empty_database(self):
368 """Test listing of versions on an empty database."""
369 response = self.admin.get(self.endpoint_url)
370 self._assert_result_length_is(response, 0)
371
372 def test_list_versions(self):
373 """Test listing versions."""
374 versions = self._create_version_entities()
375 response = self.admin.get(self.endpoint_url)
376 self._assert_result_length_is(response, len(versions))
377
378 def test_filter_versions_by_unique_entry_name(self):
379 """Test filtering versions by their unique entry name."""
380 versions = self._create_version_entities()
381 response = self.admin.get(self.endpoint_url)
382
383 # Listing all entities should return the correct result length
384 self._assert_result_length_is(response, len(versions))
385
386 # List entities with filter
387 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200388 self.unique_entry_name: getattr(versions[0], self.unique_entry_name)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200389 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200390 self._assert_filter_result_matches(
391 filter_params, expected_result=versions[0]
392 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200393
394 def test_filter_versions_by_release_type(self):
395 """Test filtering versions by release type."""
396 # Create versions for all combinations of release types
397 versions = []
398 i = 0
399 for is_official_release in True, False:
400 for is_beta_release in True, False:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200401 versions.append(
402 self._create_dummy_version(
403 **{
404 "is_official_release": is_official_release,
405 "is_beta_release": is_beta_release,
406 self.unique_entry_name: self.unique_entries[i],
407 }
408 )
409 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200410 i += 1
411
412 # # Listing all entities should return the correct result length
413 response = self.admin.get(self.endpoint_url)
414 self._assert_result_length_is(response, len(versions))
415
416 # List each of the entities with the matching filter params
417 for version in versions:
418 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200419 "is_official_release": version.is_official_release,
420 "is_beta_release": version.is_beta_release,
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200421 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200422 self._assert_filter_result_matches(
423 filter_params, expected_result=version
424 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200425
426 def test_filter_versions_by_first_seen_date(self):
427 """Test filtering versions by first seen date."""
428 versions = self._create_version_entities()
429
430 # Set the first seen date of an entity
431 versions[0].first_seen_on = Dummy.DATES[2]
432 versions[0].save()
433
434 # Listing all entities should return the correct result length
435 response = self.admin.get(self.endpoint_url)
436 self._assert_result_length_is(response, len(versions))
437
438 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200439 filter_params = {"first_seen_after": Dummy.DATES[2]}
440 self._assert_filter_result_matches(
441 filter_params, expected_result=versions[0]
442 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200443
444
445# pylint: disable=too-many-ancestors
446class RadioVersionTestCase(VersionTestCase):
447 """Test the RadioVersion REST endpoint."""
448
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200449 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200450 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200451 endpoint_url = reverse("hiccup_stats_api_v1_radio_versions")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200452
453 @staticmethod
454 def _create_dummy_version(**kwargs):
455 return Dummy.create_dummy_radio_version(**kwargs)
456
457
458class VersionDailyTestCase(_VersionTestCase):
459 """Test the VersionDaily REST endpoint."""
460
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200461 endpoint_url = reverse("hiccup_stats_api_v1_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200462
463 @staticmethod
464 def _create_dummy_daily_version(version, **kwargs):
465 return Dummy.create_dummy_daily_version(version, **kwargs)
466
467 def _create_version_entities(self):
468 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200469 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200470 for unique_entry in self.unique_entries
471 ]
472 versions_daily = [
473 self._create_dummy_daily_version(version=version)
474 for version in versions
475 ]
476 return versions_daily
477
478 def test_list_daily_versions_without_authentication(self):
479 """Test listing of daily versions without authentication."""
480 response = self.client.get(self.endpoint_url)
481 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
482
483 def test_list_daily_versions_as_device_owner(self):
484 """Test listing of daily versions as device owner."""
485 self._assert_device_owner_has_no_get_access(self.endpoint_url)
486
487 def test_list_daily_versions_empty_database(self):
488 """Test listing of daily versions on an empty database."""
489 response = self.admin.get(self.endpoint_url)
490 self._assert_result_length_is(response, 0)
491
492 def test_list_daily_versions(self):
493 """Test listing daily versions."""
494 versions_daily = self._create_version_entities()
495 response = self.admin.get(self.endpoint_url)
496 self._assert_result_length_is(response, len(versions_daily))
497
498 def test_filter_daily_versions_by_version(self):
499 """Test filtering versions by the version they relate to."""
500 # Create VersionDaily entities
501 versions = self._create_version_entities()
502
503 # Listing all entities should return the correct result length
504 response = self.admin.get(self.endpoint_url)
505 self._assert_result_length_is(response, len(versions))
506
507 # List entities with filter
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200508 param_name = "version__" + self.unique_entry_name
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200509 filter_params = {
510 param_name: getattr(versions[0].version, self.unique_entry_name)
511 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200512 self._assert_filter_result_matches(
513 filter_params, expected_result=versions[0].version
514 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200515
516 def test_filter_daily_versions_by_date(self):
517 """Test filtering daily versions by date."""
518 # Create Version and VersionDaily entities
519 versions = self._create_version_entities()
520
521 # Update the date
522 versions[0].date = Dummy.DATES[2]
523 versions[0].save()
524
525 # Listing all entities should return the correct result length
526 response = self.admin.get(self.endpoint_url)
527 self._assert_result_length_is(response, len(versions))
528
529 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200530 filter_params = {"date": versions[0].date}
531 self._assert_filter_result_matches(
532 filter_params, expected_result=versions[0].version
533 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200534
535
536class RadioVersionDailyTestCase(VersionDailyTestCase):
537 """Test the RadioVersionDaily REST endpoint."""
538
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200539 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200540 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200541 endpoint_url = reverse("hiccup_stats_api_v1_radio_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200542
543 @staticmethod
544 def _create_dummy_version(**kwargs):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200545 entity = RadioVersion(
546 **Dummy.update_copy(
547 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs
548 )
549 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200550 entity.save()
551 return entity
552
553 @staticmethod
554 def _create_dummy_daily_version(version, **kwargs):
555 return Dummy.create_dummy_daily_radio_version(version, **kwargs)
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400556
557
558class StatsCommandVersionsTestCase(TestCase):
559 """Test the generation of Version stats with the stats command."""
560
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400561 # The class of the version type to be tested
562 version_class = Version
563 # The attribute name characterising the unicity of a stats entry (the
564 # named identifier)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200565 unique_entry_name = "build_fingerprint"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400566 # The collection of unique entries to post
567 unique_entries = Dummy.BUILD_FINGERPRINTS
568
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200569 def _create_reports(
570 self, report_type, unique_entry_name, device, number, **kwargs
571 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400572 # Create reports with distinct timestamps
573 now = datetime.now(pytz.utc)
574 for i in range(number):
575 report_date = now - timedelta(milliseconds=i)
576 report_attributes = {
577 self.unique_entry_name: unique_entry_name,
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200578 "device": device,
579 "date": report_date,
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400580 }
581 report_attributes.update(**kwargs)
582 Dummy.create_dummy_report(report_type, **report_attributes)
583
584 def test_stats_calculation(self):
585 """Test generation of a Version instance."""
586 user = Dummy.create_dummy_user()
587 device = Dummy.create_dummy_device(user=user)
588 heartbeat = Dummy.create_dummy_report(HeartBeat, device=device)
589
590 # Expect that we do not have the Version before updating the stats
591 get_params = {
592 self.unique_entry_name: getattr(heartbeat, self.unique_entry_name)
593 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200594 self.assertRaises(
595 self.version_class.DoesNotExist,
596 self.version_class.objects.get,
597 **get_params
598 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400599
600 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200601 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400602
603 # Assume that a corresponding Version instance has been created
604 version = self.version_class.objects.get(**get_params)
605 self.assertIsNotNone(version)
606
607 def _assert_older_report_updates_version_date(self, report_type):
608 """Validate that older reports sent later affect the version date."""
609 user = Dummy.create_dummy_user()
610 device = Dummy.create_dummy_device(user=user)
611 report = Dummy.create_dummy_report(report_type, device=device)
612
613 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200614 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400615
616 get_params = {
617 self.unique_entry_name: getattr(report, self.unique_entry_name)
618 }
619 version = self.version_class.objects.get(**get_params)
620
621 self.assertEqual(report.date.date(), version.first_seen_on)
622
623 # Create a new report from an earlier point in time
624 report_time_2 = report.date - timedelta(weeks=1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200625 Dummy.create_dummy_report(
626 report_type, device=device, date=report_time_2
627 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400628
629 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200630 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400631
632 # Get the same version object from before
633 version = self.version_class.objects.get(**get_params)
634
635 # Validate that the date matches the report recently sent
636 self.assertEqual(report_time_2.date(), version.first_seen_on)
637
638 def test_older_heartbeat_updates_version_date(self):
639 """Validate updating version date with older heartbeats."""
640 self._assert_older_report_updates_version_date(HeartBeat)
641
642 def test_older_crash_report_updates_version_date(self):
643 """Validate updating version date with older crash reports."""
644 self._assert_older_report_updates_version_date(Crashreport)
645
646 def test_entries_are_unique(self):
647 """Validate the entries' unicity and value."""
648 # Create some reports
649 user = Dummy.create_dummy_user()
650 device = Dummy.create_dummy_device(user=user)
651 for unique_entry in self.unique_entries:
652 self._create_reports(HeartBeat, unique_entry, device, 10)
653
654 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200655 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400656
657 # Check whether the correct amount of distinct versions have been
658 # created
659 versions = self.version_class.objects.all()
660 for version in versions:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200661 self.assertIn(
662 getattr(version, self.unique_entry_name), self.unique_entries
663 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400664 self.assertEqual(len(versions), len(self.unique_entries))
665
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200666 def _assert_counter_distribution_is_correct(
667 self, report_type, numbers, counter_attribute_name, **kwargs
668 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400669 """Validate a counter distribution in the database."""
670 if len(numbers) != len(self.unique_entries):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200671 raise ValueError(
672 "The length of the numbers list must match the "
673 "length of self.unique_entries in the test class"
674 "({} != {})".format(len(numbers), len(self.unique_entries))
675 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400676 # Create some reports
677 user = Dummy.create_dummy_user()
678 device = Dummy.create_dummy_device(user=user)
679 for unique_entry, num in zip(self.unique_entries, numbers):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200680 self._create_reports(
681 report_type, unique_entry, device, num, **kwargs
682 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400683
684 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200685 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400686
687 # Check whether the numbers of reports match
688 for version in self.version_class.objects.all():
689 unique_entry_name = getattr(version, self.unique_entry_name)
690 num = numbers[self.unique_entries.index(unique_entry_name)]
691 self.assertEqual(num, getattr(version, counter_attribute_name))
692
693 def test_heartbeats_counter(self):
694 """Test the calculation of the heartbeats counter."""
695 numbers = [10, 7, 8, 5]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200696 counter_attribute_name = "heartbeats"
697 self._assert_counter_distribution_is_correct(
698 HeartBeat, numbers, counter_attribute_name
699 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400700
701 def test_crash_reports_counter(self):
702 """Test the calculation of the crashreports counter."""
703 numbers = [2, 5, 0, 3]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200704 counter_attribute_name = "prob_crashes"
705 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_UNKOWN}
706 self._assert_counter_distribution_is_correct(
707 Crashreport, numbers, counter_attribute_name, **boot_reason_param
708 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400709
710 def test_smpl_reports_counter(self):
711 """Test the calculation of the smpl reports counter."""
712 numbers = [1, 3, 4, 0]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200713 counter_attribute_name = "smpl"
714 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_RTC_ALARM}
715 self._assert_counter_distribution_is_correct(
716 Crashreport, numbers, counter_attribute_name, **boot_reason_param
717 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400718
719 def test_other_reports_counter(self):
720 """Test the calculation of the other reports counter."""
721 numbers = [0, 2, 1, 2]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200722 counter_attribute_name = "other"
723 boot_reason_param = {"boot_reason": "random boot reason"}
724 self._assert_counter_distribution_is_correct(
725 Crashreport, numbers, counter_attribute_name, **boot_reason_param
726 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400727
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200728 def _assert_duplicates_are_ignored(
729 self, report_type, device, counter_attribute_name, **kwargs
730 ):
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400731 """Validate that reports with duplicate timestamps are ignored."""
732 # Create a report
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200733 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400734
735 # Create a second report with the same timestamp
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200736 Dummy.create_dummy_report(
737 report_type, device=device, date=report.date, **kwargs
738 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400739
740 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200741 call_command("stats", "update")
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400742
743 # Get the corresponding version instance from the database
744 get_params = {
745 self.unique_entry_name: getattr(report, self.unique_entry_name)
746 }
747 version = self.version_class.objects.get(**get_params)
748
749 # Assert that the report with the duplicate timestamp is not
750 # counted, i.e. only 1 report is counted.
751 self.assertEqual(getattr(version, counter_attribute_name), 1)
752
753 def test_heartbeat_duplicates_are_ignored(self):
754 """Validate that heartbeat duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200755 counter_attribute_name = "heartbeats"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400756 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200757 self._assert_duplicates_are_ignored(
758 HeartBeat, device, counter_attribute_name
759 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400760
761 def test_crash_report_duplicates_are_ignored(self):
762 """Validate that crash report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200763 counter_attribute_name = "prob_crashes"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400764 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
765 for i, boot_reason in enumerate(Crashreport.CRASH_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200766 params = {
767 "boot_reason": boot_reason,
768 self.unique_entry_name: self.unique_entries[i],
769 }
770 self._assert_duplicates_are_ignored(
771 Crashreport, device, counter_attribute_name, **params
772 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400773
774 def test_smpl_report_duplicates_are_ignored(self):
775 """Validate that smpl report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200776 counter_attribute_name = "smpl"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400777 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
778 for i, boot_reason in enumerate(Crashreport.SMPL_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200779 params = {
780 "boot_reason": boot_reason,
781 self.unique_entry_name: self.unique_entries[i],
782 }
783 self._assert_duplicates_are_ignored(
784 Crashreport, device, counter_attribute_name, **params
785 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400786
787 def test_other_report_duplicates_are_ignored(self):
788 """Validate that other report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200789 counter_attribute_name = "other"
790 params = {"boot_reason": "random boot reason"}
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400791 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200792 self._assert_duplicates_are_ignored(
793 Crashreport, device, counter_attribute_name, **params
794 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400795
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400796
797# pylint: disable=too-many-ancestors
798class StatsCommandRadioVersionsTestCase(StatsCommandVersionsTestCase):
799 """Test the generation of RadioVersion stats with the stats command."""
800
801 version_class = RadioVersion
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200802 unique_entry_name = "radio_version"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400803 unique_entries = Dummy.RADIO_VERSIONS
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400804
805
806class CommandDebugOutputTestCase(TestCase):
807 """Test the reset and update commands debug output."""
808
809 # Additional positional arguments to pass to the commands
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200810 _CMD_ARGS = ["--no-color", "-v 2"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400811
812 # The stats models
813 _STATS_MODELS = [Version, VersionDaily, RadioVersion, RadioVersionDaily]
814 # The models that will generate an output
815 _ALL_MODELS = _STATS_MODELS + [StatsMetadata]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200816 _COUNTER_NAMES = ["heartbeats", "crashes", "smpl", "other"]
817 _COUNTER_ACTIONS = ["created", "updated"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400818
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200819 def _assert_command_output_matches(self, command, number, facts, models):
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400820 """Validate the debug output of a command.
821
822 The debug output is matched against the facts and models given in
823 the parameters.
824 """
825 buffer = StringIO()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200826 call_command("stats", command, *self._CMD_ARGS, stdout=buffer)
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400827 output = buffer.getvalue().splitlines()
828
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200829 expected_output = "{number} {model} {fact}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400830 for model in models:
831 for fact in facts:
832 self.assertIn(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200833 expected_output.format(
834 number=number, model=model.__name__, fact=fact
835 ),
836 output,
837 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400838
839 def test_reset_command_on_empty_db(self):
840 """Test the reset command on an empty database.
841
842 The reset command should yield nothing on an empty database.
843 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200844 self._assert_command_output_matches(
845 "reset", 0, ["deleted"], self._ALL_MODELS
846 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400847
848 def test_update_command_on_empty_db(self):
849 """Test the update command on an empty database.
850
851 The update command should yield nothing on an empty database.
852 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200853 pattern = "{action} for counter {counter}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400854 facts = [
855 pattern.format(action=counter_action, counter=counter_name)
856 for counter_action in self._COUNTER_ACTIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200857 for counter_name in self._COUNTER_NAMES
858 ]
859 self._assert_command_output_matches(
860 "update", 0, facts, self._STATS_MODELS
861 )
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200862
863 def test_reset_command_deletion_of_instances(self):
864 """Test the deletion of stats model instances with the reset command.
865
866 This test validates that model instances get deleted when the
867 reset command is called on a database that only contains a single
868 model instance for each class.
869 """
870 # Create dummy version instances
871 version = Dummy.create_dummy_version()
872 radio_version = Dummy.create_dummy_radio_version()
873 Dummy.create_dummy_daily_version(version)
874 Dummy.create_dummy_daily_radio_version(radio_version)
875 Dummy.create_dummy_stats_metadata()
876
877 # We expect that the model instances get deleted
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200878 self._assert_command_output_matches(
879 "reset", 1, ["deleted"], self._ALL_MODELS
880 )