blob: 66a3be0ad081d35033ddce51ac86dddced2ac15f [file] [log] [blame]
Mitja Nikolaus03e412b2018-09-18 17:50:15 +02001"""Utility functions shared by all crashreports tests."""
2
Mitja Nikolaus6e118472018-10-04 11:15:29 +02003import os
Mitja Nikolauscc90d572018-11-22 16:40:15 +01004import shutil
Mitja Nikolausbc03e682018-11-13 16:48:20 +01005import threading
Mitja Nikolaus7dc86722018-11-27 14:57:39 +01006import zipfile
7from datetime import date, datetime
Mitja Nikolaus1509d8f2018-12-06 11:33:17 +01008from typing import Optional, Dict, Type, Union, Tuple, Any
Mitja Nikolaus03e412b2018-09-18 17:50:15 +02009
Mitja Nikolaus7dc86722018-11-27 14:57:39 +010010import pytz
Mitja Nikolauscc90d572018-11-22 16:40:15 +010011from django.conf import settings
Mitja Nikolause0e83772018-11-05 10:00:53 +010012from django.contrib.auth.models import User, Group
Mitja Nikolausbc03e682018-11-13 16:48:20 +010013from django.test import TransactionTestCase
Mitja Nikolaus03e412b2018-09-18 17:50:15 +020014from django.urls import reverse
15from rest_framework import status
Mitja Nikolausbc03e682018-11-13 16:48:20 +010016from rest_framework.test import APIClient, APITestCase
Mitja Nikolaus03e412b2018-09-18 17:50:15 +020017
Mitja Nikolauscc90d572018-11-22 16:40:15 +010018from crashreports.models import (
19 Crashreport,
20 Device,
21 HeartBeat,
22 LogFile,
23 crashreport_file_name,
24)
Mitja Nikolause0e83772018-11-05 10:00:53 +010025from hiccup.allauth_adapters import FP_STAFF_GROUP_NAME
Mitja Nikolaus03e412b2018-09-18 17:50:15 +020026
27
28class InvalidCrashTypeError(BaseException):
29 """Invalid crash type encountered.
30
31 The valid crash type values (strings) are:
32 - 'crash';
33 - 'smpl';
34 - 'other'.
35
36 Args:
37 - crash_type: The invalid crash type.
38 """
39
40 def __init__(self, crash_type):
41 """Initialise the exception using the crash type to build a message.
42
43 Args:
44 crash_type: The invalid crash type.
45 """
46 super(InvalidCrashTypeError, self).__init__(
47 "{} is not a valid crash type".format(crash_type)
48 )
49
50
51class Dummy:
52 """Dummy values for devices, heartbeats and crashreports."""
53
Mitja Nikolaus7dc86722018-11-27 14:57:39 +010054 # Valid unique entries
55 BUILD_FINGERPRINTS = [
56 (
57 "Fairphone/FP2/FP2:5.1/FP2/r4275.1_FP2_gms76_1.13.0"
58 ":user/release-keys"
59 ),
60 (
61 "Fairphone/FP2/FP2:5.1.1/FP2-gms75.1.13.0/FP2-gms75.1.13.0"
62 ":user/release-keys"
63 ),
64 (
65 "Fairphone/FP2/FP2:6.0.1/FP2-gms-18.04.1/FP2-gms-18.04.1"
66 ":user/release-keys"
67 ),
68 ("Fairphone/FP2/FP2:7.1.2/18.07.2/gms-7480c31d:user/release-keys"),
69 ]
70 RADIO_VERSIONS = [
71 "4437.1-FP2-0-07",
72 "4437.1-FP2-0-08",
73 "4437.1-FP2-0-09",
74 "4437.1-FP2-0-10",
75 ]
76 UUIDs = ["e1c0cc95-ab8d-461a-a768-cb8d9d7fdb04"]
77
Mitja Nikolausfd452f82018-11-07 11:53:59 +010078 USERNAMES = ["testuser1", "testuser2", "testuser3", "testuser4"]
Mitja Nikolaus7dc86722018-11-27 14:57:39 +010079
80 DATES = [date(2018, 3, 19), date(2018, 3, 26), date(2018, 5, 1)]
81
Mitja Nikolaus77dd5652018-12-06 11:27:01 +010082 DEFAULT_USER_VALUES = {"username": USERNAMES[0]}
Mitja Nikolaus7dc86722018-11-27 14:57:39 +010083
Mitja Nikolaus77dd5652018-12-06 11:27:01 +010084 DEFAULT_DEVICE_REGISTER_VALUES = {
Mitja Nikolaus7dc86722018-11-27 14:57:39 +010085 "board_date": datetime(2015, 12, 15, 1, 23, 45, tzinfo=pytz.utc),
Mitja Nikolaus03e412b2018-09-18 17:50:15 +020086 "chipset": "Qualcomm MSM8974PRO-AA",
87 }
88
Mitja Nikolaus77dd5652018-12-06 11:27:01 +010089 DEFAULT_DEVICE_VALUES = DEFAULT_DEVICE_REGISTER_VALUES.copy()
90 DEFAULT_DEVICE_VALUES.update(
Mitja Nikolaus7dc86722018-11-27 14:57:39 +010091 {"token": "64111c62d521fb4724454ca6dea27e18f93ef56e"}
92 )
93
Mitja Nikolaus77dd5652018-12-06 11:27:01 +010094 DEFAULT_HEARTBEAT_VALUES = {
Mitja Nikolaus03e412b2018-09-18 17:50:15 +020095 "app_version": 10100,
96 "uptime": (
97 "up time: 16 days, 21:49:56, idle time: 5 days, 20:55:04, "
98 "sleep time: 10 days, 20:46:27"
99 ),
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100100 "build_fingerprint": BUILD_FINGERPRINTS[0],
101 "radio_version": RADIO_VERSIONS[0],
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100102 "date": date(2018, 3, 19),
103 }
104
105 ALTERNATIVE_HEARTBEAT_VALUES = {
106 "app_version": 10101,
107 "uptime": (
108 "up time: 2 days, 12:39:13, idle time: 2 days, 11:35:01, "
109 "sleep time: 2 days, 11:56:12"
110 ),
111 "build_fingerprint": BUILD_FINGERPRINTS[1],
112 "radio_version": RADIO_VERSIONS[1],
113 "date": date(2018, 3, 19),
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200114 }
115
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100116 DEFAULT_CRASHREPORT_VALUES = DEFAULT_HEARTBEAT_VALUES.copy()
117 DEFAULT_CRASHREPORT_VALUES.update(
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200118 {
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100119 "is_fake_report": False,
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100120 "boot_reason": Crashreport.BOOT_REASON_UNKOWN,
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200121 "power_on_reason": "it was powered on",
122 "power_off_reason": "something happened and it went off",
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100123 "date": datetime(2018, 3, 19, 12, 0, 0, tzinfo=pytz.utc),
124 }
125 )
126
127 ALTERNATIVE_CRASHREPORT_VALUES = ALTERNATIVE_HEARTBEAT_VALUES.copy()
128 ALTERNATIVE_CRASHREPORT_VALUES.update(
129 {
130 "is_fake_report": True,
131 "boot_reason": Crashreport.BOOT_REASON_KEYBOARD_POWER_ON,
132 "power_on_reason": "alternative power on reason",
133 "power_off_reason": "alternative power off reason",
134 "date": datetime(2018, 3, 19, 12, 0, 0, tzinfo=pytz.utc),
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200135 }
136 )
137
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100138 DEFAULT_LOG_FILE_NAME = "dmesg.log"
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100139
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200140 CRASH_TYPE_TO_BOOT_REASON_MAP = {
141 "crash": Crashreport.BOOT_REASON_KEYBOARD_POWER_ON,
142 "smpl": Crashreport.BOOT_REASON_RTC_ALARM,
143 "other": "whatever",
144 }
145
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100146 DEFAULT_LOG_FILE_FILENAMES = ["test_logfile_1.zip", "test_logfile_2.zip"]
147 DEFAULT_LOG_FILE_DIRECTORY = os.path.join("resources", "test")
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100148
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100149 DEFAULT_LOG_FILE_VALUES = {
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100150 "logfile_type": "last_kmsg",
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100151 "logfile": DEFAULT_LOG_FILE_FILENAMES[0],
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100152 }
153
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100154 DEFAULT_LOG_FILE_PATHS = [
155 os.path.join(DEFAULT_LOG_FILE_DIRECTORY, DEFAULT_LOG_FILE_FILENAMES[0]),
156 os.path.join(DEFAULT_LOG_FILE_DIRECTORY, DEFAULT_LOG_FILE_FILENAMES[1]),
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100157 ]
158
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200159 @staticmethod
Mitja Nikolaus1509d8f2018-12-06 11:33:17 +0100160 def _update_copy(
161 original: Dict[str, Any], update: Dict[str, Any]
162 ) -> Dict[str, Any]:
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200163 """Merge fields of update into a copy of original."""
164 data = original.copy()
165 data.update(update)
166 return data
167
168 @staticmethod
Mitja Nikolaus1509d8f2018-12-06 11:33:17 +0100169 def device_register_data(**kwargs: Any) -> Dict[str, Any]:
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200170 """Return the data required to register a device.
171
172 Use the values passed as keyword arguments or default to the ones
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100173 from `Dummy.DEFAULT_DEVICE_REGISTER_VALUES`.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200174 """
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100175 return Dummy._update_copy(Dummy.DEFAULT_DEVICE_REGISTER_VALUES, kwargs)
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200176
177 @staticmethod
Mitja Nikolaus1509d8f2018-12-06 11:33:17 +0100178 def heartbeat_data(**kwargs: Any) -> Dict[str, Any]:
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200179 """Return the data required to create a heartbeat.
180
181 Use the values passed as keyword arguments or default to the ones
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100182 from `Dummy.DEFAULT_HEARTBEAT_VALUES`.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200183 """
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100184 return Dummy._update_copy(Dummy.DEFAULT_HEARTBEAT_VALUES, kwargs)
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200185
186 @staticmethod
Mitja Nikolaus1509d8f2018-12-06 11:33:17 +0100187 def alternative_heartbeat_data(**kwargs: Any) -> Dict[str, Any]:
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100188 """Return the alternative data required to create a heartbeat.
189
190 Use the values passed as keyword arguments or default to the ones
191 from `Dummy.ALTERNATIVE_HEARTBEAT_VALUES`.
192 """
193 return Dummy._update_copy(Dummy.ALTERNATIVE_HEARTBEAT_VALUES, kwargs)
194
195 @staticmethod
Mitja Nikolaus1509d8f2018-12-06 11:33:17 +0100196 def crashreport_data(
197 report_type: Optional[str] = None, **kwargs: Any
198 ) -> Dict[str, Any]:
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200199 """Return the data required to create a crashreport.
200
201 Use the values passed as keyword arguments or default to the ones
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100202 from `Dummy.DEFAULT_CRASHREPORTS_VALUES`.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200203
204 Args:
205 report_type: A valid value from
206 `Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.keys()` that will
207 define the boot reason if not explicitly defined in the
208 keyword arguments already.
209 """
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100210 data = Dummy._update_copy(Dummy.DEFAULT_CRASHREPORT_VALUES, kwargs)
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200211 if report_type and "boot_reason" not in kwargs:
212 if report_type not in Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP:
213 raise InvalidCrashTypeError(report_type)
214 data["boot_reason"] = Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.get(
215 report_type
216 )
217 return data
218
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100219 @staticmethod
Mitja Nikolaus1509d8f2018-12-06 11:33:17 +0100220 def alternative_crashreport_data(**kwargs: Any) -> Dict[str, Any]:
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100221 """Return the alternative data required to create a crashreport.
222
223 Use the values passed as keyword arguments or default to the ones
224 from `Dummy.ALTERNATIVE_CRASHREPORT_VALUES`.
225 """
226 return Dummy._update_copy(Dummy.ALTERNATIVE_CRASHREPORT_VALUES, kwargs)
227
228 @staticmethod
Mitja Nikolaus1509d8f2018-12-06 11:33:17 +0100229 def create_user(**kwargs: Any) -> User:
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100230 """Create a dummy user instance.
231
232 The dummy instance is created and saved to the database.
233 Args:
234 **kwargs:
235 Optional arguments to extend/overwrite the default values.
236
237 Returns: The created user instance.
238
239 """
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100240 entity = User(**Dummy._update_copy(Dummy.DEFAULT_USER_VALUES, kwargs))
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100241 entity.save()
242 return entity
243
244 @staticmethod
Mitja Nikolaus1509d8f2018-12-06 11:33:17 +0100245 def create_device(user: User, **kwargs: Any) -> Device:
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100246 """Create a dummy device instance.
247
248 The dummy instance is created and saved to the database.
249 Args:
250 user: The user instance that the device should relate to
251 **kwargs:
252 Optional arguments to extend/overwrite the default values.
253
254 Returns: The created device instance.
255
256 """
257 entity = Device(
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100258 user=user, **Dummy._update_copy(Dummy.DEFAULT_DEVICE_VALUES, kwargs)
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100259 )
260 entity.save()
261 return entity
262
263 @staticmethod
Mitja Nikolaus1509d8f2018-12-06 11:33:17 +0100264 def create_report(
265 report_type: Type[Union[HeartBeat, Crashreport]],
266 device: Device,
267 **kwargs: Any
268 ) -> Union[HeartBeat, Crashreport]:
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100269 """Create a dummy report instance of the given report class type.
270
271 The dummy instance is created and saved to the database.
272 Args:
273 report_type: The class of the report type to be created.
274 user: The device instance that the heartbeat should relate to
275 **kwargs:
276 Optional arguments to extend/overwrite the default values.
277
278 Returns: The created report instance.
279
280 Raises:
281 RuntimeError: If report_type is not a report class type.
282
283 """
284 if report_type == HeartBeat:
285 entity = HeartBeat(
286 device=device,
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100287 **Dummy._update_copy(Dummy.DEFAULT_HEARTBEAT_VALUES, kwargs)
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100288 )
289 elif report_type == Crashreport:
290 entity = Crashreport(
291 device=device,
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100292 **Dummy._update_copy(Dummy.DEFAULT_CRASHREPORT_VALUES, kwargs)
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100293 )
294 else:
295 raise RuntimeError(
296 "No dummy report instance can be created for {}".format(
297 report_type.__name__
298 )
299 )
300 entity.save()
301 return entity
302
303 @staticmethod
Mitja Nikolaus1509d8f2018-12-06 11:33:17 +0100304 def create_log_file(crashreport: Crashreport, **kwargs: Any) -> LogFile:
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100305 """Create a dummy log file instance.
306
307 The dummy instance is created and saved to the database.
308
309 Args:
310 crashreport: The crashreport that the log file belongs to.
311 **kwargs: Optional arguments to extend/overwrite the default values.
312
313 Returns: The created log file instance.
314
315 """
316 entity = LogFile(
317 crashreport=crashreport,
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100318 **Dummy._update_copy(Dummy.DEFAULT_LOG_FILE_VALUES, kwargs)
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100319 )
320
321 entity.save()
322 return entity
323
324 @staticmethod
Mitja Nikolaus1509d8f2018-12-06 11:33:17 +0100325 def create_log_file_with_actual_file(
326 crashreport: Crashreport, **kwargs: Any
327 ) -> Tuple[LogFile, str]:
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100328 """Create a dummy log file instance along with a file.
329
330 The dummy instance is created and saved to the database. The log file
331 is copied to the respective location in the media directory.
332
333 Args:
334 crashreport: The crashreport that the log file belongs to.
335 **kwargs: Optional arguments to extend/overwrite the default values.
336
337 Returns: The created log file instance and the path to the copied file.
338
339 """
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100340 logfile = Dummy.create_log_file(crashreport, **kwargs)
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100341 logfile_filename = os.path.basename(logfile.logfile.path)
342 test_logfile_path = os.path.join(
343 settings.MEDIA_ROOT,
344 crashreport_file_name(logfile, logfile_filename),
345 )
346 logfile.logfile = test_logfile_path
347 logfile.save()
348
349 os.makedirs(os.path.dirname(test_logfile_path))
350 shutil.copy(
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100351 os.path.join(Dummy.DEFAULT_LOG_FILE_DIRECTORY, logfile_filename),
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100352 test_logfile_path,
353 )
354 return logfile, test_logfile_path
355
356 @staticmethod
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100357 def read_logfile_contents(path_to_zipfile, logfile_name):
358 """Read bytes of a zipped logfile."""
359 archive = zipfile.ZipFile(path_to_zipfile, "r")
360 return archive.read(logfile_name)
361
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200362
Mitja Nikolausbc03e682018-11-13 16:48:20 +0100363class HiccupCrashreportsTransactionTestCase(TransactionTestCase):
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200364 """Base class that offers a device registration method."""
365
366 REGISTER_DEVICE_URL = "api_v1_register_device"
367
368 def setUp(self):
Mitja Nikolause0e83772018-11-05 10:00:53 +0100369 """Create a Fairphone staff user for accessing the API.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200370
371 The APIClient that can be used to make authenticated requests to the
Mitja Nikolause0e83772018-11-05 10:00:53 +0100372 server is stored in self.fp_staff_client.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200373 """
Mitja Nikolause0e83772018-11-05 10:00:53 +0100374 fp_staff_group = Group.objects.get(name=FP_STAFF_GROUP_NAME)
375 fp_staff_user = User.objects.create_user(
376 "fp_staff", "somebody@fairphone.com", "thepassword"
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200377 )
Mitja Nikolause0e83772018-11-05 10:00:53 +0100378 fp_staff_user.groups.add(fp_staff_group)
379 self.fp_staff_client = APIClient()
380 self.fp_staff_client.force_login(fp_staff_user)
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200381
382 def _register_device(self, **kwargs):
383 """Register a new device.
384
385 Arguments:
386 **kwargs: The data to pass the dummy data creation
387 method `Dummy.device_register_data`.
388 Returns:
389 (UUID, APIClient, str): The uuid of the new device as well as an
390 authentication token and the associated user with credentials.
391
392 """
393 data = Dummy.device_register_data(**kwargs)
394 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
395 self.assertEqual(response.status_code, status.HTTP_200_OK)
396
397 uuid = response.data["uuid"]
398 token = response.data["token"]
399 user = APIClient()
400 user.credentials(HTTP_AUTHORIZATION="Token " + token)
401
402 return uuid, user, token
Mitja Nikolausbc03e682018-11-13 16:48:20 +0100403
404
405class HiccupCrashreportsAPITestCase(
406 HiccupCrashreportsTransactionTestCase, APITestCase
407):
408 """Base class combining device registration methods and API test methods."""
409
410 pass
411
412
413class RaceConditionsTestCase(HiccupCrashreportsTransactionTestCase):
414 """Test cases for race conditions."""
415
416 # Make data from migrations available in the test cases
417 serialized_rollback = True
418
419 def _test_create_multiple(
420 self, report_type, create_function, argslist, local_id_name
421 ):
422 """Test that no race condition occurs when creating instances."""
423 # Create multiple threads which send reports simultaneously
424 threads = []
425 for args in argslist:
426 thread = threading.Thread(target=create_function, args=args)
427 threads.append(thread)
428 thread.start()
429
430 # Wait until the threads have finished
431 for thread in threads:
432 thread.join()
433
434 # Assert that no duplicate local IDs have been assigned
435 reports = report_type.objects.all()
436 self.assertEqual(
437 reports.count(), reports.distinct(local_id_name).count()
438 )
439 self.assertEqual(reports.count(), len(argslist))