blob: e99674ba43212937e5eec7e1a9662ac38c0174f7 [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 Nikolausded30ae2018-09-14 15:40:08 +020069 "is_beta_release": False,
70 "is_official_release": True,
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020071 }
72
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020073 DEFAULT_DUMMY_VERSION_DAILY_VALUES = {"date": DATES[1]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020074
75 DEFAULT_DUMMY_RADIO_VERSION_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020076 "radio_version": RADIO_VERSIONS[0],
77 "first_seen_on": DATES[1],
78 "released_on": DATES[0],
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020079 }
80
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020081 DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES = {"date": DATES[1]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020082
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +020083 DEFAULT_DUMMY_STATSMETADATA_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020084 "updated_at": datetime(2018, 6, 15, 2, 12, 24, tzinfo=pytz.utc)
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +020085 }
86
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020087 DEFAULT_DUMMY_DEVICE_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020088 "board_date": datetime(2015, 12, 15, 1, 23, 45, tzinfo=pytz.utc),
89 "chipset": "Qualcomm MSM8974PRO-AA",
90 "token": "64111c62d521fb4724454ca6dea27e18f93ef56e",
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020091 }
92
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +020093 DEFAULT_DUMMY_USER_VALUES = {"username": USERNAMES[0]}
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +020094
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +040095 DEFAULT_DUMMY_HEARTBEAT_VALUES = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +020096 "app_version": 10100,
97 "uptime": (
98 "up time: 16 days, 21:49:56, idle time: 5 days, 20:55:04, "
99 "sleep time: 10 days, 20:46:27"
100 ),
101 "build_fingerprint": BUILD_FINGERPRINTS[0],
102 "radio_version": RADIO_VERSIONS[0],
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200103 "date": datetime(2018, 3, 19, 12, 0, 0, tzinfo=pytz.utc),
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400104 }
105
106 DEFAULT_DUMMY_CRASHREPORT_VALUES = DEFAULT_DUMMY_HEARTBEAT_VALUES.copy()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200107 DEFAULT_DUMMY_CRASHREPORT_VALUES.update(
108 {
109 "is_fake_report": 0,
110 "boot_reason": Crashreport.BOOT_REASON_UNKOWN,
111 "power_on_reason": "it was powered on",
112 "power_off_reason": "something happened and it went off",
113 }
114 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400115
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200116 DEFAULT_DUMMY_LOG_FILE_VALUES = {
117 "logfile_type": "last_kmsg",
118 "logfile": os.path.join("resources", "test", "test_logfile.zip"),
119 }
120
121 DEFAULT_DUMMY_LOG_FILE_NAME = "dmesg.log"
122
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200123 @staticmethod
124 def update_copy(original, update):
125 """Merge fields of update into a copy of original."""
126 data = original.copy()
127 data.update(update)
128 return data
129
130 @staticmethod
131 def create_dummy_user(**kwargs):
132 """Create a dummy user instance.
133
134 The dummy instance is created and saved to the database.
135 Args:
136 **kwargs:
137 Optional arguments to extend/overwrite the default values.
138
139 Returns: The created user instance.
140
141 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200142 entity = User(
143 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_USER_VALUES, kwargs)
144 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200145 entity.save()
146 return entity
147
148 @staticmethod
149 def create_dummy_device(user, **kwargs):
150 """Create a dummy device instance.
151
152 The dummy instance is created and saved to the database.
153 Args:
154 user: The user instance that the device should relate to
155 **kwargs:
156 Optional arguments to extend/overwrite the default values.
157
158 Returns: The created device instance.
159
160 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200161 entity = Device(
162 user=user,
163 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_DEVICE_VALUES, kwargs)
164 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200165 entity.save()
166 return entity
167
168 @staticmethod
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400169 def create_dummy_report(report_type, device, **kwargs):
170 """Create a dummy report instance of the given report class type.
171
172 The dummy instance is created and saved to the database.
173 Args:
174 report_type: The class of the report type to be created.
175 user: The device instance that the heartbeat should relate to
176 **kwargs:
177 Optional arguments to extend/overwrite the default values.
178
179 Returns: The created report instance.
180
181 """
182 if report_type == HeartBeat:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200183 entity = HeartBeat(
184 device=device,
185 **Dummy.update_copy(
186 Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs
187 )
188 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400189 elif report_type == Crashreport:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200190 entity = Crashreport(
191 device=device,
192 **Dummy.update_copy(
193 Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES, kwargs
194 )
195 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400196 else:
197 raise RuntimeError(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200198 "No dummy report instance can be created for {}".format(
199 report_type.__name__
200 )
201 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400202 entity.save()
203 return entity
204
205 @staticmethod
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200206 def create_dummy_log_file(crashreport, **kwargs):
207 """Create a dummy log file instance.
208
209 The dummy instance is created and saved to the database.
210
211 Args:
212 crashreport: The crashreport that the log file belongs to.
213 **kwargs: Optional arguments to extend/overwrite the default values.
214
215 Returns: The created log file instance.
216
217 """
218 entity = LogFile(
219 crashreport=crashreport,
220 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_LOG_FILE_VALUES, kwargs)
221 )
222
223 entity.save()
224 return entity
225
226 @staticmethod
227 def read_logfile_contents(path_to_zipfile, logfile_name):
228 """Read bytes of a zipped logfile."""
229 archive = zipfile.ZipFile(path_to_zipfile, "r")
230 return archive.read(logfile_name)
231
232 @staticmethod
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200233 def create_dummy_version(**kwargs):
234 """Create a dummy version instance.
235
236 The dummy instance is created and saved to the database.
237 Args:
238 **kwargs:
239 Optional arguments to extend/overwrite the default values.
240
241 Returns: The created version instance.
242
243 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200244 entity = Version(
245 **Dummy.update_copy(Dummy.DEFAULT_DUMMY_VERSION_VALUES, kwargs)
246 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200247 entity.save()
248 return entity
249
250 @staticmethod
251 def create_dummy_radio_version(**kwargs):
252 """Create a dummy 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 radio version instance.
260
261 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200262 entity = RadioVersion(
263 **Dummy.update_copy(
264 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs
265 )
266 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200267 entity.save()
268 return entity
269
270 @staticmethod
271 def create_dummy_daily_version(version, **kwargs):
272 """Create a dummy daily version instance.
273
274 The dummy instance is created and saved to the database.
275 Args:
276 **kwargs:
277 Optional arguments to extend/overwrite the default values.
278
279 Returns: The created daily version instance.
280
281 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200282 entity = VersionDaily(
283 version=version,
284 **Dummy.update_copy(
285 Dummy.DEFAULT_DUMMY_VERSION_DAILY_VALUES, kwargs
286 )
287 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200288 entity.save()
289 return entity
290
291 @staticmethod
292 def create_dummy_daily_radio_version(version, **kwargs):
293 """Create a dummy daily radio version instance.
294
295 The dummy instance is created and saved to the database.
296 Args:
297 **kwargs:
298 Optional arguments to extend/overwrite the default values.
299
300 Returns: The created daily radio version instance.
301
302 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200303 entity = RadioVersionDaily(
304 version=version,
305 **Dummy.update_copy(
306 Dummy.DEFAULT_DUMMY_RADIO_VERSION_DAILY_VALUES, kwargs
307 )
308 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200309 entity.save()
310 return entity
311
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200312 @staticmethod
313 def create_dummy_stats_metadata(**kwargs):
314 """Create a dummy stats metadata instance.
315
316 The dummy instance is created and saved to the database.
317 Args:
318 **kwargs:
319 Optional arguments to extend/overwrite the default values.
320
321 Returns: The created stats metadata instance.
322
323 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200324 entity = StatsMetadata(
325 **Dummy.update_copy(
326 Dummy.DEFAULT_DUMMY_STATSMETADATA_VALUES, kwargs
327 )
328 )
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +0200329 entity.save()
330 return entity
331
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200332
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200333class _HiccupAPITestCase(APITestCase):
334 """Abstract class for Hiccup REST API test cases to inherit from."""
335
336 @classmethod
337 def setUpTestData(cls): # noqa: N802
338 """Create an admin and client user for accessing the API.
339
340 The APIClient that can be used to make authenticated requests as
341 admin user is stored in self.admin. Another client (which is
342 related to a user that is part of the Fairphone software team group)
343 is stored in self.fp_staff_client.
344 """
345 admin_user = User.objects.create_superuser(
346 "somebody", "somebody@example.com", "thepassword"
347 )
348 cls.admin = APIClient()
349 cls.admin.force_authenticate(admin_user)
350
351 fp_software_team_group = Group(name=FP_STAFF_GROUP_NAME)
352 fp_software_team_group.save()
353 fp_software_team_user = User.objects.create_user(
354 "fp_staff", "somebody@fairphone.com", "thepassword"
355 )
356 fp_software_team_user.groups.add(fp_software_team_group)
357 cls.fp_staff_client = APIClient()
358 cls.fp_staff_client.login(username="fp_staff", password="thepassword")
359
360
Mitja Nikolaus4d19e182018-09-05 13:45:36 +0200361class StatusTestCase(_HiccupAPITestCase):
362 """Test the status endpoint."""
363
364 status_url = reverse("hiccup_stats_api_v1_status")
365
366 def _assert_status_response_is(
367 self, response, num_devices, num_crashreports, num_heartbeats
368 ):
369 self.assertEqual(response.status_code, status.HTTP_200_OK)
370 self.assertIn("devices", response.data)
371 self.assertIn("crashreports", response.data)
372 self.assertIn("heartbeats", response.data)
373 self.assertEqual(response.data["devices"], num_devices)
374 self.assertEqual(response.data["crashreports"], num_crashreports)
375 self.assertEqual(response.data["heartbeats"], num_heartbeats)
376
377 def test_get_status_empty_database(self):
378 """Get the status when the database is empty."""
379 response = self.fp_staff_client.get(self.status_url)
380 self._assert_status_response_is(response, 0, 0, 0)
381
382 def test_get_status(self):
383 """Get the status after some reports have been created."""
384 # Create a device with a heartbeat and a crash report
385 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
386 Dummy.create_dummy_report(HeartBeat, device)
387 Dummy.create_dummy_report(Crashreport, device)
388
389 # Create a second device without any reports
390 Dummy.create_dummy_device(
391 Dummy.create_dummy_user(username=Dummy.USERNAMES[1])
392 )
393
394 # Assert that the status includes the appropriate numbers
395 response = self.fp_staff_client.get(self.status_url)
396 self._assert_status_response_is(
397 response, num_devices=2, num_crashreports=1, num_heartbeats=1
398 )
399
400
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200401class _VersionTestCase(_HiccupAPITestCase):
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200402 """Abstract class for version-related test cases to inherit from."""
403
404 # The attribute name characterising the unicity of a stats entry (the
405 # named identifier)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200406 unique_entry_name = "build_fingerprint"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200407 # The collection of unique entries to post
408 unique_entries = Dummy.BUILD_FINGERPRINTS
409 # The URL to retrieve the stats entries from
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200410 endpoint_url = reverse("hiccup_stats_api_v1_versions")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200411
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200412 @staticmethod
413 def _create_dummy_version(**kwargs):
414 return Dummy.create_dummy_version(**kwargs)
415
416 def _get_with_params(self, url, params):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200417 return self.admin.get("{}?{}".format(url, urlencode(params)))
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200418
419 def _assert_result_length_is(self, response, count):
420 self.assertEqual(response.status_code, status.HTTP_200_OK)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200421 self.assertIn("results", response.data)
422 self.assertIn("count", response.data)
423 self.assertEqual(response.data["count"], count)
424 self.assertEqual(len(response.data["results"]), count)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200425
426 def _assert_device_owner_has_no_get_access(self, entries_url):
427 # Create a user and device
428 user = Dummy.create_dummy_user()
429 device = Dummy.create_dummy_device(user=user)
430
431 # Create authenticated client
432 user = APIClient()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200433 user.credentials(HTTP_AUTHORIZATION="Token " + device.token)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200434
435 # Try getting entries using the client
436 response = user.get(entries_url)
437 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
438
439 def _assert_filter_result_matches(self, filter_params, expected_result):
440 # List entities with filter
441 response = self._get_with_params(self.endpoint_url, filter_params)
442
443 # Expect only the single matching result to be returned
444 self._assert_result_length_is(response, 1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200445 self.assertEqual(
446 response.data["results"][0][self.unique_entry_name],
447 getattr(expected_result, self.unique_entry_name),
448 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200449
450
451class VersionTestCase(_VersionTestCase):
452 """Test the Version and REST endpoint."""
453
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200454 # pylint: disable=too-many-ancestors
455
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200456 def _create_version_entities(self):
457 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200458 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200459 for unique_entry in self.unique_entries
460 ]
461 return versions
462
463 def test_list_versions_without_authentication(self):
464 """Test listing of versions without authentication."""
465 response = self.client.get(self.endpoint_url)
466 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
467
468 def test_list_versions_as_device_owner(self):
469 """Test listing of versions as device owner."""
470 self._assert_device_owner_has_no_get_access(self.endpoint_url)
471
472 def test_list_versions_empty_database(self):
473 """Test listing of versions on an empty database."""
474 response = self.admin.get(self.endpoint_url)
475 self._assert_result_length_is(response, 0)
476
477 def test_list_versions(self):
478 """Test listing versions."""
479 versions = self._create_version_entities()
480 response = self.admin.get(self.endpoint_url)
481 self._assert_result_length_is(response, len(versions))
482
483 def test_filter_versions_by_unique_entry_name(self):
484 """Test filtering versions by their unique entry name."""
485 versions = self._create_version_entities()
486 response = self.admin.get(self.endpoint_url)
487
488 # Listing all entities should return the correct result length
489 self._assert_result_length_is(response, len(versions))
490
491 # List entities with filter
492 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200493 self.unique_entry_name: getattr(versions[0], self.unique_entry_name)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200494 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200495 self._assert_filter_result_matches(
496 filter_params, expected_result=versions[0]
497 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200498
499 def test_filter_versions_by_release_type(self):
500 """Test filtering versions by release type."""
501 # Create versions for all combinations of release types
502 versions = []
503 i = 0
504 for is_official_release in True, False:
505 for is_beta_release in True, False:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200506 versions.append(
507 self._create_dummy_version(
508 **{
509 "is_official_release": is_official_release,
510 "is_beta_release": is_beta_release,
511 self.unique_entry_name: self.unique_entries[i],
512 }
513 )
514 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200515 i += 1
516
517 # # Listing all entities should return the correct result length
518 response = self.admin.get(self.endpoint_url)
519 self._assert_result_length_is(response, len(versions))
520
521 # List each of the entities with the matching filter params
522 for version in versions:
523 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200524 "is_official_release": version.is_official_release,
525 "is_beta_release": version.is_beta_release,
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200526 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200527 self._assert_filter_result_matches(
528 filter_params, expected_result=version
529 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200530
531 def test_filter_versions_by_first_seen_date(self):
532 """Test filtering versions by first seen date."""
533 versions = self._create_version_entities()
534
535 # Set the first seen date of an entity
536 versions[0].first_seen_on = Dummy.DATES[2]
537 versions[0].save()
538
539 # Listing all entities should return the correct result length
540 response = self.admin.get(self.endpoint_url)
541 self._assert_result_length_is(response, len(versions))
542
543 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200544 filter_params = {"first_seen_after": Dummy.DATES[2]}
545 self._assert_filter_result_matches(
546 filter_params, expected_result=versions[0]
547 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200548
549
550# pylint: disable=too-many-ancestors
551class RadioVersionTestCase(VersionTestCase):
552 """Test the RadioVersion REST endpoint."""
553
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200554 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200555 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200556 endpoint_url = reverse("hiccup_stats_api_v1_radio_versions")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200557
558 @staticmethod
559 def _create_dummy_version(**kwargs):
560 return Dummy.create_dummy_radio_version(**kwargs)
561
562
563class VersionDailyTestCase(_VersionTestCase):
564 """Test the VersionDaily REST endpoint."""
565
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200566 endpoint_url = reverse("hiccup_stats_api_v1_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200567
568 @staticmethod
569 def _create_dummy_daily_version(version, **kwargs):
570 return Dummy.create_dummy_daily_version(version, **kwargs)
571
572 def _create_version_entities(self):
573 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200574 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200575 for unique_entry in self.unique_entries
576 ]
577 versions_daily = [
578 self._create_dummy_daily_version(version=version)
579 for version in versions
580 ]
581 return versions_daily
582
583 def test_list_daily_versions_without_authentication(self):
584 """Test listing of daily versions without authentication."""
585 response = self.client.get(self.endpoint_url)
586 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
587
588 def test_list_daily_versions_as_device_owner(self):
589 """Test listing of daily versions as device owner."""
590 self._assert_device_owner_has_no_get_access(self.endpoint_url)
591
592 def test_list_daily_versions_empty_database(self):
593 """Test listing of daily versions on an empty database."""
594 response = self.admin.get(self.endpoint_url)
595 self._assert_result_length_is(response, 0)
596
597 def test_list_daily_versions(self):
598 """Test listing daily versions."""
599 versions_daily = self._create_version_entities()
600 response = self.admin.get(self.endpoint_url)
601 self._assert_result_length_is(response, len(versions_daily))
602
603 def test_filter_daily_versions_by_version(self):
604 """Test filtering versions by the version they relate to."""
605 # Create VersionDaily entities
606 versions = self._create_version_entities()
607
608 # Listing all entities should return the correct result length
609 response = self.admin.get(self.endpoint_url)
610 self._assert_result_length_is(response, len(versions))
611
612 # List entities with filter
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200613 param_name = "version__" + self.unique_entry_name
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200614 filter_params = {
615 param_name: getattr(versions[0].version, self.unique_entry_name)
616 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200617 self._assert_filter_result_matches(
618 filter_params, expected_result=versions[0].version
619 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200620
621 def test_filter_daily_versions_by_date(self):
622 """Test filtering daily versions by date."""
623 # Create Version and VersionDaily entities
624 versions = self._create_version_entities()
625
626 # Update the date
627 versions[0].date = Dummy.DATES[2]
628 versions[0].save()
629
630 # Listing all entities should return the correct result length
631 response = self.admin.get(self.endpoint_url)
632 self._assert_result_length_is(response, len(versions))
633
634 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200635 filter_params = {"date": versions[0].date}
636 self._assert_filter_result_matches(
637 filter_params, expected_result=versions[0].version
638 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200639
640
641class RadioVersionDailyTestCase(VersionDailyTestCase):
642 """Test the RadioVersionDaily REST endpoint."""
643
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200644 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200645 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200646 endpoint_url = reverse("hiccup_stats_api_v1_radio_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200647
648 @staticmethod
649 def _create_dummy_version(**kwargs):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200650 entity = RadioVersion(
651 **Dummy.update_copy(
652 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs
653 )
654 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200655 entity.save()
656 return entity
657
658 @staticmethod
659 def _create_dummy_daily_version(version, **kwargs):
660 return Dummy.create_dummy_daily_radio_version(version, **kwargs)
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400661
662
663class StatsCommandVersionsTestCase(TestCase):
664 """Test the generation of Version stats with the stats command."""
665
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400666 # The class of the version type to be tested
667 version_class = Version
668 # The attribute name characterising the unicity of a stats entry (the
669 # named identifier)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200670 unique_entry_name = "build_fingerprint"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400671 # The collection of unique entries to post
672 unique_entries = Dummy.BUILD_FINGERPRINTS
673
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200674 def _create_reports(
675 self, report_type, unique_entry_name, device, number, **kwargs
676 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400677 # Create reports with distinct timestamps
678 now = datetime.now(pytz.utc)
679 for i in range(number):
680 report_date = now - timedelta(milliseconds=i)
681 report_attributes = {
682 self.unique_entry_name: unique_entry_name,
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200683 "device": device,
684 "date": report_date,
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400685 }
686 report_attributes.update(**kwargs)
687 Dummy.create_dummy_report(report_type, **report_attributes)
688
689 def test_stats_calculation(self):
690 """Test generation of a Version instance."""
691 user = Dummy.create_dummy_user()
692 device = Dummy.create_dummy_device(user=user)
693 heartbeat = Dummy.create_dummy_report(HeartBeat, device=device)
694
695 # Expect that we do not have the Version before updating the stats
696 get_params = {
697 self.unique_entry_name: getattr(heartbeat, self.unique_entry_name)
698 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200699 self.assertRaises(
700 self.version_class.DoesNotExist,
701 self.version_class.objects.get,
702 **get_params
703 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400704
705 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200706 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400707
708 # Assume that a corresponding Version instance has been created
709 version = self.version_class.objects.get(**get_params)
710 self.assertIsNotNone(version)
711
712 def _assert_older_report_updates_version_date(self, report_type):
713 """Validate that older reports sent later affect the version date."""
714 user = Dummy.create_dummy_user()
715 device = Dummy.create_dummy_device(user=user)
716 report = Dummy.create_dummy_report(report_type, device=device)
717
718 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200719 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400720
721 get_params = {
722 self.unique_entry_name: getattr(report, self.unique_entry_name)
723 }
724 version = self.version_class.objects.get(**get_params)
725
726 self.assertEqual(report.date.date(), version.first_seen_on)
727
728 # Create a new report from an earlier point in time
729 report_time_2 = report.date - timedelta(weeks=1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200730 Dummy.create_dummy_report(
731 report_type, device=device, date=report_time_2
732 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400733
734 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200735 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400736
737 # Get the same version object from before
738 version = self.version_class.objects.get(**get_params)
739
740 # Validate that the date matches the report recently sent
741 self.assertEqual(report_time_2.date(), version.first_seen_on)
742
743 def test_older_heartbeat_updates_version_date(self):
744 """Validate updating version date with older heartbeats."""
745 self._assert_older_report_updates_version_date(HeartBeat)
746
747 def test_older_crash_report_updates_version_date(self):
748 """Validate updating version date with older crash reports."""
749 self._assert_older_report_updates_version_date(Crashreport)
750
751 def test_entries_are_unique(self):
752 """Validate the entries' unicity and value."""
753 # Create some reports
754 user = Dummy.create_dummy_user()
755 device = Dummy.create_dummy_device(user=user)
756 for unique_entry in self.unique_entries:
757 self._create_reports(HeartBeat, unique_entry, device, 10)
758
759 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200760 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400761
762 # Check whether the correct amount of distinct versions have been
763 # created
764 versions = self.version_class.objects.all()
765 for version in versions:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200766 self.assertIn(
767 getattr(version, self.unique_entry_name), self.unique_entries
768 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400769 self.assertEqual(len(versions), len(self.unique_entries))
770
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200771 def _assert_counter_distribution_is_correct(
772 self, report_type, numbers, counter_attribute_name, **kwargs
773 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400774 """Validate a counter distribution in the database."""
775 if len(numbers) != len(self.unique_entries):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200776 raise ValueError(
777 "The length of the numbers list must match the "
778 "length of self.unique_entries in the test class"
779 "({} != {})".format(len(numbers), len(self.unique_entries))
780 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400781 # Create some reports
782 user = Dummy.create_dummy_user()
783 device = Dummy.create_dummy_device(user=user)
784 for unique_entry, num in zip(self.unique_entries, numbers):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200785 self._create_reports(
786 report_type, unique_entry, device, num, **kwargs
787 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400788
789 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200790 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400791
792 # Check whether the numbers of reports match
793 for version in self.version_class.objects.all():
794 unique_entry_name = getattr(version, self.unique_entry_name)
795 num = numbers[self.unique_entries.index(unique_entry_name)]
796 self.assertEqual(num, getattr(version, counter_attribute_name))
797
798 def test_heartbeats_counter(self):
799 """Test the calculation of the heartbeats counter."""
800 numbers = [10, 7, 8, 5]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200801 counter_attribute_name = "heartbeats"
802 self._assert_counter_distribution_is_correct(
803 HeartBeat, numbers, counter_attribute_name
804 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400805
806 def test_crash_reports_counter(self):
807 """Test the calculation of the crashreports counter."""
808 numbers = [2, 5, 0, 3]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200809 counter_attribute_name = "prob_crashes"
810 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_UNKOWN}
811 self._assert_counter_distribution_is_correct(
812 Crashreport, numbers, counter_attribute_name, **boot_reason_param
813 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400814
815 def test_smpl_reports_counter(self):
816 """Test the calculation of the smpl reports counter."""
817 numbers = [1, 3, 4, 0]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200818 counter_attribute_name = "smpl"
819 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_RTC_ALARM}
820 self._assert_counter_distribution_is_correct(
821 Crashreport, numbers, counter_attribute_name, **boot_reason_param
822 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400823
824 def test_other_reports_counter(self):
825 """Test the calculation of the other reports counter."""
826 numbers = [0, 2, 1, 2]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200827 counter_attribute_name = "other"
828 boot_reason_param = {"boot_reason": "random boot reason"}
829 self._assert_counter_distribution_is_correct(
830 Crashreport, numbers, counter_attribute_name, **boot_reason_param
831 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400832
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +0200833 def _assert_reports_with_same_timestamp_are_counted(
834 self, report_type, counter_attribute_name, **kwargs
835 ):
836 """Validate that reports with the same timestamp are counted.
837
838 Reports from different devices but the same timestamp should be
839 counted as independent reports.
840 """
841 # Create a report
842 device1 = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
843 report1 = Dummy.create_dummy_report(
844 report_type, device=device1, **kwargs
845 )
846
847 # Create a second report with the same timestamp but from another device
848 device2 = Dummy.create_dummy_device(
849 user=Dummy.create_dummy_user(username=Dummy.USERNAMES[1])
850 )
851 Dummy.create_dummy_report(
852 report_type, device=device2, date=report1.date, **kwargs
853 )
854
855 # Run the command to update the database
856 call_command("stats", "update")
857
858 # Get the corresponding version instance from the database
859 get_params = {
860 self.unique_entry_name: getattr(report1, self.unique_entry_name)
861 }
862 version = self.version_class.objects.get(**get_params)
863
864 # Assert that both reports are counted
865 self.assertEqual(getattr(version, counter_attribute_name), 2)
866
867 @unittest.skip(
868 "Duplicates are dropped based on their timestamp at the moment. This is"
869 "to be adapted so that they are dropped taking into account the device"
870 "UUID as well."
871 )
872 def test_heartbeats_with_same_timestamp_are_counted(self):
873 """Validate that heartbeats with same timestamp are counted."""
874 counter_attribute_name = "heartbeats"
875 self._assert_reports_with_same_timestamp_are_counted(
876 HeartBeat, counter_attribute_name
877 )
878
879 @unittest.skip(
880 "Duplicates are dropped based on their timestamp at the moment. This is"
881 "to be adapted so that they are dropped taking into account the device"
882 "UUID as well."
883 )
884 def test_crash_reports_with_same_timestamp_are_counted(self):
885 """Validate that crash report duplicates are ignored."""
886 counter_attribute_name = "prob_crashes"
887 for unique_entry, boot_reason in zip(
888 self.unique_entries, Crashreport.CRASH_BOOT_REASONS
889 ):
890 params = {
891 "boot_reason": boot_reason,
892 self.unique_entry_name: unique_entry,
893 }
894 self._assert_reports_with_same_timestamp_are_counted(
895 Crashreport, counter_attribute_name, **params
896 )
897
898 @unittest.skip(
899 "Duplicates are dropped based on their timestamp at the moment. This is"
900 "to be adapted so that they are dropped taking into account the device"
901 "UUID as well."
902 )
903 def test_smpl_reports_with_same_timestamp_are_counted(self):
904 """Validate that smpl report duplicates are ignored."""
905 counter_attribute_name = "smpl"
906 for unique_entry, boot_reason in zip(
907 self.unique_entries, Crashreport.SMPL_BOOT_REASONS
908 ):
909 params = {
910 "boot_reason": boot_reason,
911 self.unique_entry_name: unique_entry,
912 }
913 self._assert_reports_with_same_timestamp_are_counted(
914 Crashreport, counter_attribute_name, **params
915 )
916
917 @unittest.skip(
918 "Duplicates are dropped based on their timestamp at the moment. This is"
919 "to be adapted so that they are dropped taking into account the device"
920 "UUID as well."
921 )
922 def test_other_reports_with_same_timestamp_are_counted(self):
923 """Validate that other report duplicates are ignored."""
924 counter_attribute_name = "other"
925 params = {"boot_reason": "random boot reason"}
926 self._assert_reports_with_same_timestamp_are_counted(
927 Crashreport, counter_attribute_name, **params
928 )
929
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200930 def _assert_duplicates_are_ignored(
931 self, report_type, device, counter_attribute_name, **kwargs
932 ):
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400933 """Validate that reports with duplicate timestamps are ignored."""
934 # Create a report
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200935 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400936
937 # Create a second report with the same timestamp
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200938 Dummy.create_dummy_report(
939 report_type, device=device, date=report.date, **kwargs
940 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400941
942 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200943 call_command("stats", "update")
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400944
945 # Get the corresponding version instance from the database
946 get_params = {
947 self.unique_entry_name: getattr(report, self.unique_entry_name)
948 }
949 version = self.version_class.objects.get(**get_params)
950
951 # Assert that the report with the duplicate timestamp is not
952 # counted, i.e. only 1 report is counted.
953 self.assertEqual(getattr(version, counter_attribute_name), 1)
954
955 def test_heartbeat_duplicates_are_ignored(self):
956 """Validate that heartbeat duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200957 counter_attribute_name = "heartbeats"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400958 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200959 self._assert_duplicates_are_ignored(
960 HeartBeat, device, counter_attribute_name
961 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400962
963 def test_crash_report_duplicates_are_ignored(self):
964 """Validate that crash report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200965 counter_attribute_name = "prob_crashes"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400966 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
967 for i, boot_reason in enumerate(Crashreport.CRASH_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200968 params = {
969 "boot_reason": boot_reason,
970 self.unique_entry_name: self.unique_entries[i],
971 }
972 self._assert_duplicates_are_ignored(
973 Crashreport, device, counter_attribute_name, **params
974 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400975
976 def test_smpl_report_duplicates_are_ignored(self):
977 """Validate that smpl report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200978 counter_attribute_name = "smpl"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400979 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
980 for i, boot_reason in enumerate(Crashreport.SMPL_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200981 params = {
982 "boot_reason": boot_reason,
983 self.unique_entry_name: self.unique_entries[i],
984 }
985 self._assert_duplicates_are_ignored(
986 Crashreport, device, counter_attribute_name, **params
987 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400988
989 def test_other_report_duplicates_are_ignored(self):
990 """Validate that other report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200991 counter_attribute_name = "other"
992 params = {"boot_reason": "random boot reason"}
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400993 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200994 self._assert_duplicates_are_ignored(
995 Crashreport, device, counter_attribute_name, **params
996 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400997
Mitja Nikolaus52e44b82018-09-04 14:23:19 +0200998 def _assert_older_reports_update_released_on_date(
999 self, report_type, **kwargs
1000 ):
1001 """Test updating of the released_on date.
1002
1003 Validate that the released_on date is updated once an older report is
1004 sent.
1005 """
1006 # Create a report
1007 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
1008 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
1009
1010 # Run the command to update the database
1011 call_command("stats", "update")
1012
1013 # Get the corresponding version instance from the database
1014 version = self.version_class.objects.get(
1015 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1016 )
1017
1018 # Assert that the released_on date matches the first report date
1019 self.assertEqual(version.released_on, report.date.date())
1020
1021 # Create a second report with the a timestamp earlier in time
1022 report_2_date = report.date - timedelta(days=1)
1023 Dummy.create_dummy_report(
1024 report_type, device=device, date=report_2_date, **kwargs
1025 )
1026
1027 # Run the command to update the database
1028 call_command("stats", "update")
1029
1030 # Get the corresponding version instance from the database
1031 version = self.version_class.objects.get(
1032 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1033 )
1034
1035 # Assert that the released_on date matches the older report date
1036 self.assertEqual(version.released_on, report_2_date.date())
1037
1038 def _assert_newer_reports_do_not_update_released_on_date(
1039 self, report_type, **kwargs
1040 ):
1041 """Test updating of the released_on date.
1042
1043 Validate that the released_on date is not updated once a newer report is
1044 sent.
1045 """
1046 # Create a report
1047 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
1048 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
1049 report_1_date = report.date.date()
1050
1051 # Run the command to update the database
1052 call_command("stats", "update")
1053
1054 # Get the corresponding version instance from the database
1055 version = self.version_class.objects.get(
1056 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1057 )
1058
1059 # Assert that the released_on date matches the first report date
1060 self.assertEqual(version.released_on, report_1_date)
1061
1062 # Create a second report with the a timestamp later in time
1063 report_2_date = report.date + timedelta(days=1)
1064 Dummy.create_dummy_report(
1065 report_type, device=device, date=report_2_date, **kwargs
1066 )
1067
1068 # Run the command to update the database
1069 call_command("stats", "update")
1070
1071 # Get the corresponding version instance from the database
1072 version = self.version_class.objects.get(
1073 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1074 )
1075
1076 # Assert that the released_on date matches the older report date
1077 self.assertEqual(version.released_on, report_1_date)
1078
1079 def test_older_heartbeat_updates_released_on_date(self):
1080 """Validate that older heartbeats update the release date."""
1081 self._assert_older_reports_update_released_on_date(HeartBeat)
1082
1083 def test_older_crash_report_updates_released_on_date(self):
1084 """Validate that older crash reports update the release date."""
1085 self._assert_older_reports_update_released_on_date(Crashreport)
1086
1087 def test_newer_heartbeat_does_not_update_released_on_date(self):
1088 """Validate that newer heartbeats don't update the release date."""
1089 self._assert_newer_reports_do_not_update_released_on_date(HeartBeat)
1090
1091 def test_newer_crash_report_does_not_update_released_on_date(self):
1092 """Validate that newer crash reports don't update the release date."""
1093 self._assert_newer_reports_do_not_update_released_on_date(Crashreport)
1094
1095 def _assert_manually_changed_released_on_date_is_not_updated(
1096 self, report_type, **kwargs
1097 ):
1098 """Test updating of manually changed released_on dates.
1099
1100 Validate that a manually changed released_on date is not updated when
1101 new reports are sent.
1102 """
1103 # Create a report
1104 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
1105 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
1106
1107 # Run the command to update the database
1108 call_command("stats", "update")
1109
1110 # Get the corresponding version instance from the database
1111 version = self.version_class.objects.get(
1112 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1113 )
1114
1115 # Assert that the released_on date matches the first report date
1116 self.assertEqual(version.released_on, report.date.date())
1117
1118 # Create a second report with a timestamp earlier in time
1119 report_2_date = report.date - timedelta(days=1)
1120 Dummy.create_dummy_report(
1121 report_type, device=device, date=report_2_date, **kwargs
1122 )
1123
1124 # Manually change the released_on date
1125 version_release_date = report.date + timedelta(days=1)
1126 version.released_on = version_release_date
1127 version.save()
1128
1129 # Run the command to update the database
1130 call_command("stats", "update")
1131
1132 # Get the corresponding version instance from the database
1133 version = self.version_class.objects.get(
1134 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1135 )
1136
1137 # Assert that the released_on date still matches the date is was
1138 # manually changed to
1139 self.assertEqual(version.released_on, version_release_date.date())
1140
1141 def test_manually_changed_released_on_date_is_not_updated_by_heartbeat(
1142 self
1143 ):
1144 """Test update of manually changed released_on date with heartbeat."""
1145 self._assert_manually_changed_released_on_date_is_not_updated(HeartBeat)
1146
1147 def test_manually_changed_released_on_date_is_not_updated_by_crash_report(
1148 self
1149 ):
1150 """Test update of manually changed released_on date with crashreport."""
1151 self._assert_manually_changed_released_on_date_is_not_updated(
1152 Crashreport
1153 )
1154
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +04001155
1156# pylint: disable=too-many-ancestors
1157class StatsCommandRadioVersionsTestCase(StatsCommandVersionsTestCase):
1158 """Test the generation of RadioVersion stats with the stats command."""
1159
1160 version_class = RadioVersion
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001161 unique_entry_name = "radio_version"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +04001162 unique_entries = Dummy.RADIO_VERSIONS
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001163
1164
1165class CommandDebugOutputTestCase(TestCase):
1166 """Test the reset and update commands debug output."""
1167
1168 # Additional positional arguments to pass to the commands
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001169 _CMD_ARGS = ["--no-color", "-v 2"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001170
1171 # The stats models
1172 _STATS_MODELS = [Version, VersionDaily, RadioVersion, RadioVersionDaily]
1173 # The models that will generate an output
1174 _ALL_MODELS = _STATS_MODELS + [StatsMetadata]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001175 _COUNTER_NAMES = ["heartbeats", "crashes", "smpl", "other"]
1176 _COUNTER_ACTIONS = ["created", "updated"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001177
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +02001178 def _assert_command_output_matches(self, command, number, facts, models):
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001179 """Validate the debug output of a command.
1180
1181 The debug output is matched against the facts and models given in
1182 the parameters.
1183 """
1184 buffer = StringIO()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001185 call_command("stats", command, *self._CMD_ARGS, stdout=buffer)
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001186 output = buffer.getvalue().splitlines()
1187
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001188 expected_output = "{number} {model} {fact}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001189 for model in models:
1190 for fact in facts:
1191 self.assertIn(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001192 expected_output.format(
1193 number=number, model=model.__name__, fact=fact
1194 ),
1195 output,
1196 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001197
1198 def test_reset_command_on_empty_db(self):
1199 """Test the reset command on an empty database.
1200
1201 The reset command should yield nothing on an empty database.
1202 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001203 self._assert_command_output_matches(
1204 "reset", 0, ["deleted"], self._ALL_MODELS
1205 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001206
1207 def test_update_command_on_empty_db(self):
1208 """Test the update command on an empty database.
1209
1210 The update command should yield nothing on an empty database.
1211 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001212 pattern = "{action} for counter {counter}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001213 facts = [
1214 pattern.format(action=counter_action, counter=counter_name)
1215 for counter_action in self._COUNTER_ACTIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001216 for counter_name in self._COUNTER_NAMES
1217 ]
1218 self._assert_command_output_matches(
1219 "update", 0, facts, self._STATS_MODELS
1220 )
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +02001221
1222 def test_reset_command_deletion_of_instances(self):
1223 """Test the deletion of stats model instances with the reset command.
1224
1225 This test validates that model instances get deleted when the
1226 reset command is called on a database that only contains a single
1227 model instance for each class.
1228 """
1229 # Create dummy version instances
1230 version = Dummy.create_dummy_version()
1231 radio_version = Dummy.create_dummy_radio_version()
1232 Dummy.create_dummy_daily_version(version)
1233 Dummy.create_dummy_daily_radio_version(radio_version)
1234 Dummy.create_dummy_stats_metadata()
1235
1236 # We expect that the model instances get deleted
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001237 self._assert_command_output_matches(
1238 "reset", 1, ["deleted"], self._ALL_MODELS
1239 )
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001240
1241
1242class DeviceStatsTestCase(_HiccupAPITestCase):
1243 """Test the single device stats REST endpoints."""
1244
1245 def _get_with_params(self, url, params):
1246 url = reverse(url, kwargs=params)
1247 return self.fp_staff_client.get(url)
1248
1249 def _assert_device_stats_response_is(
1250 self,
1251 response,
1252 uuid,
1253 board_date,
1254 num_heartbeats,
1255 num_crashreports,
1256 num_smpls,
1257 crashes_per_day,
1258 smpl_per_day,
1259 last_active,
1260 ):
1261 # pylint: disable=too-many-arguments
1262 self.assertEqual(response.status_code, status.HTTP_200_OK)
1263
1264 self.assertIn("uuid", response.data)
1265 self.assertIn("board_date", response.data)
1266 self.assertIn("heartbeats", response.data)
1267 self.assertIn("crashreports", response.data)
1268 self.assertIn("smpls", response.data)
1269 self.assertIn("crashes_per_day", response.data)
1270 self.assertIn("smpl_per_day", response.data)
1271 self.assertIn("last_active", response.data)
1272
1273 self.assertEqual(response.data["uuid"], uuid)
1274 self.assertEqual(response.data["board_date"], board_date)
1275 self.assertEqual(response.data["heartbeats"], num_heartbeats)
1276 self.assertEqual(response.data["crashreports"], num_crashreports)
1277 self.assertEqual(response.data["smpls"], num_smpls)
1278 self.assertEqual(response.data["crashes_per_day"], crashes_per_day)
1279 self.assertEqual(response.data["smpl_per_day"], smpl_per_day)
1280 self.assertEqual(response.data["last_active"], last_active)
1281
1282 @unittest.skip(
1283 "Fails because there is no fallback for the last_active "
1284 "date for devices without heartbeats."
1285 )
1286 def test_get_device_stats_no_reports(self):
1287 """Test getting device stats for a device without reports."""
1288 # Create a device
1289 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1290
1291 # Get the device statistics
1292 response = self._get_with_params(
1293 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1294 )
1295
1296 # Assert that the statistics match
1297 self._assert_device_stats_response_is(
1298 response=response,
1299 uuid=str(device.uuid),
1300 board_date=device.board_date,
1301 num_heartbeats=0,
1302 num_crashreports=0,
1303 num_smpls=0,
1304 crashes_per_day=0.0,
1305 smpl_per_day=0.0,
1306 last_active=device.board_date,
1307 )
1308
1309 def test_get_device_stats_no_crash_reports(self):
1310 """Test getting device stats for a device without crashreports."""
1311 # Create a device and a heartbeat
1312 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1313 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1314
1315 # Get the device statistics
1316 response = self._get_with_params(
1317 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1318 )
1319
1320 # Assert that the statistics match
1321 self._assert_device_stats_response_is(
1322 response=response,
1323 uuid=str(device.uuid),
1324 board_date=device.board_date,
1325 num_heartbeats=1,
1326 num_crashreports=0,
1327 num_smpls=0,
1328 crashes_per_day=0.0,
1329 smpl_per_day=0.0,
1330 last_active=heartbeat.date,
1331 )
1332
1333 @unittest.skip(
1334 "Fails because there is no fallback for the last_active "
1335 "date for devices without heartbeats."
1336 )
1337 def test_get_device_stats_no_heartbeats(self):
1338 """Test getting device stats for a device without heartbeats."""
1339 # Create a device and crashreport
1340 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1341 Dummy.create_dummy_report(Crashreport, device)
1342
1343 # Get the device statistics
1344 response = self._get_with_params(
1345 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1346 )
1347
1348 # Assert that the statistics match
1349 self._assert_device_stats_response_is(
1350 response=response,
1351 uuid=str(device.uuid),
1352 board_date=device.board_date,
1353 num_heartbeats=0,
1354 num_crashreports=1,
1355 num_smpls=0,
1356 crashes_per_day=0.0,
1357 smpl_per_day=0.0,
1358 last_active=device.board_date,
1359 )
1360
1361 def test_get_device_stats(self):
1362 """Test getting device stats for a device."""
1363 # Create a device with a heartbeat and one report of each type
1364 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1365 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1366 for boot_reason in (
1367 Crashreport.SMPL_BOOT_REASONS
1368 + Crashreport.CRASH_BOOT_REASONS
1369 + ["other boot reason"]
1370 ):
1371 Dummy.create_dummy_report(
1372 Crashreport, device, boot_reason=boot_reason
1373 )
1374
1375 # Get the device statistics
1376 response = self._get_with_params(
1377 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1378 )
1379
1380 # Assert that the statistics match
1381 self._assert_device_stats_response_is(
1382 response=response,
1383 uuid=str(device.uuid),
1384 board_date=device.board_date,
1385 num_heartbeats=1,
1386 num_crashreports=len(Crashreport.CRASH_BOOT_REASONS),
1387 num_smpls=len(Crashreport.SMPL_BOOT_REASONS),
1388 crashes_per_day=len(Crashreport.CRASH_BOOT_REASONS),
1389 smpl_per_day=len(Crashreport.SMPL_BOOT_REASONS),
1390 last_active=heartbeat.date,
1391 )
1392
1393 def test_get_device_stats_multiple_days(self):
1394 """Test getting device stats for a device that sent more reports."""
1395 # Create a device with some heartbeats and reports over time
1396 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1397 num_days = 100
1398 for i in range(num_days):
1399 report_day = datetime.now(tz=pytz.utc) + timedelta(days=i)
1400 heartbeat = Dummy.create_dummy_report(
1401 HeartBeat, device, date=report_day
1402 )
1403 Dummy.create_dummy_report(Crashreport, device, date=report_day)
1404 Dummy.create_dummy_report(
1405 Crashreport,
1406 device,
1407 date=report_day,
1408 boot_reason=Crashreport.SMPL_BOOT_REASONS[0],
1409 )
1410
1411 # Get the device statistics
1412 response = self._get_with_params(
1413 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1414 )
1415
1416 # Assert that the statistics match
1417 self._assert_device_stats_response_is(
1418 response=response,
1419 uuid=str(device.uuid),
1420 board_date=device.board_date,
1421 num_heartbeats=num_days,
1422 num_crashreports=num_days,
1423 num_smpls=num_days,
1424 crashes_per_day=1,
1425 smpl_per_day=1,
1426 last_active=heartbeat.date,
1427 )
1428
1429 def test_get_device_stats_multiple_days_missing_heartbeat(self):
1430 """Test getting device stats for a device with missing heartbeat."""
1431 # Create a device with some heartbeats and reports over time
1432 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1433 num_days = 100
1434 skip_day = round(num_days / 2)
1435 for i in range(num_days):
1436 report_day = datetime.now(tz=pytz.utc) + timedelta(days=i)
1437 # Skip creation of heartbeat at one day
1438 if i != skip_day:
1439 heartbeat = Dummy.create_dummy_report(
1440 HeartBeat, device, date=report_day
1441 )
1442 Dummy.create_dummy_report(Crashreport, device, date=report_day)
1443
1444 # Get the device statistics
1445 response = self._get_with_params(
1446 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1447 )
1448
1449 # Assert that the statistics match
1450 self._assert_device_stats_response_is(
1451 response=response,
1452 uuid=str(device.uuid),
1453 board_date=device.board_date,
1454 num_heartbeats=num_days - 1,
1455 num_crashreports=num_days,
1456 num_smpls=0,
1457 crashes_per_day=num_days / (num_days - 1),
1458 smpl_per_day=0,
1459 last_active=heartbeat.date,
1460 )
1461
1462 @unittest.skip("Duplicate heartbeats are currently not dropped.")
1463 def test_get_device_stats_multiple_days_duplicate_heartbeat(self):
1464 """Test getting device stats for a device with duplicate heartbeat.
1465
1466 Duplicate heartbeats are dropped and thus should not influence the
1467 statistics.
1468 """
1469 # Create a device with some heartbeats and reports over time
1470 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1471 num_days = 100
1472 duplicate_day = round(num_days / 2)
1473 first_report_day = Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES["date"]
1474 for i in range(num_days):
1475 report_day = first_report_day + timedelta(days=i)
1476 heartbeat = Dummy.create_dummy_report(
1477 HeartBeat, device, date=report_day
1478 )
1479 # Create a second at the duplicate day (with 1 hour delay)
1480 if i == duplicate_day:
1481 Dummy.create_dummy_report(
1482 HeartBeat, device, date=report_day + timedelta(hours=1)
1483 )
1484 Dummy.create_dummy_report(Crashreport, device, date=report_day)
1485
1486 # Get the device statistics
1487 response = self._get_with_params(
1488 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1489 )
1490
1491 # Assert that the statistics match
1492 self._assert_device_stats_response_is(
1493 response=response,
1494 uuid=str(device.uuid),
1495 board_date=device.board_date,
1496 num_heartbeats=num_days,
1497 num_crashreports=num_days,
1498 num_smpls=0,
1499 crashes_per_day=1,
1500 smpl_per_day=0,
1501 last_active=heartbeat.date,
1502 )
1503
1504 def test_get_device_report_history_no_reports(self):
1505 """Test getting report history stats for a device without reports."""
1506 # Create a device
1507 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1508
1509 # Get the device report history statistics
1510 response = self._get_with_params(
1511 "hiccup_stats_api_v1_device_report_history", {"uuid": device.uuid}
1512 )
1513
1514 # Assert that the report history is empty
1515 self.assertEqual([], response.data)
1516
1517 @unittest.skip("Broken raw query. Heartbeats are not counted correctly.")
1518 def test_get_device_report_history(self):
1519 """Test getting report history stats for a device."""
1520 # Create a device with a heartbeat and one report of each type
1521 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1522 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1523 for boot_reason in (
1524 Crashreport.SMPL_BOOT_REASONS
1525 + Crashreport.CRASH_BOOT_REASONS
1526 + ["other boot reason"]
1527 ):
1528 Dummy.create_dummy_report(
1529 Crashreport, device, boot_reason=boot_reason
1530 )
1531
1532 # Get the device report history statistics
1533 response = self._get_with_params(
1534 "hiccup_stats_api_v1_device_report_history", {"uuid": device.uuid}
1535 )
1536
1537 # Assert that the statistics match
1538 report_history = [
1539 {
1540 "date": heartbeat.date.date(),
1541 "heartbeats": 1,
1542 "smpl": len(Crashreport.SMPL_BOOT_REASONS),
1543 "prob_crashes": len(Crashreport.CRASH_BOOT_REASONS),
1544 "other": 1,
1545 }
1546 ]
1547 self.assertEqual(report_history, response.data)
1548
1549 def test_get_device_update_history_no_reports(self):
1550 """Test getting update history stats for a device without reports."""
1551 # Create a device
1552 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1553
1554 # Get the device report history statistics
1555 response = self._get_with_params(
1556 "hiccup_stats_api_v1_device_update_history", {"uuid": device.uuid}
1557 )
1558
1559 # Assert that the update history is empty
1560 self.assertEqual([], response.data)
1561
1562 def test_get_device_update_history(self):
1563 """Test getting update history stats for a device."""
1564 # Create a device with a heartbeat and one report of each type
1565 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1566 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1567 for boot_reason in (
1568 Crashreport.SMPL_BOOT_REASONS
1569 + Crashreport.CRASH_BOOT_REASONS
1570 + ["other boot reason"]
1571 ):
1572 params = {"boot_reason": boot_reason}
1573 Dummy.create_dummy_report(Crashreport, device, **params)
1574
1575 # Get the device update history statistics
1576 response = self._get_with_params(
1577 "hiccup_stats_api_v1_device_update_history", {"uuid": device.uuid}
1578 )
1579
1580 # Assert that the statistics match
1581 update_history = [
1582 {
1583 "build_fingerprint": heartbeat.build_fingerprint,
1584 "heartbeats": 1,
1585 "max": device.id,
1586 "other": 1,
1587 "prob_crashes": len(Crashreport.CRASH_BOOT_REASONS),
1588 "smpl": len(Crashreport.SMPL_BOOT_REASONS),
1589 "update_date": heartbeat.date,
1590 }
1591 ]
1592 self.assertEqual(update_history, response.data)
1593
1594 def test_get_device_update_history_multiple_updates(self):
1595 """Test getting update history stats with multiple updates."""
1596 # Create a device with a heartbeats and crashreport for each build
1597 # fingerprint in the dummy values
1598 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1599 expected_update_history = []
1600 for i, build_fingerprint in enumerate(Dummy.BUILD_FINGERPRINTS):
1601 report_day = datetime.now(tz=pytz.utc) + timedelta(days=i)
1602 Dummy.create_dummy_report(
1603 HeartBeat,
1604 device,
1605 date=report_day,
1606 build_fingerprint=build_fingerprint,
1607 )
1608 Dummy.create_dummy_report(
1609 Crashreport,
1610 device,
1611 date=report_day,
1612 build_fingerprint=build_fingerprint,
1613 )
1614
1615 # Create the expected update history object
1616 expected_update_history.append(
1617 {
1618 "update_date": report_day,
1619 "build_fingerprint": build_fingerprint,
1620 "max": device.id,
1621 "prob_crashes": 1,
1622 "smpl": 0,
1623 "other": 0,
1624 "heartbeats": 1,
1625 }
1626 )
1627 # Sort the expected values by build fingerprint
1628 expected_update_history.sort(
1629 key=operator.itemgetter("build_fingerprint")
1630 )
1631
1632 # Get the device update history statistics and sort it
1633 response = self._get_with_params(
1634 "hiccup_stats_api_v1_device_update_history", {"uuid": device.uuid}
1635 )
1636 response.data.sort(key=operator.itemgetter("build_fingerprint"))
1637
1638 # Assert that the statistics match
1639 self.assertEqual(expected_update_history, response.data)
1640
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001641 def test_download_non_existing_logfile(self):
1642 """Test download of a non existing log file."""
1643 # Try to get a log file
1644 response = self._get_with_params(
1645 "hiccup_stats_api_v1_logfile_download", {"id_logfile": 0}
1646 )
1647
1648 # Assert that the log file was not found
1649 self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
1650
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001651 def test_download_logfile(self):
1652 """Test download of log files."""
1653 # Create a device with a crash report along with log file
1654 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1655 crashreport = Dummy.create_dummy_report(Crashreport, device)
1656 logfile = Dummy.create_dummy_log_file(crashreport)
1657
1658 # Get the log file
1659 response = self._get_with_params(
1660 "hiccup_stats_api_v1_logfile_download", {"id_logfile": logfile.id}
1661 )
1662
1663 # Assert that the log file contents are in the response data
1664 self.assertEqual(response.status_code, status.HTTP_200_OK)
1665 self.assertIn(Dummy.DEFAULT_DUMMY_LOG_FILE_NAME, response.data)
1666 expected_logfile_content = Dummy.read_logfile_contents(
1667 logfile.logfile.path, Dummy.DEFAULT_DUMMY_LOG_FILE_NAME
1668 )
1669 self.assertEqual(
1670 response.data[Dummy.DEFAULT_DUMMY_LOG_FILE_NAME],
1671 expected_logfile_content,
1672 )
Mitja Nikolausded30ae2018-09-14 15:40:08 +02001673
1674
1675class ViewsTestCase(_HiccupAPITestCase):
1676 """Test cases for the statistics views."""
1677
1678 home_url = reverse("device")
1679 device_url = reverse("hiccup_stats_device")
1680 versions_url = reverse("hiccup_stats_versions")
1681 versions_all_url = reverse("hiccup_stats_versions_all")
1682
1683 @staticmethod
1684 def _url_with_params(url, params):
1685 return "{}?{}".format(url, urlencode(params))
1686
1687 def _get_with_params(self, url, params):
1688 return self.fp_staff_client.get(self._url_with_params(url, params))
1689
1690 def test_get_home_view(self):
1691 """Test getting the home view with device search form."""
1692 response = self.fp_staff_client.get(self.home_url)
1693 self.assertEqual(response.status_code, status.HTTP_200_OK)
1694 self.assertTemplateUsed(
1695 response, "crashreport_stats/home.html", count=1
1696 )
1697 self.assertEqual(response.context["devices"], None)
1698
1699 def test_home_view_filter_devices_by_uuid(self):
1700 """Test filtering devices by UUID."""
1701 # Create a device
1702 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1703
1704 # Filter devices by UUID of the created device
1705 response = self.fp_staff_client.post(
1706 self.home_url, data={"uuid": str(device.uuid)}
1707 )
1708
1709 # Assert that the the client is redirected to the device page
1710 self.assertRedirects(
1711 response,
1712 self._url_with_params(self.device_url, {"uuid": device.uuid}),
1713 )
1714
1715 def test_home_view_filter_devices_by_uuid_part(self):
1716 """Test filtering devices by start of UUID."""
1717 # Create a device
1718 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1719
1720 # Filter devices with start of the created device's UUID
1721 response = self.fp_staff_client.post(
1722 self.home_url, data={"uuid": str(device.uuid)[:4]}
1723 )
1724
1725 # Assert that the the client is redirected to the device page
1726 self.assertRedirects(
1727 response,
1728 self._url_with_params(self.device_url, {"uuid": device.uuid}),
1729 )
1730
1731 def test_home_view_filter_devices_by_uuid_part_ambiguous_result(self):
1732 """Test filtering devices with common start of UUIDs."""
1733 # Create two devices
1734 device1 = Dummy.create_dummy_device(Dummy.create_dummy_user())
1735 device2 = Dummy.create_dummy_device(
1736 Dummy.create_dummy_user(username=Dummy.USERNAMES[1])
1737 )
1738
1739 # Adapt the devices' UUID so that they start with the same characters
1740 device1.uuid = "4060fd90-6de1-4b03-a380-4277c703e913"
1741 device1.save()
1742 device2.uuid = "4061c59b-823d-4ec6-a463-8ac0c1cea67d"
1743 device2.save()
1744
1745 # Filter devices with first three (common) characters of the UUID
1746 response = self.fp_staff_client.post(
1747 self.home_url, data={"uuid": str(device1.uuid)[:3]}
1748 )
1749
1750 # Assert that both devices are part of the result
1751 self.assertEqual(response.status_code, status.HTTP_200_OK)
1752 self.assertTemplateUsed(
1753 response, "crashreport_stats/home.html", count=1
1754 )
1755 self.assertEqual(set(response.context["devices"]), {device1, device2})
1756
1757 def test_home_view_filter_devices_empty_database(self):
1758 """Test filtering devices on an empty database."""
1759 response = self.fp_staff_client.post(
1760 self.home_url, data={"uuid": "TestUUID"}
1761 )
1762 self.assertEqual(response.status_code, status.HTTP_200_OK)
1763 self.assertIsNotNone(response.content)
1764
1765 def test_home_view_filter_devices_no_uuid(self):
1766 """Test filtering devices without specifying UUID."""
1767 response = self.fp_staff_client.post(self.home_url)
1768 self.assertEqual(response.status_code, status.HTTP_200_OK)
1769 self.assertTemplateUsed(
1770 response, "crashreport_stats/home.html", count=1
1771 )
1772 self.assertEqual(response.context["devices"], None)
1773
1774 def test_get_device_view_empty_database(self):
1775 """Test getting device view on an empty database."""
1776 response = self.fp_staff_client.get(self.device_url)
1777 self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
1778
1779 def test_get_device_view(self):
1780 """Test getting device view."""
1781 # Create a device
1782 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1783
1784 # Get the corresponding device view
1785 response = self._get_with_params(self.device_url, {"uuid": device.uuid})
1786
1787 # Assert that the view is constructed from the correct templates and
1788 # the response context contains the device UUID
1789 self.assertEqual(response.status_code, status.HTTP_200_OK)
1790 self.assertTemplateUsed(
1791 response, "crashreport_stats/device.html", count=1
1792 )
1793 self.assertTemplateUsed(
1794 response, "crashreport_stats/tags/device_overview.html", count=1
1795 )
1796 self.assertTemplateUsed(
1797 response,
1798 "crashreport_stats/tags/device_update_history.html",
1799 count=1,
1800 )
1801 self.assertTemplateUsed(
1802 response,
1803 "crashreport_stats/tags/device_report_history.html",
1804 count=1,
1805 )
1806 self.assertTemplateUsed(
1807 response,
1808 "crashreport_stats/tags/device_crashreport_table.html",
1809 count=1,
1810 )
1811 self.assertEqual(response.context["uuid"], str(device.uuid))
1812
1813 def _assert_versions_view_templates_are_used(self, response):
1814 self.assertTemplateUsed(
1815 response, "crashreport_stats/versions.html", count=1
1816 )
1817 self.assertTemplateUsed(
1818 response, "crashreport_stats/tags/versions_table.html", count=1
1819 )
1820 self.assertTemplateUsed(
1821 response, "crashreport_stats/tags/versions_pie_chart.html", count=1
1822 )
1823 self.assertTemplateUsed(
1824 response, "crashreport_stats/tags/versions_bar_chart.html", count=1
1825 )
1826 self.assertTemplateUsed(
1827 response, "crashreport_stats/tags/versions_area_chart.html", count=1
1828 )
1829
1830 @unittest.skip("Fails because of wrong boolean usage in views.py")
1831 def test_get_versions_view_empty_database(self):
1832 """Test getting versions view on an empty database."""
1833 response = self.fp_staff_client.get(self.versions_url)
1834
1835 # Assert that the correct templates are used and the response context
1836 # contains the correct value for is_official_release
1837 self._assert_versions_view_templates_are_used(response)
1838 self.assertEqual(response.context["is_official_release"], True)
1839
1840 @unittest.skip("Fails because of wrong boolean usage in views.py")
1841 def test_get_versions_view(self):
1842 """Test getting versions view."""
1843 # Create a version
1844 Dummy.create_dummy_version()
1845
1846 # Get the versions view
1847 response = self.fp_staff_client.get(self.versions_url)
1848
1849 # Assert that the correct templates are used and the response context
1850 # contains the correct value for is_official_release
1851 self._assert_versions_view_templates_are_used(response)
1852 self.assertEqual(response.context["is_official_release"], True)
1853
1854 @unittest.skip("Fails because of wrong boolean usage in views.py")
1855 def test_get_versions_all_view_no_versions(self):
1856 """Test getting versions all view on an empty database."""
1857 response = self.fp_staff_client.get(self.versions_all_url)
1858
1859 # Assert that the correct templates are used and the response context
1860 # contains an empty value for is_official_release
1861 self._assert_versions_view_templates_are_used(response)
1862 self.assertEqual(response.context.get("is_official_release", ""), "")
1863
1864 @unittest.skip("Fails because of wrong boolean usage in views.py")
1865 def test_get_versions_all_view(self):
1866 """Test getting versions view."""
1867 # Create a version
1868 Dummy.create_dummy_version()
1869
1870 # Get the versions view
1871 response = self.fp_staff_client.get(self.versions_all_url)
1872
1873 # Assert that the correct templates are used and the response context
1874 # contains the an empty value for is_official_release
1875 self._assert_versions_view_templates_are_used(response)
1876 self.assertEqual(response.context.get("is_official_release", ""), "")