blob: d83cf5b626e57e62af2ded06d1d81701b3089a54 [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 Nikolaus78e3a052018-09-05 12:18:35 +02007import operator
8import os
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +02009import unittest
Mitja Nikolaus78e3a052018-09-05 12:18:35 +020010import zipfile
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +020011
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020012import pytz
Dirk Vogt62ff7f22017-05-04 16:07:21 +020013
Mitja Nikolaus78e3a052018-09-05 12:18:35 +020014from django.contrib.auth.models import Group
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040015from django.core.management import call_command
16from django.test import TestCase
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020017from django.urls import reverse
18from django.utils.http import urlencode
19
20from rest_framework import status
21from rest_framework.test import APITestCase, APIClient
22
23from crashreport_stats.models import (
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020024 Version,
25 VersionDaily,
26 RadioVersion,
27 RadioVersionDaily,
28 StatsMetadata,
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020029)
30
Mitja Nikolaus78e3a052018-09-05 12:18:35 +020031from crashreports.models import Crashreport, Device, HeartBeat, LogFile, User
32from hiccup.allauth_adapters import FP_STAFF_GROUP_NAME
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020033
34
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020035class Dummy:
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020036 """Class for creating dummy instances for testing."""
37
38 # Valid unique entries
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020039 BUILD_FINGERPRINTS = [
40 (
Mitja Nikolaus19cf9a92018-08-23 18:15:01 +020041 "Fairphone/FP2/FP2:5.1/FP2/r4275.1_FP2_gms76_1.13.0"
42 ":user/release-keys"
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020043 ),
44 (
45 "Fairphone/FP2/FP2:5.1.1/FP2-gms75.1.13.0/FP2-gms75.1.13.0"
46 ":user/release-keys"
47 ),
48 (
49 "Fairphone/FP2/FP2:6.0.1/FP2-gms-18.04.1/FP2-gms-18.04.1"
50 ":user/release-keys"
51 ),
Mitja Nikolaus19cf9a92018-08-23 18:15:01 +020052 ("Fairphone/FP2/FP2:7.1.2/18.07.2/gms-7480c31d:user/release-keys"),
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020053 ]
54 RADIO_VERSIONS = [
55 "4437.1-FP2-0-07",
56 "4437.1-FP2-0-08",
57 "4437.1-FP2-0-09",
58 "4437.1-FP2-0-10",
59 ]
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020060
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +020061 USERNAMES = ["testuser1", "testuser2"]
62
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020063 DATES = [date(2018, 3, 19), date(2018, 3, 26), date(2018, 5, 1)]
64
65 DEFAULT_DUMMY_VERSION_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020066 "build_fingerprint": BUILD_FINGERPRINTS[0],
67 "first_seen_on": DATES[1],
68 "released_on": DATES[0],
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020069 }
70
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020071 DEFAULT_DUMMY_VERSION_DAILY_VALUES = {"date": DATES[1]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020072
73 DEFAULT_DUMMY_RADIO_VERSION_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020074 "radio_version": RADIO_VERSIONS[0],
75 "first_seen_on": DATES[1],
76 "released_on": DATES[0],
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020077 }
78
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020079 DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES = {"date": DATES[1]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020080
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +020081 DEFAULT_DUMMY_STATSMETADATA_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020082 "updated_at": datetime(2018, 6, 15, 2, 12, 24, tzinfo=pytz.utc)
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +020083 }
84
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020085 DEFAULT_DUMMY_DEVICE_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020086 "board_date": datetime(2015, 12, 15, 1, 23, 45, tzinfo=pytz.utc),
87 "chipset": "Qualcomm MSM8974PRO-AA",
88 "token": "64111c62d521fb4724454ca6dea27e18f93ef56e",
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020089 }
90
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +020091 DEFAULT_DUMMY_USER_VALUES = {"username": USERNAMES[0]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020092
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040093 DEFAULT_DUMMY_HEARTBEAT_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020094 "app_version": 10100,
95 "uptime": (
96 "up time: 16 days, 21:49:56, idle time: 5 days, 20:55:04, "
97 "sleep time: 10 days, 20:46:27"
98 ),
99 "build_fingerprint": BUILD_FINGERPRINTS[0],
100 "radio_version": RADIO_VERSIONS[0],
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200101 "date": datetime(2018, 3, 19, 12, 0, 0, tzinfo=pytz.utc),
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400102 }
103
104 DEFAULT_DUMMY_CRASHREPORT_VALUES = DEFAULT_DUMMY_HEARTBEAT_VALUES.copy()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200105 DEFAULT_DUMMY_CRASHREPORT_VALUES.update(
106 {
107 "is_fake_report": 0,
108 "boot_reason": Crashreport.BOOT_REASON_UNKOWN,
109 "power_on_reason": "it was powered on",
110 "power_off_reason": "something happened and it went off",
111 }
112 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400113
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200114 DEFAULT_DUMMY_LOG_FILE_VALUES = {
115 "logfile_type": "last_kmsg",
116 "logfile": os.path.join("resources", "test", "test_logfile.zip"),
117 }
118
119 DEFAULT_DUMMY_LOG_FILE_NAME = "dmesg.log"
120
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200121 @staticmethod
122 def update_copy(original, update):
123 """Merge fields of update into a copy of original."""
124 data = original.copy()
125 data.update(update)
126 return data
127
128 @staticmethod
129 def create_dummy_user(**kwargs):
130 """Create a dummy user instance.
131
132 The dummy instance is created and saved to the database.
133 Args:
134 **kwargs:
135 Optional arguments to extend/overwrite the default values.
136
137 Returns: The created user instance.
138
139 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200140 entity = User(
141 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_USER_VALUES, kwargs)
142 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200143 entity.save()
144 return entity
145
146 @staticmethod
147 def create_dummy_device(user, **kwargs):
148 """Create a dummy device instance.
149
150 The dummy instance is created and saved to the database.
151 Args:
152 user: The user instance that the device should relate to
153 **kwargs:
154 Optional arguments to extend/overwrite the default values.
155
156 Returns: The created device instance.
157
158 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200159 entity = Device(
160 user=user,
161 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_DEVICE_VALUES, kwargs)
162 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200163 entity.save()
164 return entity
165
166 @staticmethod
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400167 def create_dummy_report(report_type, device, **kwargs):
168 """Create a dummy report instance of the given report class type.
169
170 The dummy instance is created and saved to the database.
171 Args:
172 report_type: The class of the report type to be created.
173 user: The device instance that the heartbeat should relate to
174 **kwargs:
175 Optional arguments to extend/overwrite the default values.
176
177 Returns: The created report instance.
178
179 """
180 if report_type == HeartBeat:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200181 entity = HeartBeat(
182 device=device,
183 **Dummy.update_copy(
184 Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs
185 )
186 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400187 elif report_type == Crashreport:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200188 entity = Crashreport(
189 device=device,
190 **Dummy.update_copy(
191 Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES, kwargs
192 )
193 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400194 else:
195 raise RuntimeError(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200196 "No dummy report instance can be created for {}".format(
197 report_type.__name__
198 )
199 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400200 entity.save()
201 return entity
202
203 @staticmethod
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200204 def create_dummy_log_file(crashreport, **kwargs):
205 """Create a dummy log file instance.
206
207 The dummy instance is created and saved to the database.
208
209 Args:
210 crashreport: The crashreport that the log file belongs to.
211 **kwargs: Optional arguments to extend/overwrite the default values.
212
213 Returns: The created log file instance.
214
215 """
216 entity = LogFile(
217 crashreport=crashreport,
218 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_LOG_FILE_VALUES, kwargs)
219 )
220
221 entity.save()
222 return entity
223
224 @staticmethod
225 def read_logfile_contents(path_to_zipfile, logfile_name):
226 """Read bytes of a zipped logfile."""
227 archive = zipfile.ZipFile(path_to_zipfile, "r")
228 return archive.read(logfile_name)
229
230 @staticmethod
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200231 def create_dummy_version(**kwargs):
232 """Create a dummy version instance.
233
234 The dummy instance is created and saved to the database.
235 Args:
236 **kwargs:
237 Optional arguments to extend/overwrite the default values.
238
239 Returns: The created version instance.
240
241 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200242 entity = Version(
243 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_VERSION_VALUES, kwargs)
244 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200245 entity.save()
246 return entity
247
248 @staticmethod
249 def create_dummy_radio_version(**kwargs):
250 """Create a dummy radio version instance.
251
252 The dummy instance is created and saved to the database.
253 Args:
254 **kwargs:
255 Optional arguments to extend/overwrite the default values.
256
257 Returns: The created radio version instance.
258
259 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200260 entity = RadioVersion(
261 **Dummy.update_copy(
262 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs
263 )
264 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200265 entity.save()
266 return entity
267
268 @staticmethod
269 def create_dummy_daily_version(version, **kwargs):
270 """Create a dummy daily version instance.
271
272 The dummy instance is created and saved to the database.
273 Args:
274 **kwargs:
275 Optional arguments to extend/overwrite the default values.
276
277 Returns: The created daily version instance.
278
279 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200280 entity = VersionDaily(
281 version=version,
282 **Dummy.update_copy(
283 Dummy.DEFAULT_DUMMY_VERSION_DAILY_VALUES, kwargs
284 )
285 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200286 entity.save()
287 return entity
288
289 @staticmethod
290 def create_dummy_daily_radio_version(version, **kwargs):
291 """Create a dummy daily radio version instance.
292
293 The dummy instance is created and saved to the database.
294 Args:
295 **kwargs:
296 Optional arguments to extend/overwrite the default values.
297
298 Returns: The created daily radio version instance.
299
300 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200301 entity = RadioVersionDaily(
302 version=version,
303 **Dummy.update_copy(
304 Dummy.DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES, kwargs
305 )
306 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200307 entity.save()
308 return entity
309
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200310 @staticmethod
311 def create_dummy_stats_metadata(**kwargs):
312 """Create a dummy stats metadata instance.
313
314 The dummy instance is created and saved to the database.
315 Args:
316 **kwargs:
317 Optional arguments to extend/overwrite the default values.
318
319 Returns: The created stats metadata instance.
320
321 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200322 entity = StatsMetadata(
323 **Dummy.update_copy(
324 Dummy.DEFAULT_DUMMY_STATSMETADATA_VALUES, kwargs
325 )
326 )
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200327 entity.save()
328 return entity
329
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200330
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200331class _HiccupAPITestCase(APITestCase):
332 """Abstract class for Hiccup REST API test cases to inherit from."""
333
334 @classmethod
335 def setUpTestData(cls): # noqa: N802
336 """Create an admin and client user for accessing the API.
337
338 The APIClient that can be used to make authenticated requests as
339 admin user is stored in self.admin. Another client (which is
340 related to a user that is part of the Fairphone software team group)
341 is stored in self.fp_staff_client.
342 """
343 admin_user = User.objects.create_superuser(
344 "somebody", "somebody@example.com", "thepassword"
345 )
346 cls.admin = APIClient()
347 cls.admin.force_authenticate(admin_user)
348
349 fp_software_team_group = Group(name=FP_STAFF_GROUP_NAME)
350 fp_software_team_group.save()
351 fp_software_team_user = User.objects.create_user(
352 "fp_staff", "somebody@fairphone.com", "thepassword"
353 )
354 fp_software_team_user.groups.add(fp_software_team_group)
355 cls.fp_staff_client = APIClient()
356 cls.fp_staff_client.login(username="fp_staff", password="thepassword")
357
358
Mitja Nikolaus4d19e182018-09-05 13:45:36 +0200359class StatusTestCase(_HiccupAPITestCase):
360 """Test the status endpoint."""
361
362 status_url = reverse("hiccup_stats_api_v1_status")
363
364 def _assert_status_response_is(
365 self, response, num_devices, num_crashreports, num_heartbeats
366 ):
367 self.assertEqual(response.status_code, status.HTTP_200_OK)
368 self.assertIn("devices", response.data)
369 self.assertIn("crashreports", response.data)
370 self.assertIn("heartbeats", response.data)
371 self.assertEqual(response.data["devices"], num_devices)
372 self.assertEqual(response.data["crashreports"], num_crashreports)
373 self.assertEqual(response.data["heartbeats"], num_heartbeats)
374
375 def test_get_status_empty_database(self):
376 """Get the status when the database is empty."""
377 response = self.fp_staff_client.get(self.status_url)
378 self._assert_status_response_is(response, 0, 0, 0)
379
380 def test_get_status(self):
381 """Get the status after some reports have been created."""
382 # Create a device with a heartbeat and a crash report
383 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
384 Dummy.create_dummy_report(HeartBeat, device)
385 Dummy.create_dummy_report(Crashreport, device)
386
387 # Create a second device without any reports
388 Dummy.create_dummy_device(
389 Dummy.create_dummy_user(username=Dummy.USERNAMES[1])
390 )
391
392 # Assert that the status includes the appropriate numbers
393 response = self.fp_staff_client.get(self.status_url)
394 self._assert_status_response_is(
395 response, num_devices=2, num_crashreports=1, num_heartbeats=1
396 )
397
398
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200399class _VersionTestCase(_HiccupAPITestCase):
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200400 """Abstract class for version-related test cases to inherit from."""
401
402 # The attribute name characterising the unicity of a stats entry (the
403 # named identifier)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200404 unique_entry_name = "build_fingerprint"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200405 # The collection of unique entries to post
406 unique_entries = Dummy.BUILD_FINGERPRINTS
407 # The URL to retrieve the stats entries from
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200408 endpoint_url = reverse("hiccup_stats_api_v1_versions")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200409
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200410 @staticmethod
411 def _create_dummy_version(**kwargs):
412 return Dummy.create_dummy_version(**kwargs)
413
414 def _get_with_params(self, url, params):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200415 return self.admin.get("{}?{}".format(url, urlencode(params)))
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200416
417 def _assert_result_length_is(self, response, count):
418 self.assertEqual(response.status_code, status.HTTP_200_OK)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200419 self.assertIn("results", response.data)
420 self.assertIn("count", response.data)
421 self.assertEqual(response.data["count"], count)
422 self.assertEqual(len(response.data["results"]), count)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200423
424 def _assert_device_owner_has_no_get_access(self, entries_url):
425 # Create a user and device
426 user = Dummy.create_dummy_user()
427 device = Dummy.create_dummy_device(user=user)
428
429 # Create authenticated client
430 user = APIClient()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200431 user.credentials(HTTP_AUTHORIZATION="Token " + device.token)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200432
433 # Try getting entries using the client
434 response = user.get(entries_url)
435 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
436
437 def _assert_filter_result_matches(self, filter_params, expected_result):
438 # List entities with filter
439 response = self._get_with_params(self.endpoint_url, filter_params)
440
441 # Expect only the single matching result to be returned
442 self._assert_result_length_is(response, 1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200443 self.assertEqual(
444 response.data["results"][0][self.unique_entry_name],
445 getattr(expected_result, self.unique_entry_name),
446 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200447
448
449class VersionTestCase(_VersionTestCase):
450 """Test the Version and REST endpoint."""
451
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200452 # pylint: disable=too-many-ancestors
453
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200454 def _create_version_entities(self):
455 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200456 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200457 for unique_entry in self.unique_entries
458 ]
459 return versions
460
461 def test_list_versions_without_authentication(self):
462 """Test listing of versions without authentication."""
463 response = self.client.get(self.endpoint_url)
464 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
465
466 def test_list_versions_as_device_owner(self):
467 """Test listing of versions as device owner."""
468 self._assert_device_owner_has_no_get_access(self.endpoint_url)
469
470 def test_list_versions_empty_database(self):
471 """Test listing of versions on an empty database."""
472 response = self.admin.get(self.endpoint_url)
473 self._assert_result_length_is(response, 0)
474
475 def test_list_versions(self):
476 """Test listing versions."""
477 versions = self._create_version_entities()
478 response = self.admin.get(self.endpoint_url)
479 self._assert_result_length_is(response, len(versions))
480
481 def test_filter_versions_by_unique_entry_name(self):
482 """Test filtering versions by their unique entry name."""
483 versions = self._create_version_entities()
484 response = self.admin.get(self.endpoint_url)
485
486 # Listing all entities should return the correct result length
487 self._assert_result_length_is(response, len(versions))
488
489 # List entities with filter
490 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200491 self.unique_entry_name: getattr(versions[0], self.unique_entry_name)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200492 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200493 self._assert_filter_result_matches(
494 filter_params, expected_result=versions[0]
495 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200496
497 def test_filter_versions_by_release_type(self):
498 """Test filtering versions by release type."""
499 # Create versions for all combinations of release types
500 versions = []
501 i = 0
502 for is_official_release in True, False:
503 for is_beta_release in True, False:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200504 versions.append(
505 self._create_dummy_version(
506 **{
507 "is_official_release": is_official_release,
508 "is_beta_release": is_beta_release,
509 self.unique_entry_name: self.unique_entries[i],
510 }
511 )
512 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200513 i += 1
514
515 # # Listing all entities should return the correct result length
516 response = self.admin.get(self.endpoint_url)
517 self._assert_result_length_is(response, len(versions))
518
519 # List each of the entities with the matching filter params
520 for version in versions:
521 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200522 "is_official_release": version.is_official_release,
523 "is_beta_release": version.is_beta_release,
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200524 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200525 self._assert_filter_result_matches(
526 filter_params, expected_result=version
527 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200528
529 def test_filter_versions_by_first_seen_date(self):
530 """Test filtering versions by first seen date."""
531 versions = self._create_version_entities()
532
533 # Set the first seen date of an entity
534 versions[0].first_seen_on = Dummy.DATES[2]
535 versions[0].save()
536
537 # Listing all entities should return the correct result length
538 response = self.admin.get(self.endpoint_url)
539 self._assert_result_length_is(response, len(versions))
540
541 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200542 filter_params = {"first_seen_after": Dummy.DATES[2]}
543 self._assert_filter_result_matches(
544 filter_params, expected_result=versions[0]
545 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200546
547
548# pylint: disable=too-many-ancestors
549class RadioVersionTestCase(VersionTestCase):
550 """Test the RadioVersion REST endpoint."""
551
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200552 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200553 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200554 endpoint_url = reverse("hiccup_stats_api_v1_radio_versions")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200555
556 @staticmethod
557 def _create_dummy_version(**kwargs):
558 return Dummy.create_dummy_radio_version(**kwargs)
559
560
561class VersionDailyTestCase(_VersionTestCase):
562 """Test the VersionDaily REST endpoint."""
563
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200564 endpoint_url = reverse("hiccup_stats_api_v1_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200565
566 @staticmethod
567 def _create_dummy_daily_version(version, **kwargs):
568 return Dummy.create_dummy_daily_version(version, **kwargs)
569
570 def _create_version_entities(self):
571 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200572 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200573 for unique_entry in self.unique_entries
574 ]
575 versions_daily = [
576 self._create_dummy_daily_version(version=version)
577 for version in versions
578 ]
579 return versions_daily
580
581 def test_list_daily_versions_without_authentication(self):
582 """Test listing of daily versions without authentication."""
583 response = self.client.get(self.endpoint_url)
584 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
585
586 def test_list_daily_versions_as_device_owner(self):
587 """Test listing of daily versions as device owner."""
588 self._assert_device_owner_has_no_get_access(self.endpoint_url)
589
590 def test_list_daily_versions_empty_database(self):
591 """Test listing of daily versions on an empty database."""
592 response = self.admin.get(self.endpoint_url)
593 self._assert_result_length_is(response, 0)
594
595 def test_list_daily_versions(self):
596 """Test listing daily versions."""
597 versions_daily = self._create_version_entities()
598 response = self.admin.get(self.endpoint_url)
599 self._assert_result_length_is(response, len(versions_daily))
600
601 def test_filter_daily_versions_by_version(self):
602 """Test filtering versions by the version they relate to."""
603 # Create VersionDaily entities
604 versions = self._create_version_entities()
605
606 # Listing all entities should return the correct result length
607 response = self.admin.get(self.endpoint_url)
608 self._assert_result_length_is(response, len(versions))
609
610 # List entities with filter
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200611 param_name = "version__" + self.unique_entry_name
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200612 filter_params = {
613 param_name: getattr(versions[0].version, self.unique_entry_name)
614 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200615 self._assert_filter_result_matches(
616 filter_params, expected_result=versions[0].version
617 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200618
619 def test_filter_daily_versions_by_date(self):
620 """Test filtering daily versions by date."""
621 # Create Version and VersionDaily entities
622 versions = self._create_version_entities()
623
624 # Update the date
625 versions[0].date = Dummy.DATES[2]
626 versions[0].save()
627
628 # Listing all entities should return the correct result length
629 response = self.admin.get(self.endpoint_url)
630 self._assert_result_length_is(response, len(versions))
631
632 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200633 filter_params = {"date": versions[0].date}
634 self._assert_filter_result_matches(
635 filter_params, expected_result=versions[0].version
636 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200637
638
639class RadioVersionDailyTestCase(VersionDailyTestCase):
640 """Test the RadioVersionDaily REST endpoint."""
641
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200642 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200643 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200644 endpoint_url = reverse("hiccup_stats_api_v1_radio_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200645
646 @staticmethod
647 def _create_dummy_version(**kwargs):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200648 entity = RadioVersion(
649 **Dummy.update_copy(
650 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs
651 )
652 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200653 entity.save()
654 return entity
655
656 @staticmethod
657 def _create_dummy_daily_version(version, **kwargs):
658 return Dummy.create_dummy_daily_radio_version(version, **kwargs)
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400659
660
661class StatsCommandVersionsTestCase(TestCase):
662 """Test the generation of Version stats with the stats command."""
663
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400664 # The class of the version type to be tested
665 version_class = Version
666 # The attribute name characterising the unicity of a stats entry (the
667 # named identifier)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200668 unique_entry_name = "build_fingerprint"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400669 # The collection of unique entries to post
670 unique_entries = Dummy.BUILD_FINGERPRINTS
671
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200672 def _create_reports(
673 self, report_type, unique_entry_name, device, number, **kwargs
674 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400675 # Create reports with distinct timestamps
676 now = datetime.now(pytz.utc)
677 for i in range(number):
678 report_date = now - timedelta(milliseconds=i)
679 report_attributes = {
680 self.unique_entry_name: unique_entry_name,
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200681 "device": device,
682 "date": report_date,
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400683 }
684 report_attributes.update(**kwargs)
685 Dummy.create_dummy_report(report_type, **report_attributes)
686
687 def test_stats_calculation(self):
688 """Test generation of a Version instance."""
689 user = Dummy.create_dummy_user()
690 device = Dummy.create_dummy_device(user=user)
691 heartbeat = Dummy.create_dummy_report(HeartBeat, device=device)
692
693 # Expect that we do not have the Version before updating the stats
694 get_params = {
695 self.unique_entry_name: getattr(heartbeat, self.unique_entry_name)
696 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200697 self.assertRaises(
698 self.version_class.DoesNotExist,
699 self.version_class.objects.get,
700 **get_params
701 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400702
703 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200704 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400705
706 # Assume that a corresponding Version instance has been created
707 version = self.version_class.objects.get(**get_params)
708 self.assertIsNotNone(version)
709
710 def _assert_older_report_updates_version_date(self, report_type):
711 """Validate that older reports sent later affect the version date."""
712 user = Dummy.create_dummy_user()
713 device = Dummy.create_dummy_device(user=user)
714 report = Dummy.create_dummy_report(report_type, device=device)
715
716 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200717 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400718
719 get_params = {
720 self.unique_entry_name: getattr(report, self.unique_entry_name)
721 }
722 version = self.version_class.objects.get(**get_params)
723
724 self.assertEqual(report.date.date(), version.first_seen_on)
725
726 # Create a new report from an earlier point in time
727 report_time_2 = report.date - timedelta(weeks=1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200728 Dummy.create_dummy_report(
729 report_type, device=device, date=report_time_2
730 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400731
732 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200733 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400734
735 # Get the same version object from before
736 version = self.version_class.objects.get(**get_params)
737
738 # Validate that the date matches the report recently sent
739 self.assertEqual(report_time_2.date(), version.first_seen_on)
740
741 def test_older_heartbeat_updates_version_date(self):
742 """Validate updating version date with older heartbeats."""
743 self._assert_older_report_updates_version_date(HeartBeat)
744
745 def test_older_crash_report_updates_version_date(self):
746 """Validate updating version date with older crash reports."""
747 self._assert_older_report_updates_version_date(Crashreport)
748
749 def test_entries_are_unique(self):
750 """Validate the entries' unicity and value."""
751 # Create some reports
752 user = Dummy.create_dummy_user()
753 device = Dummy.create_dummy_device(user=user)
754 for unique_entry in self.unique_entries:
755 self._create_reports(HeartBeat, unique_entry, device, 10)
756
757 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200758 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400759
760 # Check whether the correct amount of distinct versions have been
761 # created
762 versions = self.version_class.objects.all()
763 for version in versions:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200764 self.assertIn(
765 getattr(version, self.unique_entry_name), self.unique_entries
766 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400767 self.assertEqual(len(versions), len(self.unique_entries))
768
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200769 def _assert_counter_distribution_is_correct(
770 self, report_type, numbers, counter_attribute_name, **kwargs
771 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400772 """Validate a counter distribution in the database."""
773 if len(numbers) != len(self.unique_entries):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200774 raise ValueError(
775 "The length of the numbers list must match the "
776 "length of self.unique_entries in the test class"
777 "({} != {})".format(len(numbers), len(self.unique_entries))
778 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400779 # Create some reports
780 user = Dummy.create_dummy_user()
781 device = Dummy.create_dummy_device(user=user)
782 for unique_entry, num in zip(self.unique_entries, numbers):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200783 self._create_reports(
784 report_type, unique_entry, device, num, **kwargs
785 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400786
787 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200788 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400789
790 # Check whether the numbers of reports match
791 for version in self.version_class.objects.all():
792 unique_entry_name = getattr(version, self.unique_entry_name)
793 num = numbers[self.unique_entries.index(unique_entry_name)]
794 self.assertEqual(num, getattr(version, counter_attribute_name))
795
796 def test_heartbeats_counter(self):
797 """Test the calculation of the heartbeats counter."""
798 numbers = [10, 7, 8, 5]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200799 counter_attribute_name = "heartbeats"
800 self._assert_counter_distribution_is_correct(
801 HeartBeat, numbers, counter_attribute_name
802 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400803
804 def test_crash_reports_counter(self):
805 """Test the calculation of the crashreports counter."""
806 numbers = [2, 5, 0, 3]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200807 counter_attribute_name = "prob_crashes"
808 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_UNKOWN}
809 self._assert_counter_distribution_is_correct(
810 Crashreport, numbers, counter_attribute_name, **boot_reason_param
811 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400812
813 def test_smpl_reports_counter(self):
814 """Test the calculation of the smpl reports counter."""
815 numbers = [1, 3, 4, 0]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200816 counter_attribute_name = "smpl"
817 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_RTC_ALARM}
818 self._assert_counter_distribution_is_correct(
819 Crashreport, numbers, counter_attribute_name, **boot_reason_param
820 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400821
822 def test_other_reports_counter(self):
823 """Test the calculation of the other reports counter."""
824 numbers = [0, 2, 1, 2]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200825 counter_attribute_name = "other"
826 boot_reason_param = {"boot_reason": "random boot reason"}
827 self._assert_counter_distribution_is_correct(
828 Crashreport, numbers, counter_attribute_name, **boot_reason_param
829 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400830
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +0200831 def _assert_reports_with_same_timestamp_are_counted(
832 self, report_type, counter_attribute_name, **kwargs
833 ):
834 """Validate that reports with the same timestamp are counted.
835
836 Reports from different devices but the same timestamp should be
837 counted as independent reports.
838 """
839 # Create a report
840 device1 = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
841 report1 = Dummy.create_dummy_report(
842 report_type, device=device1, **kwargs
843 )
844
845 # Create a second report with the same timestamp but from another device
846 device2 = Dummy.create_dummy_device(
847 user=Dummy.create_dummy_user(username=Dummy.USERNAMES[1])
848 )
849 Dummy.create_dummy_report(
850 report_type, device=device2, date=report1.date, **kwargs
851 )
852
853 # Run the command to update the database
854 call_command("stats", "update")
855
856 # Get the corresponding version instance from the database
857 get_params = {
858 self.unique_entry_name: getattr(report1, self.unique_entry_name)
859 }
860 version = self.version_class.objects.get(**get_params)
861
862 # Assert that both reports are counted
863 self.assertEqual(getattr(version, counter_attribute_name), 2)
864
865 @unittest.skip(
866 "Duplicates are dropped based on their timestamp at the moment. This is"
867 "to be adapted so that they are dropped taking into account the device"
868 "UUID as well."
869 )
870 def test_heartbeats_with_same_timestamp_are_counted(self):
871 """Validate that heartbeats with same timestamp are counted."""
872 counter_attribute_name = "heartbeats"
873 self._assert_reports_with_same_timestamp_are_counted(
874 HeartBeat, counter_attribute_name
875 )
876
877 @unittest.skip(
878 "Duplicates are dropped based on their timestamp at the moment. This is"
879 "to be adapted so that they are dropped taking into account the device"
880 "UUID as well."
881 )
882 def test_crash_reports_with_same_timestamp_are_counted(self):
883 """Validate that crash report duplicates are ignored."""
884 counter_attribute_name = "prob_crashes"
885 for unique_entry, boot_reason in zip(
886 self.unique_entries, Crashreport.CRASH_BOOT_REASONS
887 ):
888 params = {
889 "boot_reason": boot_reason,
890 self.unique_entry_name: unique_entry,
891 }
892 self._assert_reports_with_same_timestamp_are_counted(
893 Crashreport, counter_attribute_name, **params
894 )
895
896 @unittest.skip(
897 "Duplicates are dropped based on their timestamp at the moment. This is"
898 "to be adapted so that they are dropped taking into account the device"
899 "UUID as well."
900 )
901 def test_smpl_reports_with_same_timestamp_are_counted(self):
902 """Validate that smpl report duplicates are ignored."""
903 counter_attribute_name = "smpl"
904 for unique_entry, boot_reason in zip(
905 self.unique_entries, Crashreport.SMPL_BOOT_REASONS
906 ):
907 params = {
908 "boot_reason": boot_reason,
909 self.unique_entry_name: unique_entry,
910 }
911 self._assert_reports_with_same_timestamp_are_counted(
912 Crashreport, counter_attribute_name, **params
913 )
914
915 @unittest.skip(
916 "Duplicates are dropped based on their timestamp at the moment. This is"
917 "to be adapted so that they are dropped taking into account the device"
918 "UUID as well."
919 )
920 def test_other_reports_with_same_timestamp_are_counted(self):
921 """Validate that other report duplicates are ignored."""
922 counter_attribute_name = "other"
923 params = {"boot_reason": "random boot reason"}
924 self._assert_reports_with_same_timestamp_are_counted(
925 Crashreport, counter_attribute_name, **params
926 )
927
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200928 def _assert_duplicates_are_ignored(
929 self, report_type, device, counter_attribute_name, **kwargs
930 ):
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400931 """Validate that reports with duplicate timestamps are ignored."""
932 # Create a report
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200933 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400934
935 # Create a second report with the same timestamp
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200936 Dummy.create_dummy_report(
937 report_type, device=device, date=report.date, **kwargs
938 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400939
940 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200941 call_command("stats", "update")
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400942
943 # Get the corresponding version instance from the database
944 get_params = {
945 self.unique_entry_name: getattr(report, self.unique_entry_name)
946 }
947 version = self.version_class.objects.get(**get_params)
948
949 # Assert that the report with the duplicate timestamp is not
950 # counted, i.e. only 1 report is counted.
951 self.assertEqual(getattr(version, counter_attribute_name), 1)
952
953 def test_heartbeat_duplicates_are_ignored(self):
954 """Validate that heartbeat duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200955 counter_attribute_name = "heartbeats"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400956 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200957 self._assert_duplicates_are_ignored(
958 HeartBeat, device, counter_attribute_name
959 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400960
961 def test_crash_report_duplicates_are_ignored(self):
962 """Validate that crash report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200963 counter_attribute_name = "prob_crashes"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400964 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
965 for i, boot_reason in enumerate(Crashreport.CRASH_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200966 params = {
967 "boot_reason": boot_reason,
968 self.unique_entry_name: self.unique_entries[i],
969 }
970 self._assert_duplicates_are_ignored(
971 Crashreport, device, counter_attribute_name, **params
972 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400973
974 def test_smpl_report_duplicates_are_ignored(self):
975 """Validate that smpl report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200976 counter_attribute_name = "smpl"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400977 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
978 for i, boot_reason in enumerate(Crashreport.SMPL_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200979 params = {
980 "boot_reason": boot_reason,
981 self.unique_entry_name: self.unique_entries[i],
982 }
983 self._assert_duplicates_are_ignored(
984 Crashreport, device, counter_attribute_name, **params
985 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400986
987 def test_other_report_duplicates_are_ignored(self):
988 """Validate that other report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200989 counter_attribute_name = "other"
990 params = {"boot_reason": "random boot reason"}
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400991 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200992 self._assert_duplicates_are_ignored(
993 Crashreport, device, counter_attribute_name, **params
994 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400995
Mitja Nikolaus52e44b82018-09-04 14:23:19 +0200996 def _assert_older_reports_update_released_on_date(
997 self, report_type, **kwargs
998 ):
999 """Test updating of the released_on date.
1000
1001 Validate that the released_on date is updated once an older report is
1002 sent.
1003 """
1004 # Create a report
1005 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
1006 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
1007
1008 # Run the command to update the database
1009 call_command("stats", "update")
1010
1011 # Get the corresponding version instance from the database
1012 version = self.version_class.objects.get(
1013 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1014 )
1015
1016 # Assert that the released_on date matches the first report date
1017 self.assertEqual(version.released_on, report.date.date())
1018
1019 # Create a second report with the a timestamp earlier in time
1020 report_2_date = report.date - timedelta(days=1)
1021 Dummy.create_dummy_report(
1022 report_type, device=device, date=report_2_date, **kwargs
1023 )
1024
1025 # Run the command to update the database
1026 call_command("stats", "update")
1027
1028 # Get the corresponding version instance from the database
1029 version = self.version_class.objects.get(
1030 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1031 )
1032
1033 # Assert that the released_on date matches the older report date
1034 self.assertEqual(version.released_on, report_2_date.date())
1035
1036 def _assert_newer_reports_do_not_update_released_on_date(
1037 self, report_type, **kwargs
1038 ):
1039 """Test updating of the released_on date.
1040
1041 Validate that the released_on date is not updated once a newer report is
1042 sent.
1043 """
1044 # Create a report
1045 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
1046 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
1047 report_1_date = report.date.date()
1048
1049 # Run the command to update the database
1050 call_command("stats", "update")
1051
1052 # Get the corresponding version instance from the database
1053 version = self.version_class.objects.get(
1054 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1055 )
1056
1057 # Assert that the released_on date matches the first report date
1058 self.assertEqual(version.released_on, report_1_date)
1059
1060 # Create a second report with the a timestamp later in time
1061 report_2_date = report.date + timedelta(days=1)
1062 Dummy.create_dummy_report(
1063 report_type, device=device, date=report_2_date, **kwargs
1064 )
1065
1066 # Run the command to update the database
1067 call_command("stats", "update")
1068
1069 # Get the corresponding version instance from the database
1070 version = self.version_class.objects.get(
1071 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1072 )
1073
1074 # Assert that the released_on date matches the older report date
1075 self.assertEqual(version.released_on, report_1_date)
1076
1077 def test_older_heartbeat_updates_released_on_date(self):
1078 """Validate that older heartbeats update the release date."""
1079 self._assert_older_reports_update_released_on_date(HeartBeat)
1080
1081 def test_older_crash_report_updates_released_on_date(self):
1082 """Validate that older crash reports update the release date."""
1083 self._assert_older_reports_update_released_on_date(Crashreport)
1084
1085 def test_newer_heartbeat_does_not_update_released_on_date(self):
1086 """Validate that newer heartbeats don't update the release date."""
1087 self._assert_newer_reports_do_not_update_released_on_date(HeartBeat)
1088
1089 def test_newer_crash_report_does_not_update_released_on_date(self):
1090 """Validate that newer crash reports don't update the release date."""
1091 self._assert_newer_reports_do_not_update_released_on_date(Crashreport)
1092
1093 def _assert_manually_changed_released_on_date_is_not_updated(
1094 self, report_type, **kwargs
1095 ):
1096 """Test updating of manually changed released_on dates.
1097
1098 Validate that a manually changed released_on date is not updated when
1099 new reports are sent.
1100 """
1101 # Create a report
1102 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
1103 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
1104
1105 # Run the command to update the database
1106 call_command("stats", "update")
1107
1108 # Get the corresponding version instance from the database
1109 version = self.version_class.objects.get(
1110 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1111 )
1112
1113 # Assert that the released_on date matches the first report date
1114 self.assertEqual(version.released_on, report.date.date())
1115
1116 # Create a second report with a timestamp earlier in time
1117 report_2_date = report.date - timedelta(days=1)
1118 Dummy.create_dummy_report(
1119 report_type, device=device, date=report_2_date, **kwargs
1120 )
1121
1122 # Manually change the released_on date
1123 version_release_date = report.date + timedelta(days=1)
1124 version.released_on = version_release_date
1125 version.save()
1126
1127 # Run the command to update the database
1128 call_command("stats", "update")
1129
1130 # Get the corresponding version instance from the database
1131 version = self.version_class.objects.get(
1132 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1133 )
1134
1135 # Assert that the released_on date still matches the date is was
1136 # manually changed to
1137 self.assertEqual(version.released_on, version_release_date.date())
1138
1139 def test_manually_changed_released_on_date_is_not_updated_by_heartbeat(
1140 self
1141 ):
1142 """Test update of manually changed released_on date with heartbeat."""
1143 self._assert_manually_changed_released_on_date_is_not_updated(HeartBeat)
1144
1145 def test_manually_changed_released_on_date_is_not_updated_by_crash_report(
1146 self
1147 ):
1148 """Test update of manually changed released_on date with crashreport."""
1149 self._assert_manually_changed_released_on_date_is_not_updated(
1150 Crashreport
1151 )
1152
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +04001153
1154# pylint: disable=too-many-ancestors
1155class StatsCommandRadioVersionsTestCase(StatsCommandVersionsTestCase):
1156 """Test the generation of RadioVersion stats with the stats command."""
1157
1158 version_class = RadioVersion
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001159 unique_entry_name = "radio_version"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +04001160 unique_entries = Dummy.RADIO_VERSIONS
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001161
1162
1163class CommandDebugOutputTestCase(TestCase):
1164 """Test the reset and update commands debug output."""
1165
1166 # Additional positional arguments to pass to the commands
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001167 _CMD_ARGS = ["--no-color", "-v 2"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001168
1169 # The stats models
1170 _STATS_MODELS = [Version, VersionDaily, RadioVersion, RadioVersionDaily]
1171 # The models that will generate an output
1172 _ALL_MODELS = _STATS_MODELS + [StatsMetadata]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001173 _COUNTER_NAMES = ["heartbeats", "crashes", "smpl", "other"]
1174 _COUNTER_ACTIONS = ["created", "updated"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001175
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +02001176 def _assert_command_output_matches(self, command, number, facts, models):
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001177 """Validate the debug output of a command.
1178
1179 The debug output is matched against the facts and models given in
1180 the parameters.
1181 """
1182 buffer = StringIO()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001183 call_command("stats", command, *self._CMD_ARGS, stdout=buffer)
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001184 output = buffer.getvalue().splitlines()
1185
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001186 expected_output = "{number} {model} {fact}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001187 for model in models:
1188 for fact in facts:
1189 self.assertIn(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001190 expected_output.format(
1191 number=number, model=model.__name__, fact=fact
1192 ),
1193 output,
1194 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001195
1196 def test_reset_command_on_empty_db(self):
1197 """Test the reset command on an empty database.
1198
1199 The reset command should yield nothing on an empty database.
1200 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001201 self._assert_command_output_matches(
1202 "reset", 0, ["deleted"], self._ALL_MODELS
1203 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001204
1205 def test_update_command_on_empty_db(self):
1206 """Test the update command on an empty database.
1207
1208 The update command should yield nothing on an empty database.
1209 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001210 pattern = "{action} for counter {counter}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001211 facts = [
1212 pattern.format(action=counter_action, counter=counter_name)
1213 for counter_action in self._COUNTER_ACTIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001214 for counter_name in self._COUNTER_NAMES
1215 ]
1216 self._assert_command_output_matches(
1217 "update", 0, facts, self._STATS_MODELS
1218 )
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +02001219
1220 def test_reset_command_deletion_of_instances(self):
1221 """Test the deletion of stats model instances with the reset command.
1222
1223 This test validates that model instances get deleted when the
1224 reset command is called on a database that only contains a single
1225 model instance for each class.
1226 """
1227 # Create dummy version instances
1228 version = Dummy.create_dummy_version()
1229 radio_version = Dummy.create_dummy_radio_version()
1230 Dummy.create_dummy_daily_version(version)
1231 Dummy.create_dummy_daily_radio_version(radio_version)
1232 Dummy.create_dummy_stats_metadata()
1233
1234 # We expect that the model instances get deleted
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001235 self._assert_command_output_matches(
1236 "reset", 1, ["deleted"], self._ALL_MODELS
1237 )
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001238
1239
1240class DeviceStatsTestCase(_HiccupAPITestCase):
1241 """Test the single device stats REST endpoints."""
1242
1243 def _get_with_params(self, url, params):
1244 url = reverse(url, kwargs=params)
1245 return self.fp_staff_client.get(url)
1246
1247 def _assert_device_stats_response_is(
1248 self,
1249 response,
1250 uuid,
1251 board_date,
1252 num_heartbeats,
1253 num_crashreports,
1254 num_smpls,
1255 crashes_per_day,
1256 smpl_per_day,
1257 last_active,
1258 ):
1259 # pylint: disable=too-many-arguments
1260 self.assertEqual(response.status_code, status.HTTP_200_OK)
1261
1262 self.assertIn("uuid", response.data)
1263 self.assertIn("board_date", response.data)
1264 self.assertIn("heartbeats", response.data)
1265 self.assertIn("crashreports", response.data)
1266 self.assertIn("smpls", response.data)
1267 self.assertIn("crashes_per_day", response.data)
1268 self.assertIn("smpl_per_day", response.data)
1269 self.assertIn("last_active", response.data)
1270
1271 self.assertEqual(response.data["uuid"], uuid)
1272 self.assertEqual(response.data["board_date"], board_date)
1273 self.assertEqual(response.data["heartbeats"], num_heartbeats)
1274 self.assertEqual(response.data["crashreports"], num_crashreports)
1275 self.assertEqual(response.data["smpls"], num_smpls)
1276 self.assertEqual(response.data["crashes_per_day"], crashes_per_day)
1277 self.assertEqual(response.data["smpl_per_day"], smpl_per_day)
1278 self.assertEqual(response.data["last_active"], last_active)
1279
1280 @unittest.skip(
1281 "Fails because there is no fallback for the last_active "
1282 "date for devices without heartbeats."
1283 )
1284 def test_get_device_stats_no_reports(self):
1285 """Test getting device stats for a device without reports."""
1286 # Create a device
1287 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1288
1289 # Get the device statistics
1290 response = self._get_with_params(
1291 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1292 )
1293
1294 # Assert that the statistics match
1295 self._assert_device_stats_response_is(
1296 response=response,
1297 uuid=str(device.uuid),
1298 board_date=device.board_date,
1299 num_heartbeats=0,
1300 num_crashreports=0,
1301 num_smpls=0,
1302 crashes_per_day=0.0,
1303 smpl_per_day=0.0,
1304 last_active=device.board_date,
1305 )
1306
1307 def test_get_device_stats_no_crash_reports(self):
1308 """Test getting device stats for a device without crashreports."""
1309 # Create a device and a heartbeat
1310 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1311 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1312
1313 # Get the device statistics
1314 response = self._get_with_params(
1315 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1316 )
1317
1318 # Assert that the statistics match
1319 self._assert_device_stats_response_is(
1320 response=response,
1321 uuid=str(device.uuid),
1322 board_date=device.board_date,
1323 num_heartbeats=1,
1324 num_crashreports=0,
1325 num_smpls=0,
1326 crashes_per_day=0.0,
1327 smpl_per_day=0.0,
1328 last_active=heartbeat.date,
1329 )
1330
1331 @unittest.skip(
1332 "Fails because there is no fallback for the last_active "
1333 "date for devices without heartbeats."
1334 )
1335 def test_get_device_stats_no_heartbeats(self):
1336 """Test getting device stats for a device without heartbeats."""
1337 # Create a device and crashreport
1338 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1339 Dummy.create_dummy_report(Crashreport, device)
1340
1341 # Get the device statistics
1342 response = self._get_with_params(
1343 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1344 )
1345
1346 # Assert that the statistics match
1347 self._assert_device_stats_response_is(
1348 response=response,
1349 uuid=str(device.uuid),
1350 board_date=device.board_date,
1351 num_heartbeats=0,
1352 num_crashreports=1,
1353 num_smpls=0,
1354 crashes_per_day=0.0,
1355 smpl_per_day=0.0,
1356 last_active=device.board_date,
1357 )
1358
1359 def test_get_device_stats(self):
1360 """Test getting device stats for a device."""
1361 # Create a device with a heartbeat and one report of each type
1362 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1363 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1364 for boot_reason in (
1365 Crashreport.SMPL_BOOT_REASONS
1366 + Crashreport.CRASH_BOOT_REASONS
1367 + ["other boot reason"]
1368 ):
1369 Dummy.create_dummy_report(
1370 Crashreport, device, boot_reason=boot_reason
1371 )
1372
1373 # Get the device statistics
1374 response = self._get_with_params(
1375 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1376 )
1377
1378 # Assert that the statistics match
1379 self._assert_device_stats_response_is(
1380 response=response,
1381 uuid=str(device.uuid),
1382 board_date=device.board_date,
1383 num_heartbeats=1,
1384 num_crashreports=len(Crashreport.CRASH_BOOT_REASONS),
1385 num_smpls=len(Crashreport.SMPL_BOOT_REASONS),
1386 crashes_per_day=len(Crashreport.CRASH_BOOT_REASONS),
1387 smpl_per_day=len(Crashreport.SMPL_BOOT_REASONS),
1388 last_active=heartbeat.date,
1389 )
1390
1391 def test_get_device_stats_multiple_days(self):
1392 """Test getting device stats for a device that sent more reports."""
1393 # Create a device with some heartbeats and reports over time
1394 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1395 num_days = 100
1396 for i in range(num_days):
1397 report_day = datetime.now(tz=pytz.utc) + timedelta(days=i)
1398 heartbeat = Dummy.create_dummy_report(
1399 HeartBeat, device, date=report_day
1400 )
1401 Dummy.create_dummy_report(Crashreport, device, date=report_day)
1402 Dummy.create_dummy_report(
1403 Crashreport,
1404 device,
1405 date=report_day,
1406 boot_reason=Crashreport.SMPL_BOOT_REASONS[0],
1407 )
1408
1409 # Get the device statistics
1410 response = self._get_with_params(
1411 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1412 )
1413
1414 # Assert that the statistics match
1415 self._assert_device_stats_response_is(
1416 response=response,
1417 uuid=str(device.uuid),
1418 board_date=device.board_date,
1419 num_heartbeats=num_days,
1420 num_crashreports=num_days,
1421 num_smpls=num_days,
1422 crashes_per_day=1,
1423 smpl_per_day=1,
1424 last_active=heartbeat.date,
1425 )
1426
1427 def test_get_device_stats_multiple_days_missing_heartbeat(self):
1428 """Test getting device stats for a device with missing heartbeat."""
1429 # Create a device with some heartbeats and reports over time
1430 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1431 num_days = 100
1432 skip_day = round(num_days / 2)
1433 for i in range(num_days):
1434 report_day = datetime.now(tz=pytz.utc) + timedelta(days=i)
1435 # Skip creation of heartbeat at one day
1436 if i != skip_day:
1437 heartbeat = Dummy.create_dummy_report(
1438 HeartBeat, device, date=report_day
1439 )
1440 Dummy.create_dummy_report(Crashreport, device, date=report_day)
1441
1442 # Get the device statistics
1443 response = self._get_with_params(
1444 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1445 )
1446
1447 # Assert that the statistics match
1448 self._assert_device_stats_response_is(
1449 response=response,
1450 uuid=str(device.uuid),
1451 board_date=device.board_date,
1452 num_heartbeats=num_days - 1,
1453 num_crashreports=num_days,
1454 num_smpls=0,
1455 crashes_per_day=num_days / (num_days - 1),
1456 smpl_per_day=0,
1457 last_active=heartbeat.date,
1458 )
1459
1460 @unittest.skip("Duplicate heartbeats are currently not dropped.")
1461 def test_get_device_stats_multiple_days_duplicate_heartbeat(self):
1462 """Test getting device stats for a device with duplicate heartbeat.
1463
1464 Duplicate heartbeats are dropped and thus should not influence the
1465 statistics.
1466 """
1467 # Create a device with some heartbeats and reports over time
1468 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1469 num_days = 100
1470 duplicate_day = round(num_days / 2)
1471 first_report_day = Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES["date"]
1472 for i in range(num_days):
1473 report_day = first_report_day + timedelta(days=i)
1474 heartbeat = Dummy.create_dummy_report(
1475 HeartBeat, device, date=report_day
1476 )
1477 # Create a second at the duplicate day (with 1 hour delay)
1478 if i == duplicate_day:
1479 Dummy.create_dummy_report(
1480 HeartBeat, device, date=report_day + timedelta(hours=1)
1481 )
1482 Dummy.create_dummy_report(Crashreport, device, date=report_day)
1483
1484 # Get the device statistics
1485 response = self._get_with_params(
1486 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1487 )
1488
1489 # Assert that the statistics match
1490 self._assert_device_stats_response_is(
1491 response=response,
1492 uuid=str(device.uuid),
1493 board_date=device.board_date,
1494 num_heartbeats=num_days,
1495 num_crashreports=num_days,
1496 num_smpls=0,
1497 crashes_per_day=1,
1498 smpl_per_day=0,
1499 last_active=heartbeat.date,
1500 )
1501
1502 def test_get_device_report_history_no_reports(self):
1503 """Test getting report history stats for a device without reports."""
1504 # Create a device
1505 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1506
1507 # Get the device report history statistics
1508 response = self._get_with_params(
1509 "hiccup_stats_api_v1_device_report_history", {"uuid": device.uuid}
1510 )
1511
1512 # Assert that the report history is empty
1513 self.assertEqual([], response.data)
1514
1515 @unittest.skip("Broken raw query. Heartbeats are not counted correctly.")
1516 def test_get_device_report_history(self):
1517 """Test getting report history stats for a device."""
1518 # Create a device with a heartbeat and one report of each type
1519 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1520 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1521 for boot_reason in (
1522 Crashreport.SMPL_BOOT_REASONS
1523 + Crashreport.CRASH_BOOT_REASONS
1524 + ["other boot reason"]
1525 ):
1526 Dummy.create_dummy_report(
1527 Crashreport, device, boot_reason=boot_reason
1528 )
1529
1530 # Get the device report history statistics
1531 response = self._get_with_params(
1532 "hiccup_stats_api_v1_device_report_history", {"uuid": device.uuid}
1533 )
1534
1535 # Assert that the statistics match
1536 report_history = [
1537 {
1538 "date": heartbeat.date.date(),
1539 "heartbeats": 1,
1540 "smpl": len(Crashreport.SMPL_BOOT_REASONS),
1541 "prob_crashes": len(Crashreport.CRASH_BOOT_REASONS),
1542 "other": 1,
1543 }
1544 ]
1545 self.assertEqual(report_history, response.data)
1546
1547 def test_get_device_update_history_no_reports(self):
1548 """Test getting update history stats for a device without reports."""
1549 # Create a device
1550 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1551
1552 # Get the device report history statistics
1553 response = self._get_with_params(
1554 "hiccup_stats_api_v1_device_update_history", {"uuid": device.uuid}
1555 )
1556
1557 # Assert that the update history is empty
1558 self.assertEqual([], response.data)
1559
1560 def test_get_device_update_history(self):
1561 """Test getting update history stats for a device."""
1562 # Create a device with a heartbeat and one report of each type
1563 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1564 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1565 for boot_reason in (
1566 Crashreport.SMPL_BOOT_REASONS
1567 + Crashreport.CRASH_BOOT_REASONS
1568 + ["other boot reason"]
1569 ):
1570 params = {"boot_reason": boot_reason}
1571 Dummy.create_dummy_report(Crashreport, device, **params)
1572
1573 # Get the device update history statistics
1574 response = self._get_with_params(
1575 "hiccup_stats_api_v1_device_update_history", {"uuid": device.uuid}
1576 )
1577
1578 # Assert that the statistics match
1579 update_history = [
1580 {
1581 "build_fingerprint": heartbeat.build_fingerprint,
1582 "heartbeats": 1,
1583 "max": device.id,
1584 "other": 1,
1585 "prob_crashes": len(Crashreport.CRASH_BOOT_REASONS),
1586 "smpl": len(Crashreport.SMPL_BOOT_REASONS),
1587 "update_date": heartbeat.date,
1588 }
1589 ]
1590 self.assertEqual(update_history, response.data)
1591
1592 def test_get_device_update_history_multiple_updates(self):
1593 """Test getting update history stats with multiple updates."""
1594 # Create a device with a heartbeats and crashreport for each build
1595 # fingerprint in the dummy values
1596 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1597 expected_update_history = []
1598 for i, build_fingerprint in enumerate(Dummy.BUILD_FINGERPRINTS):
1599 report_day = datetime.now(tz=pytz.utc) + timedelta(days=i)
1600 Dummy.create_dummy_report(
1601 HeartBeat,
1602 device,
1603 date=report_day,
1604 build_fingerprint=build_fingerprint,
1605 )
1606 Dummy.create_dummy_report(
1607 Crashreport,
1608 device,
1609 date=report_day,
1610 build_fingerprint=build_fingerprint,
1611 )
1612
1613 # Create the expected update history object
1614 expected_update_history.append(
1615 {
1616 "update_date": report_day,
1617 "build_fingerprint": build_fingerprint,
1618 "max": device.id,
1619 "prob_crashes": 1,
1620 "smpl": 0,
1621 "other": 0,
1622 "heartbeats": 1,
1623 }
1624 )
1625 # Sort the expected values by build fingerprint
1626 expected_update_history.sort(
1627 key=operator.itemgetter("build_fingerprint")
1628 )
1629
1630 # Get the device update history statistics and sort it
1631 response = self._get_with_params(
1632 "hiccup_stats_api_v1_device_update_history", {"uuid": device.uuid}
1633 )
1634 response.data.sort(key=operator.itemgetter("build_fingerprint"))
1635
1636 # Assert that the statistics match
1637 self.assertEqual(expected_update_history, response.data)
1638
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001639 def test_download_non_existing_logfile(self):
1640 """Test download of a non existing log file."""
1641 # Try to get a log file
1642 response = self._get_with_params(
1643 "hiccup_stats_api_v1_logfile_download", {"id_logfile": 0}
1644 )
1645
1646 # Assert that the log file was not found
1647 self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
1648
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001649 def test_download_logfile(self):
1650 """Test download of log files."""
1651 # Create a device with a crash report along with log file
1652 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1653 crashreport = Dummy.create_dummy_report(Crashreport, device)
1654 logfile = Dummy.create_dummy_log_file(crashreport)
1655
1656 # Get the log file
1657 response = self._get_with_params(
1658 "hiccup_stats_api_v1_logfile_download", {"id_logfile": logfile.id}
1659 )
1660
1661 # Assert that the log file contents are in the response data
1662 self.assertEqual(response.status_code, status.HTTP_200_OK)
1663 self.assertIn(Dummy.DEFAULT_DUMMY_LOG_FILE_NAME, response.data)
1664 expected_logfile_content = Dummy.read_logfile_contents(
1665 logfile.logfile.path, Dummy.DEFAULT_DUMMY_LOG_FILE_NAME
1666 )
1667 self.assertEqual(
1668 response.data[Dummy.DEFAULT_DUMMY_LOG_FILE_NAME],
1669 expected_logfile_content,
1670 )