blob: 68f8e82d512f7496b57d69efb9d18f8b1a4f96ee [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 (
31 "Fairphone/FP2/FP2:5.1/FP2/r4275.1_FP2_gms76_1.13.0:user/release-keys"
32 ),
33 (
34 "Fairphone/FP2/FP2:5.1.1/FP2-gms75.1.13.0/FP2-gms75.1.13.0"
35 ":user/release-keys"
36 ),
37 (
38 "Fairphone/FP2/FP2:6.0.1/FP2-gms-18.04.1/FP2-gms-18.04.1"
39 ":user/release-keys"
40 ),
41 ("Fairphone/FP2/FP2:7.1.2/18.07.2/gms-7480c31d" ":user/release-keys"),
42 ]
43 RADIO_VERSIONS = [
44 "4437.1-FP2-0-07",
45 "4437.1-FP2-0-08",
46 "4437.1-FP2-0-09",
47 "4437.1-FP2-0-10",
48 ]
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020049
50 DATES = [date(2018, 3, 19), date(2018, 3, 26), date(2018, 5, 1)]
51
52 DEFAULT_DUMMY_VERSION_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020053 "build_fingerprint": BUILD_FINGERPRINTS[0],
54 "first_seen_on": DATES[1],
55 "released_on": DATES[0],
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020056 }
57
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020058 DEFAULT_DUMMY_VERSION_DAILY_VALUES = {"date": DATES[1]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020059
60 DEFAULT_DUMMY_RADIO_VERSION_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020061 "radio_version": RADIO_VERSIONS[0],
62 "first_seen_on": DATES[1],
63 "released_on": DATES[0],
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020064 }
65
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020066 DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES = {"date": DATES[1]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020067
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +020068 DEFAULT_DUMMY_STATSMETADATA_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020069 "updated_at": datetime(2018, 6, 15, 2, 12, 24, tzinfo=pytz.utc)
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +020070 }
71
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020072 DEFAULT_DUMMY_DEVICE_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020073 "board_date": datetime(2015, 12, 15, 1, 23, 45, tzinfo=pytz.utc),
74 "chipset": "Qualcomm MSM8974PRO-AA",
75 "token": "64111c62d521fb4724454ca6dea27e18f93ef56e",
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020076 }
77
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020078 DEFAULT_DUMMY_USER_VALUES = {"username": "testuser"}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020079
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040080 DEFAULT_DUMMY_HEARTBEAT_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020081 "app_version": 10100,
82 "uptime": (
83 "up time: 16 days, 21:49:56, idle time: 5 days, 20:55:04, "
84 "sleep time: 10 days, 20:46:27"
85 ),
86 "build_fingerprint": BUILD_FINGERPRINTS[0],
87 "radio_version": RADIO_VERSIONS[0],
88 "date": datetime(2018, 3, 19, tzinfo=pytz.utc),
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040089 }
90
91 DEFAULT_DUMMY_CRASHREPORT_VALUES = DEFAULT_DUMMY_HEARTBEAT_VALUES.copy()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020092 DEFAULT_DUMMY_CRASHREPORT_VALUES.update(
93 {
94 "is_fake_report": 0,
95 "boot_reason": Crashreport.BOOT_REASON_UNKOWN,
96 "power_on_reason": "it was powered on",
97 "power_off_reason": "something happened and it went off",
98 }
99 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400100
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200101 @staticmethod
102 def update_copy(original, update):
103 """Merge fields of update into a copy of original."""
104 data = original.copy()
105 data.update(update)
106 return data
107
108 @staticmethod
109 def create_dummy_user(**kwargs):
110 """Create a dummy user instance.
111
112 The dummy instance is created and saved to the database.
113 Args:
114 **kwargs:
115 Optional arguments to extend/overwrite the default values.
116
117 Returns: The created user instance.
118
119 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200120 entity = User(
121 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_USER_VALUES, kwargs)
122 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200123 entity.save()
124 return entity
125
126 @staticmethod
127 def create_dummy_device(user, **kwargs):
128 """Create a dummy device instance.
129
130 The dummy instance is created and saved to the database.
131 Args:
132 user: The user instance that the device should relate to
133 **kwargs:
134 Optional arguments to extend/overwrite the default values.
135
136 Returns: The created device instance.
137
138 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200139 entity = Device(
140 user=user,
141 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_DEVICE_VALUES, kwargs)
142 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200143 entity.save()
144 return entity
145
146 @staticmethod
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400147 def create_dummy_report(report_type, device, **kwargs):
148 """Create a dummy report instance of the given report class type.
149
150 The dummy instance is created and saved to the database.
151 Args:
152 report_type: The class of the report type to be created.
153 user: The device instance that the heartbeat should relate to
154 **kwargs:
155 Optional arguments to extend/overwrite the default values.
156
157 Returns: The created report instance.
158
159 """
160 if report_type == HeartBeat:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200161 entity = HeartBeat(
162 device=device,
163 **Dummy.update_copy(
164 Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs
165 )
166 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400167 elif report_type == Crashreport:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200168 entity = Crashreport(
169 device=device,
170 **Dummy.update_copy(
171 Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES, kwargs
172 )
173 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400174 else:
175 raise RuntimeError(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200176 "No dummy report instance can be created for {}".format(
177 report_type.__name__
178 )
179 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400180 entity.save()
181 return entity
182
183 @staticmethod
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200184 def create_dummy_version(**kwargs):
185 """Create a dummy version instance.
186
187 The dummy instance is created and saved to the database.
188 Args:
189 **kwargs:
190 Optional arguments to extend/overwrite the default values.
191
192 Returns: The created version instance.
193
194 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200195 entity = Version(
196 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_VERSION_VALUES, kwargs)
197 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200198 entity.save()
199 return entity
200
201 @staticmethod
202 def create_dummy_radio_version(**kwargs):
203 """Create a dummy radio version instance.
204
205 The dummy instance is created and saved to the database.
206 Args:
207 **kwargs:
208 Optional arguments to extend/overwrite the default values.
209
210 Returns: The created radio version instance.
211
212 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200213 entity = RadioVersion(
214 **Dummy.update_copy(
215 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs
216 )
217 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200218 entity.save()
219 return entity
220
221 @staticmethod
222 def create_dummy_daily_version(version, **kwargs):
223 """Create a dummy daily version instance.
224
225 The dummy instance is created and saved to the database.
226 Args:
227 **kwargs:
228 Optional arguments to extend/overwrite the default values.
229
230 Returns: The created daily version instance.
231
232 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200233 entity = VersionDaily(
234 version=version,
235 **Dummy.update_copy(
236 Dummy.DEFAULT_DUMMY_VERSION_DAILY_VALUES, kwargs
237 )
238 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200239 entity.save()
240 return entity
241
242 @staticmethod
243 def create_dummy_daily_radio_version(version, **kwargs):
244 """Create a dummy daily radio version instance.
245
246 The dummy instance is created and saved to the database.
247 Args:
248 **kwargs:
249 Optional arguments to extend/overwrite the default values.
250
251 Returns: The created daily radio version instance.
252
253 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200254 entity = RadioVersionDaily(
255 version=version,
256 **Dummy.update_copy(
257 Dummy.DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES, kwargs
258 )
259 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200260 entity.save()
261 return entity
262
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200263 @staticmethod
264 def create_dummy_stats_metadata(**kwargs):
265 """Create a dummy stats metadata instance.
266
267 The dummy instance is created and saved to the database.
268 Args:
269 **kwargs:
270 Optional arguments to extend/overwrite the default values.
271
272 Returns: The created stats metadata instance.
273
274 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200275 entity = StatsMetadata(
276 **Dummy.update_copy(
277 Dummy.DEFAULT_DUMMY_STATSMETADATA_VALUES, kwargs
278 )
279 )
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200280 entity.save()
281 return entity
282
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200283
284class _VersionTestCase(APITestCase):
285 """Abstract class for version-related test cases to inherit from."""
286
287 # The attribute name characterising the unicity of a stats entry (the
288 # named identifier)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200289 unique_entry_name = "build_fingerprint"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200290 # The collection of unique entries to post
291 unique_entries = Dummy.BUILD_FINGERPRINTS
292 # The URL to retrieve the stats entries from
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200293 endpoint_url = reverse("hiccup_stats_api_v1_versions")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200294
295 @classmethod
296 def setUpTestData(cls): # noqa: N802
297 """Create an admin user for accessing the API.
298
299 The APIClient that can be used to make authenticated requests to the
300 server is stored in self.admin.
301 """
302 admin_user = User.objects.create_superuser(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200303 "somebody", "somebody@example.com", "thepassword"
304 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200305 cls.admin = APIClient()
306 cls.admin.force_authenticate(admin_user)
307
308 @staticmethod
309 def _create_dummy_version(**kwargs):
310 return Dummy.create_dummy_version(**kwargs)
311
312 def _get_with_params(self, url, params):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200313 return self.admin.get("{}?{}".format(url, urlencode(params)))
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200314
315 def _assert_result_length_is(self, response, count):
316 self.assertEqual(response.status_code, status.HTTP_200_OK)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200317 self.assertIn("results", response.data)
318 self.assertIn("count", response.data)
319 self.assertEqual(response.data["count"], count)
320 self.assertEqual(len(response.data["results"]), count)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200321
322 def _assert_device_owner_has_no_get_access(self, entries_url):
323 # Create a user and device
324 user = Dummy.create_dummy_user()
325 device = Dummy.create_dummy_device(user=user)
326
327 # Create authenticated client
328 user = APIClient()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200329 user.credentials(HTTP_AUTHORIZATION="Token " + device.token)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200330
331 # Try getting entries using the client
332 response = user.get(entries_url)
333 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
334
335 def _assert_filter_result_matches(self, filter_params, expected_result):
336 # List entities with filter
337 response = self._get_with_params(self.endpoint_url, filter_params)
338
339 # Expect only the single matching result to be returned
340 self._assert_result_length_is(response, 1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200341 self.assertEqual(
342 response.data["results"][0][self.unique_entry_name],
343 getattr(expected_result, self.unique_entry_name),
344 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200345
346
347class VersionTestCase(_VersionTestCase):
348 """Test the Version and REST endpoint."""
349
350 def _create_version_entities(self):
351 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200352 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200353 for unique_entry in self.unique_entries
354 ]
355 return versions
356
357 def test_list_versions_without_authentication(self):
358 """Test listing of versions without authentication."""
359 response = self.client.get(self.endpoint_url)
360 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
361
362 def test_list_versions_as_device_owner(self):
363 """Test listing of versions as device owner."""
364 self._assert_device_owner_has_no_get_access(self.endpoint_url)
365
366 def test_list_versions_empty_database(self):
367 """Test listing of versions on an empty database."""
368 response = self.admin.get(self.endpoint_url)
369 self._assert_result_length_is(response, 0)
370
371 def test_list_versions(self):
372 """Test listing versions."""
373 versions = self._create_version_entities()
374 response = self.admin.get(self.endpoint_url)
375 self._assert_result_length_is(response, len(versions))
376
377 def test_filter_versions_by_unique_entry_name(self):
378 """Test filtering versions by their unique entry name."""
379 versions = self._create_version_entities()
380 response = self.admin.get(self.endpoint_url)
381
382 # Listing all entities should return the correct result length
383 self._assert_result_length_is(response, len(versions))
384
385 # List entities with filter
386 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200387 self.unique_entry_name: getattr(versions[0], self.unique_entry_name)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200388 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200389 self._assert_filter_result_matches(
390 filter_params, expected_result=versions[0]
391 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200392
393 def test_filter_versions_by_release_type(self):
394 """Test filtering versions by release type."""
395 # Create versions for all combinations of release types
396 versions = []
397 i = 0
398 for is_official_release in True, False:
399 for is_beta_release in True, False:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200400 versions.append(
401 self._create_dummy_version(
402 **{
403 "is_official_release": is_official_release,
404 "is_beta_release": is_beta_release,
405 self.unique_entry_name: self.unique_entries[i],
406 }
407 )
408 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200409 i += 1
410
411 # # Listing all entities should return the correct result length
412 response = self.admin.get(self.endpoint_url)
413 self._assert_result_length_is(response, len(versions))
414
415 # List each of the entities with the matching filter params
416 for version in versions:
417 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200418 "is_official_release": version.is_official_release,
419 "is_beta_release": version.is_beta_release,
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200420 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200421 self._assert_filter_result_matches(
422 filter_params, expected_result=version
423 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200424
425 def test_filter_versions_by_first_seen_date(self):
426 """Test filtering versions by first seen date."""
427 versions = self._create_version_entities()
428
429 # Set the first seen date of an entity
430 versions[0].first_seen_on = Dummy.DATES[2]
431 versions[0].save()
432
433 # Listing all entities should return the correct result length
434 response = self.admin.get(self.endpoint_url)
435 self._assert_result_length_is(response, len(versions))
436
437 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200438 filter_params = {"first_seen_after": Dummy.DATES[2]}
439 self._assert_filter_result_matches(
440 filter_params, expected_result=versions[0]
441 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200442
443
444# pylint: disable=too-many-ancestors
445class RadioVersionTestCase(VersionTestCase):
446 """Test the RadioVersion REST endpoint."""
447
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200448 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200449 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200450 endpoint_url = reverse("hiccup_stats_api_v1_radio_versions")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200451
452 @staticmethod
453 def _create_dummy_version(**kwargs):
454 return Dummy.create_dummy_radio_version(**kwargs)
455
456
457class VersionDailyTestCase(_VersionTestCase):
458 """Test the VersionDaily REST endpoint."""
459
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200460 endpoint_url = reverse("hiccup_stats_api_v1_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200461
462 @staticmethod
463 def _create_dummy_daily_version(version, **kwargs):
464 return Dummy.create_dummy_daily_version(version, **kwargs)
465
466 def _create_version_entities(self):
467 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200468 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200469 for unique_entry in self.unique_entries
470 ]
471 versions_daily = [
472 self._create_dummy_daily_version(version=version)
473 for version in versions
474 ]
475 return versions_daily
476
477 def test_list_daily_versions_without_authentication(self):
478 """Test listing of daily versions without authentication."""
479 response = self.client.get(self.endpoint_url)
480 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
481
482 def test_list_daily_versions_as_device_owner(self):
483 """Test listing of daily versions as device owner."""
484 self._assert_device_owner_has_no_get_access(self.endpoint_url)
485
486 def test_list_daily_versions_empty_database(self):
487 """Test listing of daily versions on an empty database."""
488 response = self.admin.get(self.endpoint_url)
489 self._assert_result_length_is(response, 0)
490
491 def test_list_daily_versions(self):
492 """Test listing daily versions."""
493 versions_daily = self._create_version_entities()
494 response = self.admin.get(self.endpoint_url)
495 self._assert_result_length_is(response, len(versions_daily))
496
497 def test_filter_daily_versions_by_version(self):
498 """Test filtering versions by the version they relate to."""
499 # Create VersionDaily entities
500 versions = self._create_version_entities()
501
502 # Listing all entities should return the correct result length
503 response = self.admin.get(self.endpoint_url)
504 self._assert_result_length_is(response, len(versions))
505
506 # List entities with filter
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200507 param_name = "version__" + self.unique_entry_name
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200508 filter_params = {
509 param_name: getattr(versions[0].version, self.unique_entry_name)
510 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200511 self._assert_filter_result_matches(
512 filter_params, expected_result=versions[0].version
513 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200514
515 def test_filter_daily_versions_by_date(self):
516 """Test filtering daily versions by date."""
517 # Create Version and VersionDaily entities
518 versions = self._create_version_entities()
519
520 # Update the date
521 versions[0].date = Dummy.DATES[2]
522 versions[0].save()
523
524 # Listing all entities should return the correct result length
525 response = self.admin.get(self.endpoint_url)
526 self._assert_result_length_is(response, len(versions))
527
528 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200529 filter_params = {"date": versions[0].date}
530 self._assert_filter_result_matches(
531 filter_params, expected_result=versions[0].version
532 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200533
534
535class RadioVersionDailyTestCase(VersionDailyTestCase):
536 """Test the RadioVersionDaily REST endpoint."""
537
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200538 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200539 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200540 endpoint_url = reverse("hiccup_stats_api_v1_radio_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200541
542 @staticmethod
543 def _create_dummy_version(**kwargs):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200544 entity = RadioVersion(
545 **Dummy.update_copy(
546 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs
547 )
548 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200549 entity.save()
550 return entity
551
552 @staticmethod
553 def _create_dummy_daily_version(version, **kwargs):
554 return Dummy.create_dummy_daily_radio_version(version, **kwargs)
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400555
556
557class StatsCommandVersionsTestCase(TestCase):
558 """Test the generation of Version stats with the stats command."""
559
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400560 # FIXME: Test for false duplicates: same timestamps but different UUIDs
561 # FIXME: Test that the 'released_on' field changes or not once an older
562 # report has been sent depending on whether the field has been manually
563 # changed
564 # FIXME: Test that tests the daily version stats
565 # FIXME: Test creating stats from reports of different devices/users.
566
567 # The class of the version type to be tested
568 version_class = Version
569 # The attribute name characterising the unicity of a stats entry (the
570 # named identifier)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200571 unique_entry_name = "build_fingerprint"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400572 # The collection of unique entries to post
573 unique_entries = Dummy.BUILD_FINGERPRINTS
574
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200575 def _create_reports(
576 self, report_type, unique_entry_name, device, number, **kwargs
577 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400578 # Create reports with distinct timestamps
579 now = datetime.now(pytz.utc)
580 for i in range(number):
581 report_date = now - timedelta(milliseconds=i)
582 report_attributes = {
583 self.unique_entry_name: unique_entry_name,
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200584 "device": device,
585 "date": report_date,
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400586 }
587 report_attributes.update(**kwargs)
588 Dummy.create_dummy_report(report_type, **report_attributes)
589
590 def test_stats_calculation(self):
591 """Test generation of a Version instance."""
592 user = Dummy.create_dummy_user()
593 device = Dummy.create_dummy_device(user=user)
594 heartbeat = Dummy.create_dummy_report(HeartBeat, device=device)
595
596 # Expect that we do not have the Version before updating the stats
597 get_params = {
598 self.unique_entry_name: getattr(heartbeat, self.unique_entry_name)
599 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200600 self.assertRaises(
601 self.version_class.DoesNotExist,
602 self.version_class.objects.get,
603 **get_params
604 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400605
606 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200607 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400608
609 # Assume that a corresponding Version instance has been created
610 version = self.version_class.objects.get(**get_params)
611 self.assertIsNotNone(version)
612
613 def _assert_older_report_updates_version_date(self, report_type):
614 """Validate that older reports sent later affect the version date."""
615 user = Dummy.create_dummy_user()
616 device = Dummy.create_dummy_device(user=user)
617 report = Dummy.create_dummy_report(report_type, device=device)
618
619 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200620 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400621
622 get_params = {
623 self.unique_entry_name: getattr(report, self.unique_entry_name)
624 }
625 version = self.version_class.objects.get(**get_params)
626
627 self.assertEqual(report.date.date(), version.first_seen_on)
628
629 # Create a new report from an earlier point in time
630 report_time_2 = report.date - timedelta(weeks=1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200631 Dummy.create_dummy_report(
632 report_type, device=device, date=report_time_2
633 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400634
635 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200636 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400637
638 # Get the same version object from before
639 version = self.version_class.objects.get(**get_params)
640
641 # Validate that the date matches the report recently sent
642 self.assertEqual(report_time_2.date(), version.first_seen_on)
643
644 def test_older_heartbeat_updates_version_date(self):
645 """Validate updating version date with older heartbeats."""
646 self._assert_older_report_updates_version_date(HeartBeat)
647
648 def test_older_crash_report_updates_version_date(self):
649 """Validate updating version date with older crash reports."""
650 self._assert_older_report_updates_version_date(Crashreport)
651
652 def test_entries_are_unique(self):
653 """Validate the entries' unicity and value."""
654 # Create some reports
655 user = Dummy.create_dummy_user()
656 device = Dummy.create_dummy_device(user=user)
657 for unique_entry in self.unique_entries:
658 self._create_reports(HeartBeat, unique_entry, device, 10)
659
660 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200661 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400662
663 # Check whether the correct amount of distinct versions have been
664 # created
665 versions = self.version_class.objects.all()
666 for version in versions:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200667 self.assertIn(
668 getattr(version, self.unique_entry_name), self.unique_entries
669 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400670 self.assertEqual(len(versions), len(self.unique_entries))
671
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200672 def _assert_counter_distribution_is_correct(
673 self, report_type, numbers, counter_attribute_name, **kwargs
674 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400675 """Validate a counter distribution in the database."""
676 if len(numbers) != len(self.unique_entries):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200677 raise ValueError(
678 "The length of the numbers list must match the "
679 "length of self.unique_entries in the test class"
680 "({} != {})".format(len(numbers), len(self.unique_entries))
681 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400682 # Create some reports
683 user = Dummy.create_dummy_user()
684 device = Dummy.create_dummy_device(user=user)
685 for unique_entry, num in zip(self.unique_entries, numbers):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200686 self._create_reports(
687 report_type, unique_entry, device, num, **kwargs
688 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400689
690 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200691 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400692
693 # Check whether the numbers of reports match
694 for version in self.version_class.objects.all():
695 unique_entry_name = getattr(version, self.unique_entry_name)
696 num = numbers[self.unique_entries.index(unique_entry_name)]
697 self.assertEqual(num, getattr(version, counter_attribute_name))
698
699 def test_heartbeats_counter(self):
700 """Test the calculation of the heartbeats counter."""
701 numbers = [10, 7, 8, 5]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200702 counter_attribute_name = "heartbeats"
703 self._assert_counter_distribution_is_correct(
704 HeartBeat, numbers, counter_attribute_name
705 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400706
707 def test_crash_reports_counter(self):
708 """Test the calculation of the crashreports counter."""
709 numbers = [2, 5, 0, 3]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200710 counter_attribute_name = "prob_crashes"
711 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_UNKOWN}
712 self._assert_counter_distribution_is_correct(
713 Crashreport, numbers, counter_attribute_name, **boot_reason_param
714 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400715
716 def test_smpl_reports_counter(self):
717 """Test the calculation of the smpl reports counter."""
718 numbers = [1, 3, 4, 0]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200719 counter_attribute_name = "smpl"
720 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_RTC_ALARM}
721 self._assert_counter_distribution_is_correct(
722 Crashreport, numbers, counter_attribute_name, **boot_reason_param
723 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400724
725 def test_other_reports_counter(self):
726 """Test the calculation of the other reports counter."""
727 numbers = [0, 2, 1, 2]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200728 counter_attribute_name = "other"
729 boot_reason_param = {"boot_reason": "random boot reason"}
730 self._assert_counter_distribution_is_correct(
731 Crashreport, numbers, counter_attribute_name, **boot_reason_param
732 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400733
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200734 def _assert_duplicates_are_ignored(
735 self, report_type, device, counter_attribute_name, **kwargs
736 ):
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400737 """Validate that reports with duplicate timestamps are ignored."""
738 # Create a report
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200739 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400740
741 # Create a second report with the same timestamp
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200742 Dummy.create_dummy_report(
743 report_type, device=device, date=report.date, **kwargs
744 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400745
746 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200747 call_command("stats", "update")
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400748
749 # Get the corresponding version instance from the database
750 get_params = {
751 self.unique_entry_name: getattr(report, self.unique_entry_name)
752 }
753 version = self.version_class.objects.get(**get_params)
754
755 # Assert that the report with the duplicate timestamp is not
756 # counted, i.e. only 1 report is counted.
757 self.assertEqual(getattr(version, counter_attribute_name), 1)
758
759 def test_heartbeat_duplicates_are_ignored(self):
760 """Validate that heartbeat duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200761 counter_attribute_name = "heartbeats"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400762 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200763 self._assert_duplicates_are_ignored(
764 HeartBeat, device, counter_attribute_name
765 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400766
767 def test_crash_report_duplicates_are_ignored(self):
768 """Validate that crash report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200769 counter_attribute_name = "prob_crashes"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400770 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
771 for i, boot_reason in enumerate(Crashreport.CRASH_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200772 params = {
773 "boot_reason": boot_reason,
774 self.unique_entry_name: self.unique_entries[i],
775 }
776 self._assert_duplicates_are_ignored(
777 Crashreport, device, counter_attribute_name, **params
778 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400779
780 def test_smpl_report_duplicates_are_ignored(self):
781 """Validate that smpl report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200782 counter_attribute_name = "smpl"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400783 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
784 for i, boot_reason in enumerate(Crashreport.SMPL_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200785 params = {
786 "boot_reason": boot_reason,
787 self.unique_entry_name: self.unique_entries[i],
788 }
789 self._assert_duplicates_are_ignored(
790 Crashreport, device, counter_attribute_name, **params
791 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400792
793 def test_other_report_duplicates_are_ignored(self):
794 """Validate that other report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200795 counter_attribute_name = "other"
796 params = {"boot_reason": "random boot reason"}
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400797 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200798 self._assert_duplicates_are_ignored(
799 Crashreport, device, counter_attribute_name, **params
800 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400801
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400802
803# pylint: disable=too-many-ancestors
804class StatsCommandRadioVersionsTestCase(StatsCommandVersionsTestCase):
805 """Test the generation of RadioVersion stats with the stats command."""
806
807 version_class = RadioVersion
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200808 unique_entry_name = "radio_version"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400809 unique_entries = Dummy.RADIO_VERSIONS
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400810
811
812class CommandDebugOutputTestCase(TestCase):
813 """Test the reset and update commands debug output."""
814
815 # Additional positional arguments to pass to the commands
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200816 _CMD_ARGS = ["--no-color", "-v 2"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400817
818 # The stats models
819 _STATS_MODELS = [Version, VersionDaily, RadioVersion, RadioVersionDaily]
820 # The models that will generate an output
821 _ALL_MODELS = _STATS_MODELS + [StatsMetadata]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200822 _COUNTER_NAMES = ["heartbeats", "crashes", "smpl", "other"]
823 _COUNTER_ACTIONS = ["created", "updated"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400824
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200825 def _assert_command_output_matches(self, command, number, facts, models):
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400826 """Validate the debug output of a command.
827
828 The debug output is matched against the facts and models given in
829 the parameters.
830 """
831 buffer = StringIO()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200832 call_command("stats", command, *self._CMD_ARGS, stdout=buffer)
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400833 output = buffer.getvalue().splitlines()
834
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200835 expected_output = "{number} {model} {fact}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400836 for model in models:
837 for fact in facts:
838 self.assertIn(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200839 expected_output.format(
840 number=number, model=model.__name__, fact=fact
841 ),
842 output,
843 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400844
845 def test_reset_command_on_empty_db(self):
846 """Test the reset command on an empty database.
847
848 The reset command should yield nothing on an empty database.
849 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200850 self._assert_command_output_matches(
851 "reset", 0, ["deleted"], self._ALL_MODELS
852 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400853
854 def test_update_command_on_empty_db(self):
855 """Test the update command on an empty database.
856
857 The update command should yield nothing on an empty database.
858 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200859 pattern = "{action} for counter {counter}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +0400860 facts = [
861 pattern.format(action=counter_action, counter=counter_name)
862 for counter_action in self._COUNTER_ACTIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200863 for counter_name in self._COUNTER_NAMES
864 ]
865 self._assert_command_output_matches(
866 "update", 0, facts, self._STATS_MODELS
867 )
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200868
869 def test_reset_command_deletion_of_instances(self):
870 """Test the deletion of stats model instances with the reset command.
871
872 This test validates that model instances get deleted when the
873 reset command is called on a database that only contains a single
874 model instance for each class.
875 """
876 # Create dummy version instances
877 version = Dummy.create_dummy_version()
878 radio_version = Dummy.create_dummy_radio_version()
879 Dummy.create_dummy_daily_version(version)
880 Dummy.create_dummy_daily_radio_version(radio_version)
881 Dummy.create_dummy_stats_metadata()
882
883 # We expect that the model instances get deleted
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200884 self._assert_command_output_matches(
885 "reset", 1, ["deleted"], self._ALL_MODELS
886 )