blob: e9a3734ab3f3a940a6f863df7bc71a553dfe1760 [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
359class _VersionTestCase(_HiccupAPITestCase):
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200360 """Abstract class for version-related test cases to inherit from."""
361
362 # The attribute name characterising the unicity of a stats entry (the
363 # named identifier)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200364 unique_entry_name = "build_fingerprint"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200365 # The collection of unique entries to post
366 unique_entries = Dummy.BUILD_FINGERPRINTS
367 # The URL to retrieve the stats entries from
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200368 endpoint_url = reverse("hiccup_stats_api_v1_versions")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200369
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200370 @staticmethod
371 def _create_dummy_version(**kwargs):
372 return Dummy.create_dummy_version(**kwargs)
373
374 def _get_with_params(self, url, params):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200375 return self.admin.get("{}?{}".format(url, urlencode(params)))
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200376
377 def _assert_result_length_is(self, response, count):
378 self.assertEqual(response.status_code, status.HTTP_200_OK)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200379 self.assertIn("results", response.data)
380 self.assertIn("count", response.data)
381 self.assertEqual(response.data["count"], count)
382 self.assertEqual(len(response.data["results"]), count)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200383
384 def _assert_device_owner_has_no_get_access(self, entries_url):
385 # Create a user and device
386 user = Dummy.create_dummy_user()
387 device = Dummy.create_dummy_device(user=user)
388
389 # Create authenticated client
390 user = APIClient()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200391 user.credentials(HTTP_AUTHORIZATION="Token " + device.token)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200392
393 # Try getting entries using the client
394 response = user.get(entries_url)
395 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
396
397 def _assert_filter_result_matches(self, filter_params, expected_result):
398 # List entities with filter
399 response = self._get_with_params(self.endpoint_url, filter_params)
400
401 # Expect only the single matching result to be returned
402 self._assert_result_length_is(response, 1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200403 self.assertEqual(
404 response.data["results"][0][self.unique_entry_name],
405 getattr(expected_result, self.unique_entry_name),
406 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200407
408
409class VersionTestCase(_VersionTestCase):
410 """Test the Version and REST endpoint."""
411
Mitja Nikolaus78e3a052018-09-05 12:18:35 +0200412 # pylint: disable=too-many-ancestors
413
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200414 def _create_version_entities(self):
415 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200416 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200417 for unique_entry in self.unique_entries
418 ]
419 return versions
420
421 def test_list_versions_without_authentication(self):
422 """Test listing of versions without authentication."""
423 response = self.client.get(self.endpoint_url)
424 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
425
426 def test_list_versions_as_device_owner(self):
427 """Test listing of versions as device owner."""
428 self._assert_device_owner_has_no_get_access(self.endpoint_url)
429
430 def test_list_versions_empty_database(self):
431 """Test listing of versions on an empty database."""
432 response = self.admin.get(self.endpoint_url)
433 self._assert_result_length_is(response, 0)
434
435 def test_list_versions(self):
436 """Test listing versions."""
437 versions = self._create_version_entities()
438 response = self.admin.get(self.endpoint_url)
439 self._assert_result_length_is(response, len(versions))
440
441 def test_filter_versions_by_unique_entry_name(self):
442 """Test filtering versions by their unique entry name."""
443 versions = self._create_version_entities()
444 response = self.admin.get(self.endpoint_url)
445
446 # Listing all entities should return the correct result length
447 self._assert_result_length_is(response, len(versions))
448
449 # List entities with filter
450 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200451 self.unique_entry_name: getattr(versions[0], self.unique_entry_name)
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200452 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200453 self._assert_filter_result_matches(
454 filter_params, expected_result=versions[0]
455 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200456
457 def test_filter_versions_by_release_type(self):
458 """Test filtering versions by release type."""
459 # Create versions for all combinations of release types
460 versions = []
461 i = 0
462 for is_official_release in True, False:
463 for is_beta_release in True, False:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200464 versions.append(
465 self._create_dummy_version(
466 **{
467 "is_official_release": is_official_release,
468 "is_beta_release": is_beta_release,
469 self.unique_entry_name: self.unique_entries[i],
470 }
471 )
472 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200473 i += 1
474
475 # # Listing all entities should return the correct result length
476 response = self.admin.get(self.endpoint_url)
477 self._assert_result_length_is(response, len(versions))
478
479 # List each of the entities with the matching filter params
480 for version in versions:
481 filter_params = {
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200482 "is_official_release": version.is_official_release,
483 "is_beta_release": version.is_beta_release,
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200484 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200485 self._assert_filter_result_matches(
486 filter_params, expected_result=version
487 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200488
489 def test_filter_versions_by_first_seen_date(self):
490 """Test filtering versions by first seen date."""
491 versions = self._create_version_entities()
492
493 # Set the first seen date of an entity
494 versions[0].first_seen_on = Dummy.DATES[2]
495 versions[0].save()
496
497 # Listing all entities should return the correct result length
498 response = self.admin.get(self.endpoint_url)
499 self._assert_result_length_is(response, len(versions))
500
501 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200502 filter_params = {"first_seen_after": Dummy.DATES[2]}
503 self._assert_filter_result_matches(
504 filter_params, expected_result=versions[0]
505 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200506
507
508# pylint: disable=too-many-ancestors
509class RadioVersionTestCase(VersionTestCase):
510 """Test the RadioVersion REST endpoint."""
511
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200512 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200513 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200514 endpoint_url = reverse("hiccup_stats_api_v1_radio_versions")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200515
516 @staticmethod
517 def _create_dummy_version(**kwargs):
518 return Dummy.create_dummy_radio_version(**kwargs)
519
520
521class VersionDailyTestCase(_VersionTestCase):
522 """Test the VersionDaily REST endpoint."""
523
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200524 endpoint_url = reverse("hiccup_stats_api_v1_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200525
526 @staticmethod
527 def _create_dummy_daily_version(version, **kwargs):
528 return Dummy.create_dummy_daily_version(version, **kwargs)
529
530 def _create_version_entities(self):
531 versions = [
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200532 self._create_dummy_version(**{self.unique_entry_name: unique_entry})
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200533 for unique_entry in self.unique_entries
534 ]
535 versions_daily = [
536 self._create_dummy_daily_version(version=version)
537 for version in versions
538 ]
539 return versions_daily
540
541 def test_list_daily_versions_without_authentication(self):
542 """Test listing of daily versions without authentication."""
543 response = self.client.get(self.endpoint_url)
544 self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
545
546 def test_list_daily_versions_as_device_owner(self):
547 """Test listing of daily versions as device owner."""
548 self._assert_device_owner_has_no_get_access(self.endpoint_url)
549
550 def test_list_daily_versions_empty_database(self):
551 """Test listing of daily versions on an empty database."""
552 response = self.admin.get(self.endpoint_url)
553 self._assert_result_length_is(response, 0)
554
555 def test_list_daily_versions(self):
556 """Test listing daily versions."""
557 versions_daily = self._create_version_entities()
558 response = self.admin.get(self.endpoint_url)
559 self._assert_result_length_is(response, len(versions_daily))
560
561 def test_filter_daily_versions_by_version(self):
562 """Test filtering versions by the version they relate to."""
563 # Create VersionDaily entities
564 versions = self._create_version_entities()
565
566 # Listing all entities should return the correct result length
567 response = self.admin.get(self.endpoint_url)
568 self._assert_result_length_is(response, len(versions))
569
570 # List entities with filter
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200571 param_name = "version__" + self.unique_entry_name
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200572 filter_params = {
573 param_name: getattr(versions[0].version, self.unique_entry_name)
574 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200575 self._assert_filter_result_matches(
576 filter_params, expected_result=versions[0].version
577 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200578
579 def test_filter_daily_versions_by_date(self):
580 """Test filtering daily versions by date."""
581 # Create Version and VersionDaily entities
582 versions = self._create_version_entities()
583
584 # Update the date
585 versions[0].date = Dummy.DATES[2]
586 versions[0].save()
587
588 # Listing all entities should return the correct result length
589 response = self.admin.get(self.endpoint_url)
590 self._assert_result_length_is(response, len(versions))
591
592 # Expect the single matching result to be returned
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200593 filter_params = {"date": versions[0].date}
594 self._assert_filter_result_matches(
595 filter_params, expected_result=versions[0].version
596 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200597
598
599class RadioVersionDailyTestCase(VersionDailyTestCase):
600 """Test the RadioVersionDaily REST endpoint."""
601
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200602 unique_entry_name = "radio_version"
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200603 unique_entries = Dummy.RADIO_VERSIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200604 endpoint_url = reverse("hiccup_stats_api_v1_radio_version_daily")
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200605
606 @staticmethod
607 def _create_dummy_version(**kwargs):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200608 entity = RadioVersion(
609 **Dummy.update_copy(
610 Dummy.DEFAULT_DUMMY_RADIO_VERSION_VALUES, kwargs
611 )
612 )
Mitja Nikolaus1f7c03d2018-08-09 11:11:28 +0200613 entity.save()
614 return entity
615
616 @staticmethod
617 def _create_dummy_daily_version(version, **kwargs):
618 return Dummy.create_dummy_daily_radio_version(version, **kwargs)
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400619
620
621class StatsCommandVersionsTestCase(TestCase):
622 """Test the generation of Version stats with the stats command."""
623
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400624 # The class of the version type to be tested
625 version_class = Version
626 # The attribute name characterising the unicity of a stats entry (the
627 # named identifier)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200628 unique_entry_name = "build_fingerprint"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400629 # The collection of unique entries to post
630 unique_entries = Dummy.BUILD_FINGERPRINTS
631
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200632 def _create_reports(
633 self, report_type, unique_entry_name, device, number, **kwargs
634 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400635 # Create reports with distinct timestamps
636 now = datetime.now(pytz.utc)
637 for i in range(number):
638 report_date = now - timedelta(milliseconds=i)
639 report_attributes = {
640 self.unique_entry_name: unique_entry_name,
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200641 "device": device,
642 "date": report_date,
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400643 }
644 report_attributes.update(**kwargs)
645 Dummy.create_dummy_report(report_type, **report_attributes)
646
647 def test_stats_calculation(self):
648 """Test generation of a Version instance."""
649 user = Dummy.create_dummy_user()
650 device = Dummy.create_dummy_device(user=user)
651 heartbeat = Dummy.create_dummy_report(HeartBeat, device=device)
652
653 # Expect that we do not have the Version before updating the stats
654 get_params = {
655 self.unique_entry_name: getattr(heartbeat, self.unique_entry_name)
656 }
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200657 self.assertRaises(
658 self.version_class.DoesNotExist,
659 self.version_class.objects.get,
660 **get_params
661 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400662
663 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200664 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400665
666 # Assume that a corresponding Version instance has been created
667 version = self.version_class.objects.get(**get_params)
668 self.assertIsNotNone(version)
669
670 def _assert_older_report_updates_version_date(self, report_type):
671 """Validate that older reports sent later affect the version date."""
672 user = Dummy.create_dummy_user()
673 device = Dummy.create_dummy_device(user=user)
674 report = Dummy.create_dummy_report(report_type, device=device)
675
676 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200677 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400678
679 get_params = {
680 self.unique_entry_name: getattr(report, self.unique_entry_name)
681 }
682 version = self.version_class.objects.get(**get_params)
683
684 self.assertEqual(report.date.date(), version.first_seen_on)
685
686 # Create a new report from an earlier point in time
687 report_time_2 = report.date - timedelta(weeks=1)
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200688 Dummy.create_dummy_report(
689 report_type, device=device, date=report_time_2
690 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400691
692 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200693 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400694
695 # Get the same version object from before
696 version = self.version_class.objects.get(**get_params)
697
698 # Validate that the date matches the report recently sent
699 self.assertEqual(report_time_2.date(), version.first_seen_on)
700
701 def test_older_heartbeat_updates_version_date(self):
702 """Validate updating version date with older heartbeats."""
703 self._assert_older_report_updates_version_date(HeartBeat)
704
705 def test_older_crash_report_updates_version_date(self):
706 """Validate updating version date with older crash reports."""
707 self._assert_older_report_updates_version_date(Crashreport)
708
709 def test_entries_are_unique(self):
710 """Validate the entries' unicity and value."""
711 # Create some reports
712 user = Dummy.create_dummy_user()
713 device = Dummy.create_dummy_device(user=user)
714 for unique_entry in self.unique_entries:
715 self._create_reports(HeartBeat, unique_entry, device, 10)
716
717 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200718 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400719
720 # Check whether the correct amount of distinct versions have been
721 # created
722 versions = self.version_class.objects.all()
723 for version in versions:
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200724 self.assertIn(
725 getattr(version, self.unique_entry_name), self.unique_entries
726 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400727 self.assertEqual(len(versions), len(self.unique_entries))
728
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200729 def _assert_counter_distribution_is_correct(
730 self, report_type, numbers, counter_attribute_name, **kwargs
731 ):
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400732 """Validate a counter distribution in the database."""
733 if len(numbers) != len(self.unique_entries):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200734 raise ValueError(
735 "The length of the numbers list must match the "
736 "length of self.unique_entries in the test class"
737 "({} != {})".format(len(numbers), len(self.unique_entries))
738 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400739 # Create some reports
740 user = Dummy.create_dummy_user()
741 device = Dummy.create_dummy_device(user=user)
742 for unique_entry, num in zip(self.unique_entries, numbers):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200743 self._create_reports(
744 report_type, unique_entry, device, num, **kwargs
745 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400746
747 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200748 call_command("stats", "update")
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400749
750 # Check whether the numbers of reports match
751 for version in self.version_class.objects.all():
752 unique_entry_name = getattr(version, self.unique_entry_name)
753 num = numbers[self.unique_entries.index(unique_entry_name)]
754 self.assertEqual(num, getattr(version, counter_attribute_name))
755
756 def test_heartbeats_counter(self):
757 """Test the calculation of the heartbeats counter."""
758 numbers = [10, 7, 8, 5]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200759 counter_attribute_name = "heartbeats"
760 self._assert_counter_distribution_is_correct(
761 HeartBeat, numbers, counter_attribute_name
762 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400763
764 def test_crash_reports_counter(self):
765 """Test the calculation of the crashreports counter."""
766 numbers = [2, 5, 0, 3]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200767 counter_attribute_name = "prob_crashes"
768 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_UNKOWN}
769 self._assert_counter_distribution_is_correct(
770 Crashreport, numbers, counter_attribute_name, **boot_reason_param
771 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400772
773 def test_smpl_reports_counter(self):
774 """Test the calculation of the smpl reports counter."""
775 numbers = [1, 3, 4, 0]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200776 counter_attribute_name = "smpl"
777 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_RTC_ALARM}
778 self._assert_counter_distribution_is_correct(
779 Crashreport, numbers, counter_attribute_name, **boot_reason_param
780 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400781
782 def test_other_reports_counter(self):
783 """Test the calculation of the other reports counter."""
784 numbers = [0, 2, 1, 2]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200785 counter_attribute_name = "other"
786 boot_reason_param = {"boot_reason": "random boot reason"}
787 self._assert_counter_distribution_is_correct(
788 Crashreport, numbers, counter_attribute_name, **boot_reason_param
789 )
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +0400790
Mitja Nikolaus3a09c6e2018-09-04 12:17:45 +0200791 def _assert_reports_with_same_timestamp_are_counted(
792 self, report_type, counter_attribute_name, **kwargs
793 ):
794 """Validate that reports with the same timestamp are counted.
795
796 Reports from different devices but the same timestamp should be
797 counted as independent reports.
798 """
799 # Create a report
800 device1 = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
801 report1 = Dummy.create_dummy_report(
802 report_type, device=device1, **kwargs
803 )
804
805 # Create a second report with the same timestamp but from another device
806 device2 = Dummy.create_dummy_device(
807 user=Dummy.create_dummy_user(username=Dummy.USERNAMES[1])
808 )
809 Dummy.create_dummy_report(
810 report_type, device=device2, date=report1.date, **kwargs
811 )
812
813 # Run the command to update the database
814 call_command("stats", "update")
815
816 # Get the corresponding version instance from the database
817 get_params = {
818 self.unique_entry_name: getattr(report1, self.unique_entry_name)
819 }
820 version = self.version_class.objects.get(**get_params)
821
822 # Assert that both reports are counted
823 self.assertEqual(getattr(version, counter_attribute_name), 2)
824
825 @unittest.skip(
826 "Duplicates are dropped based on their timestamp at the moment. This is"
827 "to be adapted so that they are dropped taking into account the device"
828 "UUID as well."
829 )
830 def test_heartbeats_with_same_timestamp_are_counted(self):
831 """Validate that heartbeats with same timestamp are counted."""
832 counter_attribute_name = "heartbeats"
833 self._assert_reports_with_same_timestamp_are_counted(
834 HeartBeat, counter_attribute_name
835 )
836
837 @unittest.skip(
838 "Duplicates are dropped based on their timestamp at the moment. This is"
839 "to be adapted so that they are dropped taking into account the device"
840 "UUID as well."
841 )
842 def test_crash_reports_with_same_timestamp_are_counted(self):
843 """Validate that crash report duplicates are ignored."""
844 counter_attribute_name = "prob_crashes"
845 for unique_entry, boot_reason in zip(
846 self.unique_entries, Crashreport.CRASH_BOOT_REASONS
847 ):
848 params = {
849 "boot_reason": boot_reason,
850 self.unique_entry_name: unique_entry,
851 }
852 self._assert_reports_with_same_timestamp_are_counted(
853 Crashreport, counter_attribute_name, **params
854 )
855
856 @unittest.skip(
857 "Duplicates are dropped based on their timestamp at the moment. This is"
858 "to be adapted so that they are dropped taking into account the device"
859 "UUID as well."
860 )
861 def test_smpl_reports_with_same_timestamp_are_counted(self):
862 """Validate that smpl report duplicates are ignored."""
863 counter_attribute_name = "smpl"
864 for unique_entry, boot_reason in zip(
865 self.unique_entries, Crashreport.SMPL_BOOT_REASONS
866 ):
867 params = {
868 "boot_reason": boot_reason,
869 self.unique_entry_name: unique_entry,
870 }
871 self._assert_reports_with_same_timestamp_are_counted(
872 Crashreport, counter_attribute_name, **params
873 )
874
875 @unittest.skip(
876 "Duplicates are dropped based on their timestamp at the moment. This is"
877 "to be adapted so that they are dropped taking into account the device"
878 "UUID as well."
879 )
880 def test_other_reports_with_same_timestamp_are_counted(self):
881 """Validate that other report duplicates are ignored."""
882 counter_attribute_name = "other"
883 params = {"boot_reason": "random boot reason"}
884 self._assert_reports_with_same_timestamp_are_counted(
885 Crashreport, counter_attribute_name, **params
886 )
887
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200888 def _assert_duplicates_are_ignored(
889 self, report_type, device, counter_attribute_name, **kwargs
890 ):
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400891 """Validate that reports with duplicate timestamps are ignored."""
892 # Create a report
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200893 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400894
895 # Create a second report with the same timestamp
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200896 Dummy.create_dummy_report(
897 report_type, device=device, date=report.date, **kwargs
898 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400899
900 # Run the command to update the database
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200901 call_command("stats", "update")
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400902
903 # Get the corresponding version instance from the database
904 get_params = {
905 self.unique_entry_name: getattr(report, self.unique_entry_name)
906 }
907 version = self.version_class.objects.get(**get_params)
908
909 # Assert that the report with the duplicate timestamp is not
910 # counted, i.e. only 1 report is counted.
911 self.assertEqual(getattr(version, counter_attribute_name), 1)
912
913 def test_heartbeat_duplicates_are_ignored(self):
914 """Validate that heartbeat duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200915 counter_attribute_name = "heartbeats"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400916 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200917 self._assert_duplicates_are_ignored(
918 HeartBeat, device, counter_attribute_name
919 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400920
921 def test_crash_report_duplicates_are_ignored(self):
922 """Validate that crash report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200923 counter_attribute_name = "prob_crashes"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400924 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
925 for i, boot_reason in enumerate(Crashreport.CRASH_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200926 params = {
927 "boot_reason": boot_reason,
928 self.unique_entry_name: self.unique_entries[i],
929 }
930 self._assert_duplicates_are_ignored(
931 Crashreport, device, counter_attribute_name, **params
932 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400933
934 def test_smpl_report_duplicates_are_ignored(self):
935 """Validate that smpl report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200936 counter_attribute_name = "smpl"
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400937 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
938 for i, boot_reason in enumerate(Crashreport.SMPL_BOOT_REASONS):
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200939 params = {
940 "boot_reason": boot_reason,
941 self.unique_entry_name: self.unique_entries[i],
942 }
943 self._assert_duplicates_are_ignored(
944 Crashreport, device, counter_attribute_name, **params
945 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400946
947 def test_other_report_duplicates_are_ignored(self):
948 """Validate that other report duplicates are ignored."""
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200949 counter_attribute_name = "other"
950 params = {"boot_reason": "random boot reason"}
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400951 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +0200952 self._assert_duplicates_are_ignored(
953 Crashreport, device, counter_attribute_name, **params
954 )
Borjan Tchakaloffd803b632018-03-20 17:32:37 +0400955
Mitja Nikolaus52e44b82018-09-04 14:23:19 +0200956 def _assert_older_reports_update_released_on_date(
957 self, report_type, **kwargs
958 ):
959 """Test updating of the released_on date.
960
961 Validate that the released_on date is updated once an older report is
962 sent.
963 """
964 # Create a report
965 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
966 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
967
968 # Run the command to update the database
969 call_command("stats", "update")
970
971 # Get the corresponding version instance from the database
972 version = self.version_class.objects.get(
973 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
974 )
975
976 # Assert that the released_on date matches the first report date
977 self.assertEqual(version.released_on, report.date.date())
978
979 # Create a second report with the a timestamp earlier in time
980 report_2_date = report.date - timedelta(days=1)
981 Dummy.create_dummy_report(
982 report_type, device=device, date=report_2_date, **kwargs
983 )
984
985 # Run the command to update the database
986 call_command("stats", "update")
987
988 # Get the corresponding version instance from the database
989 version = self.version_class.objects.get(
990 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
991 )
992
993 # Assert that the released_on date matches the older report date
994 self.assertEqual(version.released_on, report_2_date.date())
995
996 def _assert_newer_reports_do_not_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 not updated once a newer 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 report_1_date = report.date.date()
1008
1009 # Run the command to update the database
1010 call_command("stats", "update")
1011
1012 # Get the corresponding version instance from the database
1013 version = self.version_class.objects.get(
1014 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1015 )
1016
1017 # Assert that the released_on date matches the first report date
1018 self.assertEqual(version.released_on, report_1_date)
1019
1020 # Create a second report with the a timestamp later in time
1021 report_2_date = report.date + timedelta(days=1)
1022 Dummy.create_dummy_report(
1023 report_type, device=device, date=report_2_date, **kwargs
1024 )
1025
1026 # Run the command to update the database
1027 call_command("stats", "update")
1028
1029 # Get the corresponding version instance from the database
1030 version = self.version_class.objects.get(
1031 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1032 )
1033
1034 # Assert that the released_on date matches the older report date
1035 self.assertEqual(version.released_on, report_1_date)
1036
1037 def test_older_heartbeat_updates_released_on_date(self):
1038 """Validate that older heartbeats update the release date."""
1039 self._assert_older_reports_update_released_on_date(HeartBeat)
1040
1041 def test_older_crash_report_updates_released_on_date(self):
1042 """Validate that older crash reports update the release date."""
1043 self._assert_older_reports_update_released_on_date(Crashreport)
1044
1045 def test_newer_heartbeat_does_not_update_released_on_date(self):
1046 """Validate that newer heartbeats don't update the release date."""
1047 self._assert_newer_reports_do_not_update_released_on_date(HeartBeat)
1048
1049 def test_newer_crash_report_does_not_update_released_on_date(self):
1050 """Validate that newer crash reports don't update the release date."""
1051 self._assert_newer_reports_do_not_update_released_on_date(Crashreport)
1052
1053 def _assert_manually_changed_released_on_date_is_not_updated(
1054 self, report_type, **kwargs
1055 ):
1056 """Test updating of manually changed released_on dates.
1057
1058 Validate that a manually changed released_on date is not updated when
1059 new reports are sent.
1060 """
1061 # Create a report
1062 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
1063 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
1064
1065 # Run the command to update the database
1066 call_command("stats", "update")
1067
1068 # Get the corresponding version instance from the database
1069 version = self.version_class.objects.get(
1070 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1071 )
1072
1073 # Assert that the released_on date matches the first report date
1074 self.assertEqual(version.released_on, report.date.date())
1075
1076 # Create a second report with a timestamp earlier in time
1077 report_2_date = report.date - timedelta(days=1)
1078 Dummy.create_dummy_report(
1079 report_type, device=device, date=report_2_date, **kwargs
1080 )
1081
1082 # Manually change the released_on date
1083 version_release_date = report.date + timedelta(days=1)
1084 version.released_on = version_release_date
1085 version.save()
1086
1087 # Run the command to update the database
1088 call_command("stats", "update")
1089
1090 # Get the corresponding version instance from the database
1091 version = self.version_class.objects.get(
1092 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
1093 )
1094
1095 # Assert that the released_on date still matches the date is was
1096 # manually changed to
1097 self.assertEqual(version.released_on, version_release_date.date())
1098
1099 def test_manually_changed_released_on_date_is_not_updated_by_heartbeat(
1100 self
1101 ):
1102 """Test update of manually changed released_on date with heartbeat."""
1103 self._assert_manually_changed_released_on_date_is_not_updated(HeartBeat)
1104
1105 def test_manually_changed_released_on_date_is_not_updated_by_crash_report(
1106 self
1107 ):
1108 """Test update of manually changed released_on date with crashreport."""
1109 self._assert_manually_changed_released_on_date_is_not_updated(
1110 Crashreport
1111 )
1112
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +04001113
1114# pylint: disable=too-many-ancestors
1115class StatsCommandRadioVersionsTestCase(StatsCommandVersionsTestCase):
1116 """Test the generation of RadioVersion stats with the stats command."""
1117
1118 version_class = RadioVersion
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001119 unique_entry_name = "radio_version"
Borjan Tchakaloff0aa1a272018-03-19 17:23:58 +04001120 unique_entries = Dummy.RADIO_VERSIONS
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001121
1122
1123class CommandDebugOutputTestCase(TestCase):
1124 """Test the reset and update commands debug output."""
1125
1126 # Additional positional arguments to pass to the commands
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001127 _CMD_ARGS = ["--no-color", "-v 2"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001128
1129 # The stats models
1130 _STATS_MODELS = [Version, VersionDaily, RadioVersion, RadioVersionDaily]
1131 # The models that will generate an output
1132 _ALL_MODELS = _STATS_MODELS + [StatsMetadata]
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001133 _COUNTER_NAMES = ["heartbeats", "crashes", "smpl", "other"]
1134 _COUNTER_ACTIONS = ["created", "updated"]
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001135
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +02001136 def _assert_command_output_matches(self, command, number, facts, models):
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001137 """Validate the debug output of a command.
1138
1139 The debug output is matched against the facts and models given in
1140 the parameters.
1141 """
1142 buffer = StringIO()
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001143 call_command("stats", command, *self._CMD_ARGS, stdout=buffer)
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001144 output = buffer.getvalue().splitlines()
1145
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001146 expected_output = "{number} {model} {fact}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001147 for model in models:
1148 for fact in facts:
1149 self.assertIn(
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001150 expected_output.format(
1151 number=number, model=model.__name__, fact=fact
1152 ),
1153 output,
1154 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001155
1156 def test_reset_command_on_empty_db(self):
1157 """Test the reset command on an empty database.
1158
1159 The reset command should yield nothing on an empty database.
1160 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001161 self._assert_command_output_matches(
1162 "reset", 0, ["deleted"], self._ALL_MODELS
1163 )
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001164
1165 def test_update_command_on_empty_db(self):
1166 """Test the update command on an empty database.
1167
1168 The update command should yield nothing on an empty database.
1169 """
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001170 pattern = "{action} for counter {counter}"
Borjan Tchakaloff89d5b6c2018-03-22 18:24:56 +04001171 facts = [
1172 pattern.format(action=counter_action, counter=counter_name)
1173 for counter_action in self._COUNTER_ACTIONS
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001174 for counter_name in self._COUNTER_NAMES
1175 ]
1176 self._assert_command_output_matches(
1177 "update", 0, facts, self._STATS_MODELS
1178 )
Franz-Xaver Geigercc1e04d2018-08-07 11:51:51 +02001179
1180 def test_reset_command_deletion_of_instances(self):
1181 """Test the deletion of stats model instances with the reset command.
1182
1183 This test validates that model instances get deleted when the
1184 reset command is called on a database that only contains a single
1185 model instance for each class.
1186 """
1187 # Create dummy version instances
1188 version = Dummy.create_dummy_version()
1189 radio_version = Dummy.create_dummy_radio_version()
1190 Dummy.create_dummy_daily_version(version)
1191 Dummy.create_dummy_daily_radio_version(radio_version)
1192 Dummy.create_dummy_stats_metadata()
1193
1194 # We expect that the model instances get deleted
Mitja Nikolauscb50f2c2018-08-24 13:54:48 +02001195 self._assert_command_output_matches(
1196 "reset", 1, ["deleted"], self._ALL_MODELS
1197 )
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001198
1199
1200class DeviceStatsTestCase(_HiccupAPITestCase):
1201 """Test the single device stats REST endpoints."""
1202
1203 def _get_with_params(self, url, params):
1204 url = reverse(url, kwargs=params)
1205 return self.fp_staff_client.get(url)
1206
1207 def _assert_device_stats_response_is(
1208 self,
1209 response,
1210 uuid,
1211 board_date,
1212 num_heartbeats,
1213 num_crashreports,
1214 num_smpls,
1215 crashes_per_day,
1216 smpl_per_day,
1217 last_active,
1218 ):
1219 # pylint: disable=too-many-arguments
1220 self.assertEqual(response.status_code, status.HTTP_200_OK)
1221
1222 self.assertIn("uuid", response.data)
1223 self.assertIn("board_date", response.data)
1224 self.assertIn("heartbeats", response.data)
1225 self.assertIn("crashreports", response.data)
1226 self.assertIn("smpls", response.data)
1227 self.assertIn("crashes_per_day", response.data)
1228 self.assertIn("smpl_per_day", response.data)
1229 self.assertIn("last_active", response.data)
1230
1231 self.assertEqual(response.data["uuid"], uuid)
1232 self.assertEqual(response.data["board_date"], board_date)
1233 self.assertEqual(response.data["heartbeats"], num_heartbeats)
1234 self.assertEqual(response.data["crashreports"], num_crashreports)
1235 self.assertEqual(response.data["smpls"], num_smpls)
1236 self.assertEqual(response.data["crashes_per_day"], crashes_per_day)
1237 self.assertEqual(response.data["smpl_per_day"], smpl_per_day)
1238 self.assertEqual(response.data["last_active"], last_active)
1239
1240 @unittest.skip(
1241 "Fails because there is no fallback for the last_active "
1242 "date for devices without heartbeats."
1243 )
1244 def test_get_device_stats_no_reports(self):
1245 """Test getting device stats for a device without reports."""
1246 # Create a device
1247 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1248
1249 # Get the device statistics
1250 response = self._get_with_params(
1251 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1252 )
1253
1254 # Assert that the statistics match
1255 self._assert_device_stats_response_is(
1256 response=response,
1257 uuid=str(device.uuid),
1258 board_date=device.board_date,
1259 num_heartbeats=0,
1260 num_crashreports=0,
1261 num_smpls=0,
1262 crashes_per_day=0.0,
1263 smpl_per_day=0.0,
1264 last_active=device.board_date,
1265 )
1266
1267 def test_get_device_stats_no_crash_reports(self):
1268 """Test getting device stats for a device without crashreports."""
1269 # Create a device and a heartbeat
1270 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1271 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1272
1273 # Get the device statistics
1274 response = self._get_with_params(
1275 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1276 )
1277
1278 # Assert that the statistics match
1279 self._assert_device_stats_response_is(
1280 response=response,
1281 uuid=str(device.uuid),
1282 board_date=device.board_date,
1283 num_heartbeats=1,
1284 num_crashreports=0,
1285 num_smpls=0,
1286 crashes_per_day=0.0,
1287 smpl_per_day=0.0,
1288 last_active=heartbeat.date,
1289 )
1290
1291 @unittest.skip(
1292 "Fails because there is no fallback for the last_active "
1293 "date for devices without heartbeats."
1294 )
1295 def test_get_device_stats_no_heartbeats(self):
1296 """Test getting device stats for a device without heartbeats."""
1297 # Create a device and crashreport
1298 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1299 Dummy.create_dummy_report(Crashreport, device)
1300
1301 # Get the device statistics
1302 response = self._get_with_params(
1303 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1304 )
1305
1306 # Assert that the statistics match
1307 self._assert_device_stats_response_is(
1308 response=response,
1309 uuid=str(device.uuid),
1310 board_date=device.board_date,
1311 num_heartbeats=0,
1312 num_crashreports=1,
1313 num_smpls=0,
1314 crashes_per_day=0.0,
1315 smpl_per_day=0.0,
1316 last_active=device.board_date,
1317 )
1318
1319 def test_get_device_stats(self):
1320 """Test getting device stats for a device."""
1321 # Create a device with a heartbeat and one report of each type
1322 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1323 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1324 for boot_reason in (
1325 Crashreport.SMPL_BOOT_REASONS
1326 + Crashreport.CRASH_BOOT_REASONS
1327 + ["other boot reason"]
1328 ):
1329 Dummy.create_dummy_report(
1330 Crashreport, device, boot_reason=boot_reason
1331 )
1332
1333 # Get the device statistics
1334 response = self._get_with_params(
1335 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1336 )
1337
1338 # Assert that the statistics match
1339 self._assert_device_stats_response_is(
1340 response=response,
1341 uuid=str(device.uuid),
1342 board_date=device.board_date,
1343 num_heartbeats=1,
1344 num_crashreports=len(Crashreport.CRASH_BOOT_REASONS),
1345 num_smpls=len(Crashreport.SMPL_BOOT_REASONS),
1346 crashes_per_day=len(Crashreport.CRASH_BOOT_REASONS),
1347 smpl_per_day=len(Crashreport.SMPL_BOOT_REASONS),
1348 last_active=heartbeat.date,
1349 )
1350
1351 def test_get_device_stats_multiple_days(self):
1352 """Test getting device stats for a device that sent more reports."""
1353 # Create a device with some heartbeats and reports over time
1354 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1355 num_days = 100
1356 for i in range(num_days):
1357 report_day = datetime.now(tz=pytz.utc) + timedelta(days=i)
1358 heartbeat = Dummy.create_dummy_report(
1359 HeartBeat, device, date=report_day
1360 )
1361 Dummy.create_dummy_report(Crashreport, device, date=report_day)
1362 Dummy.create_dummy_report(
1363 Crashreport,
1364 device,
1365 date=report_day,
1366 boot_reason=Crashreport.SMPL_BOOT_REASONS[0],
1367 )
1368
1369 # Get the device statistics
1370 response = self._get_with_params(
1371 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1372 )
1373
1374 # Assert that the statistics match
1375 self._assert_device_stats_response_is(
1376 response=response,
1377 uuid=str(device.uuid),
1378 board_date=device.board_date,
1379 num_heartbeats=num_days,
1380 num_crashreports=num_days,
1381 num_smpls=num_days,
1382 crashes_per_day=1,
1383 smpl_per_day=1,
1384 last_active=heartbeat.date,
1385 )
1386
1387 def test_get_device_stats_multiple_days_missing_heartbeat(self):
1388 """Test getting device stats for a device with missing heartbeat."""
1389 # Create a device with some heartbeats and reports over time
1390 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1391 num_days = 100
1392 skip_day = round(num_days / 2)
1393 for i in range(num_days):
1394 report_day = datetime.now(tz=pytz.utc) + timedelta(days=i)
1395 # Skip creation of heartbeat at one day
1396 if i != skip_day:
1397 heartbeat = Dummy.create_dummy_report(
1398 HeartBeat, device, date=report_day
1399 )
1400 Dummy.create_dummy_report(Crashreport, device, date=report_day)
1401
1402 # Get the device statistics
1403 response = self._get_with_params(
1404 "hiccup_stats_api_v1_device_overview", {"uuid": device.uuid}
1405 )
1406
1407 # Assert that the statistics match
1408 self._assert_device_stats_response_is(
1409 response=response,
1410 uuid=str(device.uuid),
1411 board_date=device.board_date,
1412 num_heartbeats=num_days - 1,
1413 num_crashreports=num_days,
1414 num_smpls=0,
1415 crashes_per_day=num_days / (num_days - 1),
1416 smpl_per_day=0,
1417 last_active=heartbeat.date,
1418 )
1419
1420 @unittest.skip("Duplicate heartbeats are currently not dropped.")
1421 def test_get_device_stats_multiple_days_duplicate_heartbeat(self):
1422 """Test getting device stats for a device with duplicate heartbeat.
1423
1424 Duplicate heartbeats are dropped and thus should not influence the
1425 statistics.
1426 """
1427 # Create a device with some heartbeats and reports over time
1428 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1429 num_days = 100
1430 duplicate_day = round(num_days / 2)
1431 first_report_day = Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES["date"]
1432 for i in range(num_days):
1433 report_day = first_report_day + timedelta(days=i)
1434 heartbeat = Dummy.create_dummy_report(
1435 HeartBeat, device, date=report_day
1436 )
1437 # Create a second at the duplicate day (with 1 hour delay)
1438 if i == duplicate_day:
1439 Dummy.create_dummy_report(
1440 HeartBeat, device, date=report_day + timedelta(hours=1)
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,
1455 num_crashreports=num_days,
1456 num_smpls=0,
1457 crashes_per_day=1,
1458 smpl_per_day=0,
1459 last_active=heartbeat.date,
1460 )
1461
1462 def test_get_device_report_history_no_reports(self):
1463 """Test getting report history stats for a device without reports."""
1464 # Create a device
1465 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1466
1467 # Get the device report history statistics
1468 response = self._get_with_params(
1469 "hiccup_stats_api_v1_device_report_history", {"uuid": device.uuid}
1470 )
1471
1472 # Assert that the report history is empty
1473 self.assertEqual([], response.data)
1474
1475 @unittest.skip("Broken raw query. Heartbeats are not counted correctly.")
1476 def test_get_device_report_history(self):
1477 """Test getting report history stats for a device."""
1478 # Create a device with a heartbeat and one report of each type
1479 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1480 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1481 for boot_reason in (
1482 Crashreport.SMPL_BOOT_REASONS
1483 + Crashreport.CRASH_BOOT_REASONS
1484 + ["other boot reason"]
1485 ):
1486 Dummy.create_dummy_report(
1487 Crashreport, device, boot_reason=boot_reason
1488 )
1489
1490 # Get the device report history statistics
1491 response = self._get_with_params(
1492 "hiccup_stats_api_v1_device_report_history", {"uuid": device.uuid}
1493 )
1494
1495 # Assert that the statistics match
1496 report_history = [
1497 {
1498 "date": heartbeat.date.date(),
1499 "heartbeats": 1,
1500 "smpl": len(Crashreport.SMPL_BOOT_REASONS),
1501 "prob_crashes": len(Crashreport.CRASH_BOOT_REASONS),
1502 "other": 1,
1503 }
1504 ]
1505 self.assertEqual(report_history, response.data)
1506
1507 def test_get_device_update_history_no_reports(self):
1508 """Test getting update history stats for a device without reports."""
1509 # Create a device
1510 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1511
1512 # Get the device report history statistics
1513 response = self._get_with_params(
1514 "hiccup_stats_api_v1_device_update_history", {"uuid": device.uuid}
1515 )
1516
1517 # Assert that the update history is empty
1518 self.assertEqual([], response.data)
1519
1520 def test_get_device_update_history(self):
1521 """Test getting update history stats for a device."""
1522 # Create a device with a heartbeat and one report of each type
1523 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1524 heartbeat = Dummy.create_dummy_report(HeartBeat, device)
1525 for boot_reason in (
1526 Crashreport.SMPL_BOOT_REASONS
1527 + Crashreport.CRASH_BOOT_REASONS
1528 + ["other boot reason"]
1529 ):
1530 params = {"boot_reason": boot_reason}
1531 Dummy.create_dummy_report(Crashreport, device, **params)
1532
1533 # Get the device update history statistics
1534 response = self._get_with_params(
1535 "hiccup_stats_api_v1_device_update_history", {"uuid": device.uuid}
1536 )
1537
1538 # Assert that the statistics match
1539 update_history = [
1540 {
1541 "build_fingerprint": heartbeat.build_fingerprint,
1542 "heartbeats": 1,
1543 "max": device.id,
1544 "other": 1,
1545 "prob_crashes": len(Crashreport.CRASH_BOOT_REASONS),
1546 "smpl": len(Crashreport.SMPL_BOOT_REASONS),
1547 "update_date": heartbeat.date,
1548 }
1549 ]
1550 self.assertEqual(update_history, response.data)
1551
1552 def test_get_device_update_history_multiple_updates(self):
1553 """Test getting update history stats with multiple updates."""
1554 # Create a device with a heartbeats and crashreport for each build
1555 # fingerprint in the dummy values
1556 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1557 expected_update_history = []
1558 for i, build_fingerprint in enumerate(Dummy.BUILD_FINGERPRINTS):
1559 report_day = datetime.now(tz=pytz.utc) + timedelta(days=i)
1560 Dummy.create_dummy_report(
1561 HeartBeat,
1562 device,
1563 date=report_day,
1564 build_fingerprint=build_fingerprint,
1565 )
1566 Dummy.create_dummy_report(
1567 Crashreport,
1568 device,
1569 date=report_day,
1570 build_fingerprint=build_fingerprint,
1571 )
1572
1573 # Create the expected update history object
1574 expected_update_history.append(
1575 {
1576 "update_date": report_day,
1577 "build_fingerprint": build_fingerprint,
1578 "max": device.id,
1579 "prob_crashes": 1,
1580 "smpl": 0,
1581 "other": 0,
1582 "heartbeats": 1,
1583 }
1584 )
1585 # Sort the expected values by build fingerprint
1586 expected_update_history.sort(
1587 key=operator.itemgetter("build_fingerprint")
1588 )
1589
1590 # Get the device update history statistics and sort it
1591 response = self._get_with_params(
1592 "hiccup_stats_api_v1_device_update_history", {"uuid": device.uuid}
1593 )
1594 response.data.sort(key=operator.itemgetter("build_fingerprint"))
1595
1596 # Assert that the statistics match
1597 self.assertEqual(expected_update_history, response.data)
1598
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001599 def test_download_non_existing_logfile(self):
1600 """Test download of a non existing log file."""
1601 # Try to get a log file
1602 response = self._get_with_params(
1603 "hiccup_stats_api_v1_logfile_download", {"id_logfile": 0}
1604 )
1605
1606 # Assert that the log file was not found
1607 self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
1608
Mitja Nikolaus78e3a052018-09-05 12:18:35 +02001609 def test_download_logfile(self):
1610 """Test download of log files."""
1611 # Create a device with a crash report along with log file
1612 device = Dummy.create_dummy_device(Dummy.create_dummy_user())
1613 crashreport = Dummy.create_dummy_report(Crashreport, device)
1614 logfile = Dummy.create_dummy_log_file(crashreport)
1615
1616 # Get the log file
1617 response = self._get_with_params(
1618 "hiccup_stats_api_v1_logfile_download", {"id_logfile": logfile.id}
1619 )
1620
1621 # Assert that the log file contents are in the response data
1622 self.assertEqual(response.status_code, status.HTTP_200_OK)
1623 self.assertIn(Dummy.DEFAULT_DUMMY_LOG_FILE_NAME, response.data)
1624 expected_logfile_content = Dummy.read_logfile_contents(
1625 logfile.logfile.path, Dummy.DEFAULT_DUMMY_LOG_FILE_NAME
1626 )
1627 self.assertEqual(
1628 response.data[Dummy.DEFAULT_DUMMY_LOG_FILE_NAME],
1629 expected_logfile_content,
1630 )