blob: 22ef52a8b24ade8a4ba8d4c5b6d0df2583a0ac41 [file] [log] [blame]
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +02001"""Test crashreport_stats models and the 'stats' command."""
Mitja Nikolaus52e44b82018-09-04 14:23:19 +02002
3# pylint: disable=too-many-lines,too-many-public-methods
4
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04005from io import StringIO
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +04006from datetime import datetime, date, timedelta
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +02007import unittest
8
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +02009import pytz
Dirk Vogt62ff7f22017-05-04 16:07:21 +020010
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040011from django.core.management import call_command
12from django.test import TestCase
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020013from django.urls import reverse
14from django.utils.http import urlencode
15
16from rest_framework import status
17from rest_framework.test import APITestCase, APIClient
18
19from crashreport_stats.models import (
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020020 Version,
21 VersionDaily,
22 RadioVersion,
23 RadioVersionDaily,
24 StatsMetadata,
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020025)
26
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040027from crashreports.models import User, Device, Crashreport, HeartBeat
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020028
29
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020030class Dummy:
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020031 """Class for creating dummy instances for testing."""
32
33 # Valid unique entries
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020034 BUILD_FINGERPRINTS = [
35 (
Mitja Nikolaus19cf9a92018-08-23 18:15:01 +020036 "Fairphone/FP2/FP2:5.1/FP2/r4275.1_FP2_gms76_1.13.0"
37 ":user/release-keys"
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020038 ),
39 (
40 "Fairphone/FP2/FP2:5.1.1/FP2-gms75.1.13.0/FP2-gms75.1.13.0"
41 ":user/release-keys"
42 ),
43 (
44 "Fairphone/FP2/FP2:6.0.1/FP2-gms-18.04.1/FP2-gms-18.04.1"
45 ":user/release-keys"
46 ),
Mitja Nikolaus19cf9a92018-08-23 18:15:01 +020047 ("Fairphone/FP2/FP2:7.1.2/18.07.2/gms-7480c31d:user/release-keys"),
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020048 ]
49 RADIO_VERSIONS = [
50 "4437.1-FP2-0-07",
51 "4437.1-FP2-0-08",
52 "4437.1-FP2-0-09",
53 "4437.1-FP2-0-10",
54 ]
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020055
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +020056 USERNAMES = ["testuser1", "testuser2"]
57
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020058 DATES = [date(2018, 3, 19), date(2018, 3, 26), date(2018, 5, 1)]
59
60 DEFAULT_DUMMY_VERSION_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020061 "build_fingerprint": BUILD_FINGERPRINTS[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_VERSION_DAILY_VALUES = {"date": DATES[1]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020067
68 DEFAULT_DUMMY_RADIO_VERSION_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020069 "radio_version": RADIO_VERSIONS[0],
70 "first_seen_on": DATES[1],
71 "released_on": DATES[0],
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020072 }
73
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020074 DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES = {"date": DATES[1]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020075
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +020076 DEFAULT_DUMMY_STATSMETADATA_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020077 "updated_at": datetime(2018, 6, 15, 2, 12, 24, tzinfo=pytz.utc)
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +020078 }
79
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020080 DEFAULT_DUMMY_DEVICE_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020081 "board_date": datetime(2015, 12, 15, 1, 23, 45, tzinfo=pytz.utc),
82 "chipset": "Qualcomm MSM8974PRO-AA",
83 "token": "64111c62d521fb4724454ca6dea27e18f93ef56e",
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020084 }
85
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +020086 DEFAULT_DUMMY_USER_VALUES = {"username": USERNAMES[0]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020087
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040088 DEFAULT_DUMMY_HEARTBEAT_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020089 "app_version": 10100,
90 "uptime": (
91 "up time: 16 days, 21:49:56, idle time: 5 days, 20:55:04, "
92 "sleep time: 10 days, 20:46:27"
93 ),
94 "build_fingerprint": BUILD_FINGERPRINTS[0],
95 "radio_version": RADIO_VERSIONS[0],
96 "date": datetime(2018, 3, 19, tzinfo=pytz.utc),
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040097 }
98
99 DEFAULT_DUMMY_CRASHREPORT_VALUES = DEFAULT_DUMMY_HEARTBEAT_VALUES.copy()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200100 DEFAULT_DUMMY_CRASHREPORT_VALUES.update(
101 {
102 "is_fake_report": 0,
103 "boot_reason": Crashreport.BOOT_REASON_UNKOWN,
104 "power_on_reason": "it was powered on",
105 "power_off_reason": "something happened and it went off",
106 }
107 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400108
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200109 @staticmethod
110 def update_copy(original, update):
111 """Merge fields of update into a copy of original."""
112 data = original.copy()
113 data.update(update)
114 return data
115
116 @staticmethod
117 def create_dummy_user(**kwargs):
118 """Create a dummy user instance.
119
120 The dummy instance is created and saved to the database.
121 Args:
122 **kwargs:
123 Optional arguments to extend/overwrite the default values.
124
125 Returns: The created user instance.
126
127 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200128 entity = User(
129 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_USER_VALUES, kwargs)
130 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200131 entity.save()
132 return entity
133
134 @staticmethod
135 def create_dummy_device(user, **kwargs):
136 """Create a dummy device instance.
137
138 The dummy instance is created and saved to the database.
139 Args:
140 user: The user instance that the device should relate to
141 **kwargs:
142 Optional arguments to extend/overwrite the default values.
143
144 Returns: The created device instance.
145
146 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200147 entity = Device(
148 user=user,
149 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_DEVICE_VALUES, kwargs)
150 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200151 entity.save()
152 return entity
153
154 @staticmethod
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400155 def create_dummy_report(report_type, device, **kwargs):
156 """Create a dummy report instance of the given report class type.
157
158 The dummy instance is created and saved to the database.
159 Args:
160 report_type: The class of the report type to be created.
161 user: The device instance that the heartbeat should relate to
162 **kwargs:
163 Optional arguments to extend/overwrite the default values.
164
165 Returns: The created report instance.
166
167 """
168 if report_type == HeartBeat:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200169 entity = HeartBeat(
170 device=device,
171 **Dummy.update_copy(
172 Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs
173 )
174 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400175 elif report_type == Crashreport:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200176 entity = Crashreport(
177 device=device,
178 **Dummy.update_copy(
179 Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES, kwargs
180 )
181 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400182 else:
183 raise RuntimeError(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200184 "No dummy report instance can be created for {}".format(
185 report_type.__name__
186 )
187 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400188 entity.save()
189 return entity
190
191 @staticmethod
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200192 def create_dummy_version(**kwargs):
193 """Create a dummy version instance.
194
195 The dummy instance is created and saved to the database.
196 Args:
197 **kwargs:
198 Optional arguments to extend/overwrite the default values.
199
200 Returns: The created version instance.
201
202 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200203 entity = Version(
204 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_VERSION_VALUES, kwargs)
205 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200206 entity.save()
207 return entity
208
209 @staticmethod
210 def create_dummy_radio_version(**kwargs):
211 """Create a dummy radio version instance.
212
213 The dummy instance is created and saved to the database.
214 Args:
215 **kwargs:
216 Optional arguments to extend/overwrite the default values.
217
218 Returns: The created radio version instance.
219
220 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200221 entity = RadioVersion(
222 **Dummy.update_copy(
223 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs
224 )
225 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200226 entity.save()
227 return entity
228
229 @staticmethod
230 def create_dummy_daily_version(version, **kwargs):
231 """Create a dummy daily version instance.
232
233 The dummy instance is created and saved to the database.
234 Args:
235 **kwargs:
236 Optional arguments to extend/overwrite the default values.
237
238 Returns: The created daily version instance.
239
240 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200241 entity = VersionDaily(
242 version=version,
243 **Dummy.update_copy(
244 Dummy.DEFAULT_DUMMY_VERSION_DAILY_VALUES, kwargs
245 )
246 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200247 entity.save()
248 return entity
249
250 @staticmethod
251 def create_dummy_daily_radio_version(version, **kwargs):
252 """Create a dummy daily radio version instance.
253
254 The dummy instance is created and saved to the database.
255 Args:
256 **kwargs:
257 Optional arguments to extend/overwrite the default values.
258
259 Returns: The created daily radio version instance.
260
261 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200262 entity = RadioVersionDaily(
263 version=version,
264 **Dummy.update_copy(
265 Dummy.DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES, kwargs
266 )
267 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200268 entity.save()
269 return entity
270
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200271 @staticmethod
272 def create_dummy_stats_metadata(**kwargs):
273 """Create a dummy stats metadata instance.
274
275 The dummy instance is created and saved to the database.
276 Args:
277 **kwargs:
278 Optional arguments to extend/overwrite the default values.
279
280 Returns: The created stats metadata instance.
281
282 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200283 entity = StatsMetadata(
284 **Dummy.update_copy(
285 Dummy.DEFAULT_DUMMY_STATSMETADATA_VALUES, kwargs
286 )
287 )
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200288 entity.save()
289 return entity
290
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200291
292class _VersionTestCase(APITestCase):
293 """Abstract class for version-related test cases to inherit from."""
294
295 # The attribute name characterising the unicity of a stats entry (the
296 # named identifier)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200297 unique_entry_name = "build_fingerprint"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200298 # The collection of unique entries to post
299 unique_entries = Dummy.BUILD_FINGERPRINTS
300 # The URL to retrieve the stats entries from
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200301 endpoint_url = reverse("hiccup_stats_api_v1_versions")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200302
303 @classmethod
304 def setUpTestData(cls): # noqa: N802
305 """Create an admin user for accessing the API.
306
307 The APIClient that can be used to make authenticated requests to the
308 server is stored in self.admin.
309 """
310 admin_user = User.objects.create_superuser(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200311 "somebody", "somebody@example.com", "thepassword"
312 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200313 cls.admin = APIClient()
314 cls.admin.force_authenticate(admin_user)
315
316 @staticmethod
317 def _create_dummy_version(**kwargs):
318 return Dummy.create_dummy_version(**kwargs)
319
320 def _get_with_params(self, url, params):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200321 return self.admin.get("{}?{}".format(url, urlencode(params)))
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200322
323 def _assert_result_length_is(self, response, count):
324 self.assertEqual(response.status_code, status.HTTP_200_OK)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200325 self.assertIn("results", response.data)
326 self.assertIn("count", response.data)
327 self.assertEqual(response.data["count"], count)
328 self.assertEqual(len(response.data["results"]), count)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200329
330 def _assert_device_owner_has_no_get_access(self, entries_url):
331 # Create a user and device
332 user = Dummy.create_dummy_user()
333 device = Dummy.create_dummy_device(user=user)
334
335 # Create authenticated client
336 user = APIClient()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200337 user.credentials(HTTP_AUTHORIZATION="Token " + device.token)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200338
339 # Try getting entries using the client
340 response = user.get(entries_url)
341 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
342
343 def _assert_filter_result_matches(self, filter_params, expected_result):
344 # List entities with filter
345 response = self._get_with_params(self.endpoint_url, filter_params)
346
347 # Expect only the single matching result to be returned
348 self._assert_result_length_is(response, 1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200349 self.assertEqual(
350 response.data["results"][0][self.unique_entry_name],
351 getattr(expected_result, self.unique_entry_name),
352 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200353
354
355class VersionTestCase(_VersionTestCase):
356 """Test the Version and REST endpoint."""
357
358 def _create_version_entities(self):
359 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200360 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200361 for unique_entry in self.unique_entries
362 ]
363 return versions
364
365 def test_list_versions_without_authentication(self):
366 """Test listing of versions without authentication."""
367 response = self.client.get(self.endpoint_url)
368 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
369
370 def test_list_versions_as_device_owner(self):
371 """Test listing of versions as device owner."""
372 self._assert_device_owner_has_no_get_access(self.endpoint_url)
373
374 def test_list_versions_empty_database(self):
375 """Test listing of versions on an empty database."""
376 response = self.admin.get(self.endpoint_url)
377 self._assert_result_length_is(response, 0)
378
379 def test_list_versions(self):
380 """Test listing versions."""
381 versions = self._create_version_entities()
382 response = self.admin.get(self.endpoint_url)
383 self._assert_result_length_is(response, len(versions))
384
385 def test_filter_versions_by_unique_entry_name(self):
386 """Test filtering versions by their unique entry name."""
387 versions = self._create_version_entities()
388 response = self.admin.get(self.endpoint_url)
389
390 # Listing all entities should return the correct result length
391 self._assert_result_length_is(response, len(versions))
392
393 # List entities with filter
394 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200395 self.unique_entry_name: getattr(versions[0], self.unique_entry_name)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200396 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200397 self._assert_filter_result_matches(
398 filter_params, expected_result=versions[0]
399 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200400
401 def test_filter_versions_by_release_type(self):
402 """Test filtering versions by release type."""
403 # Create versions for all combinations of release types
404 versions = []
405 i = 0
406 for is_official_release in True, False:
407 for is_beta_release in True, False:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200408 versions.append(
409 self._create_dummy_version(
410 **{
411 "is_official_release": is_official_release,
412 "is_beta_release": is_beta_release,
413 self.unique_entry_name: self.unique_entries[i],
414 }
415 )
416 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200417 i += 1
418
419 # # Listing all entities should return the correct result length
420 response = self.admin.get(self.endpoint_url)
421 self._assert_result_length_is(response, len(versions))
422
423 # List each of the entities with the matching filter params
424 for version in versions:
425 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200426 "is_official_release": version.is_official_release,
427 "is_beta_release": version.is_beta_release,
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200428 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200429 self._assert_filter_result_matches(
430 filter_params, expected_result=version
431 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200432
433 def test_filter_versions_by_first_seen_date(self):
434 """Test filtering versions by first seen date."""
435 versions = self._create_version_entities()
436
437 # Set the first seen date of an entity
438 versions[0].first_seen_on = Dummy.DATES[2]
439 versions[0].save()
440
441 # Listing all entities should return the correct result length
442 response = self.admin.get(self.endpoint_url)
443 self._assert_result_length_is(response, len(versions))
444
445 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200446 filter_params = {"first_seen_after": Dummy.DATES[2]}
447 self._assert_filter_result_matches(
448 filter_params, expected_result=versions[0]
449 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200450
451
452# pylint: disable=too-many-ancestors
453class RadioVersionTestCase(VersionTestCase):
454 """Test the RadioVersion REST endpoint."""
455
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200456 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200457 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200458 endpoint_url = reverse("hiccup_stats_api_v1_radio_versions")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200459
460 @staticmethod
461 def _create_dummy_version(**kwargs):
462 return Dummy.create_dummy_radio_version(**kwargs)
463
464
465class VersionDailyTestCase(_VersionTestCase):
466 """Test the VersionDaily REST endpoint."""
467
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200468 endpoint_url = reverse("hiccup_stats_api_v1_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200469
470 @staticmethod
471 def _create_dummy_daily_version(version, **kwargs):
472 return Dummy.create_dummy_daily_version(version, **kwargs)
473
474 def _create_version_entities(self):
475 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200476 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200477 for unique_entry in self.unique_entries
478 ]
479 versions_daily = [
480 self._create_dummy_daily_version(version=version)
481 for version in versions
482 ]
483 return versions_daily
484
485 def test_list_daily_versions_without_authentication(self):
486 """Test listing of daily versions without authentication."""
487 response = self.client.get(self.endpoint_url)
488 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
489
490 def test_list_daily_versions_as_device_owner(self):
491 """Test listing of daily versions as device owner."""
492 self._assert_device_owner_has_no_get_access(self.endpoint_url)
493
494 def test_list_daily_versions_empty_database(self):
495 """Test listing of daily versions on an empty database."""
496 response = self.admin.get(self.endpoint_url)
497 self._assert_result_length_is(response, 0)
498
499 def test_list_daily_versions(self):
500 """Test listing daily versions."""
501 versions_daily = self._create_version_entities()
502 response = self.admin.get(self.endpoint_url)
503 self._assert_result_length_is(response, len(versions_daily))
504
505 def test_filter_daily_versions_by_version(self):
506 """Test filtering versions by the version they relate to."""
507 # Create VersionDaily entities
508 versions = self._create_version_entities()
509
510 # Listing all entities should return the correct result length
511 response = self.admin.get(self.endpoint_url)
512 self._assert_result_length_is(response, len(versions))
513
514 # List entities with filter
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200515 param_name = "version__" + self.unique_entry_name
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200516 filter_params = {
517 param_name: getattr(versions[0].version, self.unique_entry_name)
518 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200519 self._assert_filter_result_matches(
520 filter_params, expected_result=versions[0].version
521 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200522
523 def test_filter_daily_versions_by_date(self):
524 """Test filtering daily versions by date."""
525 # Create Version and VersionDaily entities
526 versions = self._create_version_entities()
527
528 # Update the date
529 versions[0].date = Dummy.DATES[2]
530 versions[0].save()
531
532 # Listing all entities should return the correct result length
533 response = self.admin.get(self.endpoint_url)
534 self._assert_result_length_is(response, len(versions))
535
536 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200537 filter_params = {"date": versions[0].date}
538 self._assert_filter_result_matches(
539 filter_params, expected_result=versions[0].version
540 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200541
542
543class RadioVersionDailyTestCase(VersionDailyTestCase):
544 """Test the RadioVersionDaily REST endpoint."""
545
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200546 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200547 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200548 endpoint_url = reverse("hiccup_stats_api_v1_radio_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200549
550 @staticmethod
551 def _create_dummy_version(**kwargs):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200552 entity = RadioVersion(
553 **Dummy.update_copy(
554 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs
555 )
556 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200557 entity.save()
558 return entity
559
560 @staticmethod
561 def _create_dummy_daily_version(version, **kwargs):
562 return Dummy.create_dummy_daily_radio_version(version, **kwargs)
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400563
564
565class StatsCommandVersionsTestCase(TestCase):
566 """Test the generation of Version stats with the stats command."""
567
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400568 # The class of the version type to be tested
569 version_class = Version
570 # The attribute name characterising the unicity of a stats entry (the
571 # named identifier)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200572 unique_entry_name = "build_fingerprint"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400573 # The collection of unique entries to post
574 unique_entries = Dummy.BUILD_FINGERPRINTS
575
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200576 def _create_reports(
577 self, report_type, unique_entry_name, device, number, **kwargs
578 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400579 # Create reports with distinct timestamps
580 now = datetime.now(pytz.utc)
581 for i in range(number):
582 report_date = now - timedelta(milliseconds=i)
583 report_attributes = {
584 self.unique_entry_name: unique_entry_name,
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200585 "device": device,
586 "date": report_date,
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400587 }
588 report_attributes.update(**kwargs)
589 Dummy.create_dummy_report(report_type, **report_attributes)
590
591 def test_stats_calculation(self):
592 """Test generation of a Version instance."""
593 user = Dummy.create_dummy_user()
594 device = Dummy.create_dummy_device(user=user)
595 heartbeat = Dummy.create_dummy_report(HeartBeat, device=device)
596
597 # Expect that we do not have the Version before updating the stats
598 get_params = {
599 self.unique_entry_name: getattr(heartbeat, self.unique_entry_name)
600 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200601 self.assertRaises(
602 self.version_class.DoesNotExist,
603 self.version_class.objects.get,
604 **get_params
605 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400606
607 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200608 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400609
610 # Assume that a corresponding Version instance has been created
611 version = self.version_class.objects.get(**get_params)
612 self.assertIsNotNone(version)
613
614 def _assert_older_report_updates_version_date(self, report_type):
615 """Validate that older reports sent later affect the version date."""
616 user = Dummy.create_dummy_user()
617 device = Dummy.create_dummy_device(user=user)
618 report = Dummy.create_dummy_report(report_type, device=device)
619
620 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200621 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400622
623 get_params = {
624 self.unique_entry_name: getattr(report, self.unique_entry_name)
625 }
626 version = self.version_class.objects.get(**get_params)
627
628 self.assertEqual(report.date.date(), version.first_seen_on)
629
630 # Create a new report from an earlier point in time
631 report_time_2 = report.date - timedelta(weeks=1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200632 Dummy.create_dummy_report(
633 report_type, device=device, date=report_time_2
634 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400635
636 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200637 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400638
639 # Get the same version object from before
640 version = self.version_class.objects.get(**get_params)
641
642 # Validate that the date matches the report recently sent
643 self.assertEqual(report_time_2.date(), version.first_seen_on)
644
645 def test_older_heartbeat_updates_version_date(self):
646 """Validate updating version date with older heartbeats."""
647 self._assert_older_report_updates_version_date(HeartBeat)
648
649 def test_older_crash_report_updates_version_date(self):
650 """Validate updating version date with older crash reports."""
651 self._assert_older_report_updates_version_date(Crashreport)
652
653 def test_entries_are_unique(self):
654 """Validate the entries' unicity and value."""
655 # Create some reports
656 user = Dummy.create_dummy_user()
657 device = Dummy.create_dummy_device(user=user)
658 for unique_entry in self.unique_entries:
659 self._create_reports(HeartBeat, unique_entry, device, 10)
660
661 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200662 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400663
664 # Check whether the correct amount of distinct versions have been
665 # created
666 versions = self.version_class.objects.all()
667 for version in versions:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200668 self.assertIn(
669 getattr(version, self.unique_entry_name), self.unique_entries
670 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400671 self.assertEqual(len(versions), len(self.unique_entries))
672
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200673 def _assert_counter_distribution_is_correct(
674 self, report_type, numbers, counter_attribute_name, **kwargs
675 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400676 """Validate a counter distribution in the database."""
677 if len(numbers) != len(self.unique_entries):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200678 raise ValueError(
679 "The length of the numbers list must match the "
680 "length of self.unique_entries in the test class"
681 "({} != {})".format(len(numbers), len(self.unique_entries))
682 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400683 # Create some reports
684 user = Dummy.create_dummy_user()
685 device = Dummy.create_dummy_device(user=user)
686 for unique_entry, num in zip(self.unique_entries, numbers):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200687 self._create_reports(
688 report_type, unique_entry, device, num, **kwargs
689 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400690
691 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200692 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400693
694 # Check whether the numbers of reports match
695 for version in self.version_class.objects.all():
696 unique_entry_name = getattr(version, self.unique_entry_name)
697 num = numbers[self.unique_entries.index(unique_entry_name)]
698 self.assertEqual(num, getattr(version, counter_attribute_name))
699
700 def test_heartbeats_counter(self):
701 """Test the calculation of the heartbeats counter."""
702 numbers = [10, 7, 8, 5]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200703 counter_attribute_name = "heartbeats"
704 self._assert_counter_distribution_is_correct(
705 HeartBeat, numbers, counter_attribute_name
706 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400707
708 def test_crash_reports_counter(self):
709 """Test the calculation of the crashreports counter."""
710 numbers = [2, 5, 0, 3]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200711 counter_attribute_name = "prob_crashes"
712 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_UNKOWN}
713 self._assert_counter_distribution_is_correct(
714 Crashreport, numbers, counter_attribute_name, **boot_reason_param
715 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400716
717 def test_smpl_reports_counter(self):
718 """Test the calculation of the smpl reports counter."""
719 numbers = [1, 3, 4, 0]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200720 counter_attribute_name = "smpl"
721 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_RTC_ALARM}
722 self._assert_counter_distribution_is_correct(
723 Crashreport, numbers, counter_attribute_name, **boot_reason_param
724 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400725
726 def test_other_reports_counter(self):
727 """Test the calculation of the other reports counter."""
728 numbers = [0, 2, 1, 2]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200729 counter_attribute_name = "other"
730 boot_reason_param = {"boot_reason": "random boot reason"}
731 self._assert_counter_distribution_is_correct(
732 Crashreport, numbers, counter_attribute_name, **boot_reason_param
733 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400734
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +0200735 def _assert_reports_with_same_timestamp_are_counted(
736 self, report_type, counter_attribute_name, **kwargs
737 ):
738 """Validate that reports with the same timestamp are counted.
739
740 Reports from different devices but the same timestamp should be
741 counted as independent reports.
742 """
743 # Create a report
744 device1 = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
745 report1 = Dummy.create_dummy_report(
746 report_type, device=device1, **kwargs
747 )
748
749 # Create a second report with the same timestamp but from another device
750 device2 = Dummy.create_dummy_device(
751 user=Dummy.create_dummy_user(username=Dummy.USERNAMES[1])
752 )
753 Dummy.create_dummy_report(
754 report_type, device=device2, date=report1.date, **kwargs
755 )
756
757 # Run the command to update the database
758 call_command("stats", "update")
759
760 # Get the corresponding version instance from the database
761 get_params = {
762 self.unique_entry_name: getattr(report1, self.unique_entry_name)
763 }
764 version = self.version_class.objects.get(**get_params)
765
766 # Assert that both reports are counted
767 self.assertEqual(getattr(version, counter_attribute_name), 2)
768
769 @unittest.skip(
770 "Duplicates are dropped based on their timestamp at the moment. This is"
771 "to be adapted so that they are dropped taking into account the device"
772 "UUID as well."
773 )
774 def test_heartbeats_with_same_timestamp_are_counted(self):
775 """Validate that heartbeats with same timestamp are counted."""
776 counter_attribute_name = "heartbeats"
777 self._assert_reports_with_same_timestamp_are_counted(
778 HeartBeat, counter_attribute_name
779 )
780
781 @unittest.skip(
782 "Duplicates are dropped based on their timestamp at the moment. This is"
783 "to be adapted so that they are dropped taking into account the device"
784 "UUID as well."
785 )
786 def test_crash_reports_with_same_timestamp_are_counted(self):
787 """Validate that crash report duplicates are ignored."""
788 counter_attribute_name = "prob_crashes"
789 for unique_entry, boot_reason in zip(
790 self.unique_entries, Crashreport.CRASH_BOOT_REASONS
791 ):
792 params = {
793 "boot_reason": boot_reason,
794 self.unique_entry_name: unique_entry,
795 }
796 self._assert_reports_with_same_timestamp_are_counted(
797 Crashreport, counter_attribute_name, **params
798 )
799
800 @unittest.skip(
801 "Duplicates are dropped based on their timestamp at the moment. This is"
802 "to be adapted so that they are dropped taking into account the device"
803 "UUID as well."
804 )
805 def test_smpl_reports_with_same_timestamp_are_counted(self):
806 """Validate that smpl report duplicates are ignored."""
807 counter_attribute_name = "smpl"
808 for unique_entry, boot_reason in zip(
809 self.unique_entries, Crashreport.SMPL_BOOT_REASONS
810 ):
811 params = {
812 "boot_reason": boot_reason,
813 self.unique_entry_name: unique_entry,
814 }
815 self._assert_reports_with_same_timestamp_are_counted(
816 Crashreport, counter_attribute_name, **params
817 )
818
819 @unittest.skip(
820 "Duplicates are dropped based on their timestamp at the moment. This is"
821 "to be adapted so that they are dropped taking into account the device"
822 "UUID as well."
823 )
824 def test_other_reports_with_same_timestamp_are_counted(self):
825 """Validate that other report duplicates are ignored."""
826 counter_attribute_name = "other"
827 params = {"boot_reason": "random boot reason"}
828 self._assert_reports_with_same_timestamp_are_counted(
829 Crashreport, counter_attribute_name, **params
830 )
831
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200832 def _assert_duplicates_are_ignored(
833 self, report_type, device, counter_attribute_name, **kwargs
834 ):
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400835 """Validate that reports with duplicate timestamps are ignored."""
836 # Create a report
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200837 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400838
839 # Create a second report with the same timestamp
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200840 Dummy.create_dummy_report(
841 report_type, device=device, date=report.date, **kwargs
842 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400843
844 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200845 call_command("stats", "update")
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400846
847 # Get the corresponding version instance from the database
848 get_params = {
849 self.unique_entry_name: getattr(report, self.unique_entry_name)
850 }
851 version = self.version_class.objects.get(**get_params)
852
853 # Assert that the report with the duplicate timestamp is not
854 # counted, i.e. only 1 report is counted.
855 self.assertEqual(getattr(version, counter_attribute_name), 1)
856
857 def test_heartbeat_duplicates_are_ignored(self):
858 """Validate that heartbeat duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200859 counter_attribute_name = "heartbeats"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400860 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200861 self._assert_duplicates_are_ignored(
862 HeartBeat, device, counter_attribute_name
863 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400864
865 def test_crash_report_duplicates_are_ignored(self):
866 """Validate that crash report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200867 counter_attribute_name = "prob_crashes"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400868 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
869 for i, boot_reason in enumerate(Crashreport.CRASH_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200870 params = {
871 "boot_reason": boot_reason,
872 self.unique_entry_name: self.unique_entries[i],
873 }
874 self._assert_duplicates_are_ignored(
875 Crashreport, device, counter_attribute_name, **params
876 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400877
878 def test_smpl_report_duplicates_are_ignored(self):
879 """Validate that smpl report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200880 counter_attribute_name = "smpl"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400881 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
882 for i, boot_reason in enumerate(Crashreport.SMPL_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200883 params = {
884 "boot_reason": boot_reason,
885 self.unique_entry_name: self.unique_entries[i],
886 }
887 self._assert_duplicates_are_ignored(
888 Crashreport, device, counter_attribute_name, **params
889 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400890
891 def test_other_report_duplicates_are_ignored(self):
892 """Validate that other report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200893 counter_attribute_name = "other"
894 params = {"boot_reason": "random boot reason"}
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400895 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200896 self._assert_duplicates_are_ignored(
897 Crashreport, device, counter_attribute_name, **params
898 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400899
Mitja Nikolaus52e44b82018-09-04 14:23:19 +0200900 def _assert_older_reports_update_released_on_date(
901 self, report_type, **kwargs
902 ):
903 """Test updating of the released_on date.
904
905 Validate that the released_on date is updated once an older report is
906 sent.
907 """
908 # Create a report
909 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
910 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
911
912 # Run the command to update the database
913 call_command("stats", "update")
914
915 # Get the corresponding version instance from the database
916 version = self.version_class.objects.get(
917 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
918 )
919
920 # Assert that the released_on date matches the first report date
921 self.assertEqual(version.released_on, report.date.date())
922
923 # Create a second report with the a timestamp earlier in time
924 report_2_date = report.date - timedelta(days=1)
925 Dummy.create_dummy_report(
926 report_type, device=device, date=report_2_date, **kwargs
927 )
928
929 # Run the command to update the database
930 call_command("stats", "update")
931
932 # Get the corresponding version instance from the database
933 version = self.version_class.objects.get(
934 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
935 )
936
937 # Assert that the released_on date matches the older report date
938 self.assertEqual(version.released_on, report_2_date.date())
939
940 def _assert_newer_reports_do_not_update_released_on_date(
941 self, report_type, **kwargs
942 ):
943 """Test updating of the released_on date.
944
945 Validate that the released_on date is not updated once a newer report is
946 sent.
947 """
948 # Create a report
949 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
950 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
951 report_1_date = report.date.date()
952
953 # Run the command to update the database
954 call_command("stats", "update")
955
956 # Get the corresponding version instance from the database
957 version = self.version_class.objects.get(
958 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
959 )
960
961 # Assert that the released_on date matches the first report date
962 self.assertEqual(version.released_on, report_1_date)
963
964 # Create a second report with the a timestamp later in time
965 report_2_date = report.date + timedelta(days=1)
966 Dummy.create_dummy_report(
967 report_type, device=device, date=report_2_date, **kwargs
968 )
969
970 # Run the command to update the database
971 call_command("stats", "update")
972
973 # Get the corresponding version instance from the database
974 version = self.version_class.objects.get(
975 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
976 )
977
978 # Assert that the released_on date matches the older report date
979 self.assertEqual(version.released_on, report_1_date)
980
981 def test_older_heartbeat_updates_released_on_date(self):
982 """Validate that older heartbeats update the release date."""
983 self._assert_older_reports_update_released_on_date(HeartBeat)
984
985 def test_older_crash_report_updates_released_on_date(self):
986 """Validate that older crash reports update the release date."""
987 self._assert_older_reports_update_released_on_date(Crashreport)
988
989 def test_newer_heartbeat_does_not_update_released_on_date(self):
990 """Validate that newer heartbeats don't update the release date."""
991 self._assert_newer_reports_do_not_update_released_on_date(HeartBeat)
992
993 def test_newer_crash_report_does_not_update_released_on_date(self):
994 """Validate that newer crash reports don't update the release date."""
995 self._assert_newer_reports_do_not_update_released_on_date(Crashreport)
996
997 def _assert_manually_changed_released_on_date_is_not_updated(
998 self, report_type, **kwargs
999 ):
1000 """Test updating of manually changed released_on dates.
1001
1002 Validate that a manually changed released_on date is not updated when
1003 new reports are sent.
1004 """
1005 # Create a report
1006 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
1007 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
1008
1009 # Run the command to update the database
1010 call_command("stats", "update")
1011
1012 # Get the corresponding version instance from the database
1013 version = self.version_class.objects.get(
1014 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1015 )
1016
1017 # Assert that the released_on date matches the first report date
1018 self.assertEqual(version.released_on, report.date.date())
1019
1020 # Create a second report with a timestamp earlier in time
1021 report_2_date = report.date - timedelta(days=1)
1022 Dummy.create_dummy_report(
1023 report_type, device=device, date=report_2_date, **kwargs
1024 )
1025
1026 # Manually change the released_on date
1027 version_release_date = report.date + timedelta(days=1)
1028 version.released_on = version_release_date
1029 version.save()
1030
1031 # Run the command to update the database
1032 call_command("stats", "update")
1033
1034 # Get the corresponding version instance from the database
1035 version = self.version_class.objects.get(
1036 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1037 )
1038
1039 # Assert that the released_on date still matches the date is was
1040 # manually changed to
1041 self.assertEqual(version.released_on, version_release_date.date())
1042
1043 def test_manually_changed_released_on_date_is_not_updated_by_heartbeat(
1044 self
1045 ):
1046 """Test update of manually changed released_on date with heartbeat."""
1047 self._assert_manually_changed_released_on_date_is_not_updated(HeartBeat)
1048
1049 def test_manually_changed_released_on_date_is_not_updated_by_crash_report(
1050 self
1051 ):
1052 """Test update of manually changed released_on date with crashreport."""
1053 self._assert_manually_changed_released_on_date_is_not_updated(
1054 Crashreport
1055 )
1056
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +04001057
1058# pylint: disable=too-many-ancestors
1059class StatsCommandRadioVersionsTestCase(StatsCommandVersionsTestCase):
1060 """Test the generation of RadioVersion stats with the stats command."""
1061
1062 version_class = RadioVersion
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001063 unique_entry_name = "radio_version"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +04001064 unique_entries = Dummy.RADIO_VERSIONS
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001065
1066
1067class CommandDebugOutputTestCase(TestCase):
1068 """Test the reset and update commands debug output."""
1069
1070 # Additional positional arguments to pass to the commands
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001071 _CMD_ARGS = ["--no-color", "-v 2"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001072
1073 # The stats models
1074 _STATS_MODELS = [Version, VersionDaily, RadioVersion, RadioVersionDaily]
1075 # The models that will generate an output
1076 _ALL_MODELS = _STATS_MODELS + [StatsMetadata]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001077 _COUNTER_NAMES = ["heartbeats", "crashes", "smpl", "other"]
1078 _COUNTER_ACTIONS = ["created", "updated"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001079
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +02001080 def _assert_command_output_matches(self, command, number, facts, models):
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001081 """Validate the debug output of a command.
1082
1083 The debug output is matched against the facts and models given in
1084 the parameters.
1085 """
1086 buffer = StringIO()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001087 call_command("stats", command, *self._CMD_ARGS, stdout=buffer)
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001088 output = buffer.getvalue().splitlines()
1089
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001090 expected_output = "{number} {model} {fact}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001091 for model in models:
1092 for fact in facts:
1093 self.assertIn(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001094 expected_output.format(
1095 number=number, model=model.__name__, fact=fact
1096 ),
1097 output,
1098 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001099
1100 def test_reset_command_on_empty_db(self):
1101 """Test the reset command on an empty database.
1102
1103 The reset command should yield nothing on an empty database.
1104 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001105 self._assert_command_output_matches(
1106 "reset", 0, ["deleted"], self._ALL_MODELS
1107 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001108
1109 def test_update_command_on_empty_db(self):
1110 """Test the update command on an empty database.
1111
1112 The update command should yield nothing on an empty database.
1113 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001114 pattern = "{action} for counter {counter}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001115 facts = [
1116 pattern.format(action=counter_action, counter=counter_name)
1117 for counter_action in self._COUNTER_ACTIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001118 for counter_name in self._COUNTER_NAMES
1119 ]
1120 self._assert_command_output_matches(
1121 "update", 0, facts, self._STATS_MODELS
1122 )
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +02001123
1124 def test_reset_command_deletion_of_instances(self):
1125 """Test the deletion of stats model instances with the reset command.
1126
1127 This test validates that model instances get deleted when the
1128 reset command is called on a database that only contains a single
1129 model instance for each class.
1130 """
1131 # Create dummy version instances
1132 version = Dummy.create_dummy_version()
1133 radio_version = Dummy.create_dummy_radio_version()
1134 Dummy.create_dummy_daily_version(version)
1135 Dummy.create_dummy_daily_radio_version(radio_version)
1136 Dummy.create_dummy_stats_metadata()
1137
1138 # We expect that the model instances get deleted
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001139 self._assert_command_output_matches(
1140 "reset", 1, ["deleted"], self._ALL_MODELS
1141 )