blob: e9139df43e8b0565c3d5ff143cc9486e26444ad9 [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 Nikolaus03e412b2018-09-18 17:50:15 +02008from typing import Optional
9
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
82 DEFAULT_DUMMY_USER_VALUES = {"username": USERNAMES[0]}
83
Mitja Nikolaus03e412b2018-09-18 17:50:15 +020084 DEFAULT_DUMMY_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 Nikolaus7dc86722018-11-27 14:57:39 +010089 DEFAULT_DUMMY_DEVICE_VALUES = DEFAULT_DUMMY_DEVICE_REGISTER_VALUES.copy()
90 DEFAULT_DUMMY_DEVICE_VALUES.update(
91 {"token": "64111c62d521fb4724454ca6dea27e18f93ef56e"}
92 )
93
Mitja Nikolaus03e412b2018-09-18 17:50:15 +020094 DEFAULT_DUMMY_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 Nikolaus7dc86722018-11-27 14:57:39 +0100116 DEFAULT_DUMMY_CRASHREPORT_VALUES = DEFAULT_DUMMY_HEARTBEAT_VALUES.copy()
117 DEFAULT_DUMMY_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 Nikolaus7dc86722018-11-27 14:57:39 +0100138 DEFAULT_DUMMY_LOG_FILE_NAME = "dmesg.log"
139
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 Nikolausfd452f82018-11-07 11:53:59 +0100146 DEFAULT_DUMMY_LOG_FILE_FILENAMES = [
147 "test_logfile_1.zip",
148 "test_logfile_2.zip",
149 ]
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100150 DEFAULT_DUMMY_LOG_FILE_DIRECTORY = os.path.join("resources", "test")
151
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100152 DEFAULT_DUMMY_LOG_FILE_VALUES = {
153 "logfile_type": "last_kmsg",
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100154 "logfile": DEFAULT_DUMMY_LOG_FILE_FILENAMES[0],
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100155 }
156
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100157 DEFAULT_DUMMY_LOG_FILE_PATHS = [
158 os.path.join(
159 DEFAULT_DUMMY_LOG_FILE_DIRECTORY,
160 DEFAULT_DUMMY_LOG_FILE_FILENAMES[0],
161 ),
162 os.path.join(
163 DEFAULT_DUMMY_LOG_FILE_DIRECTORY,
164 DEFAULT_DUMMY_LOG_FILE_FILENAMES[1],
165 ),
166 ]
167
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200168 @staticmethod
169 def _update_copy(original, update):
170 """Merge fields of update into a copy of original."""
171 data = original.copy()
172 data.update(update)
173 return data
174
175 @staticmethod
176 def device_register_data(**kwargs):
177 """Return the data required to register a device.
178
179 Use the values passed as keyword arguments or default to the ones
180 from `Dummy.DEFAULT_DUMMY_DEVICE_REGISTER_VALUES`.
181 """
182 return Dummy._update_copy(
183 Dummy.DEFAULT_DUMMY_DEVICE_REGISTER_VALUES, kwargs
184 )
185
186 @staticmethod
187 def heartbeat_data(**kwargs):
188 """Return the data required to create a heartbeat.
189
190 Use the values passed as keyword arguments or default to the ones
191 from `Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES`.
192 """
193 return Dummy._update_copy(Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs)
194
195 @staticmethod
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100196 def alternative_heartbeat_data(**kwargs):
197 """Return the alternative data required to create a heartbeat.
198
199 Use the values passed as keyword arguments or default to the ones
200 from `Dummy.ALTERNATIVE_HEARTBEAT_VALUES`.
201 """
202 return Dummy._update_copy(Dummy.ALTERNATIVE_HEARTBEAT_VALUES, kwargs)
203
204 @staticmethod
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200205 def crashreport_data(report_type: Optional[str] = None, **kwargs):
206 """Return the data required to create a crashreport.
207
208 Use the values passed as keyword arguments or default to the ones
209 from `Dummy.DEFAULT_DUMMY_CRASHREPORTS_VALUES`.
210
211 Args:
212 report_type: A valid value from
213 `Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.keys()` that will
214 define the boot reason if not explicitly defined in the
215 keyword arguments already.
216 """
217 data = Dummy._update_copy(
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100218 Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES, kwargs
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200219 )
220 if report_type and "boot_reason" not in kwargs:
221 if report_type not in Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP:
222 raise InvalidCrashTypeError(report_type)
223 data["boot_reason"] = Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.get(
224 report_type
225 )
226 return data
227
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100228 @staticmethod
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100229 def alternative_crashreport_data(**kwargs):
230 """Return the alternative data required to create a crashreport.
231
232 Use the values passed as keyword arguments or default to the ones
233 from `Dummy.ALTERNATIVE_CRASHREPORT_VALUES`.
234 """
235 return Dummy._update_copy(Dummy.ALTERNATIVE_CRASHREPORT_VALUES, kwargs)
236
237 @staticmethod
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100238 def create_dummy_user(**kwargs):
239 """Create a dummy user instance.
240
241 The dummy instance is created and saved to the database.
242 Args:
243 **kwargs:
244 Optional arguments to extend/overwrite the default values.
245
246 Returns: The created user instance.
247
248 """
249 entity = User(
250 **Dummy._update_copy(Dummy.DEFAULT_DUMMY_USER_VALUES, kwargs)
251 )
252 entity.save()
253 return entity
254
255 @staticmethod
256 def create_dummy_device(user, **kwargs):
257 """Create a dummy device instance.
258
259 The dummy instance is created and saved to the database.
260 Args:
261 user: The user instance that the device should relate to
262 **kwargs:
263 Optional arguments to extend/overwrite the default values.
264
265 Returns: The created device instance.
266
267 """
268 entity = Device(
269 user=user,
270 **Dummy._update_copy(Dummy.DEFAULT_DUMMY_DEVICE_VALUES, kwargs)
271 )
272 entity.save()
273 return entity
274
275 @staticmethod
276 def create_dummy_report(report_type, device, **kwargs):
277 """Create a dummy report instance of the given report class type.
278
279 The dummy instance is created and saved to the database.
280 Args:
281 report_type: The class of the report type to be created.
282 user: The device instance that the heartbeat should relate to
283 **kwargs:
284 Optional arguments to extend/overwrite the default values.
285
286 Returns: The created report instance.
287
288 Raises:
289 RuntimeError: If report_type is not a report class type.
290
291 """
292 if report_type == HeartBeat:
293 entity = HeartBeat(
294 device=device,
295 **Dummy._update_copy(
296 Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs
297 )
298 )
299 elif report_type == Crashreport:
300 entity = Crashreport(
301 device=device,
302 **Dummy._update_copy(
303 Dummy.DEFAULT_DUMMY_CRASHREPORT_VALUES, kwargs
304 )
305 )
306 else:
307 raise RuntimeError(
308 "No dummy report instance can be created for {}".format(
309 report_type.__name__
310 )
311 )
312 entity.save()
313 return entity
314
315 @staticmethod
316 def create_dummy_log_file(crashreport, **kwargs):
317 """Create a dummy log file instance.
318
319 The dummy instance is created and saved to the database.
320
321 Args:
322 crashreport: The crashreport that the log file belongs to.
323 **kwargs: Optional arguments to extend/overwrite the default values.
324
325 Returns: The created log file instance.
326
327 """
328 entity = LogFile(
329 crashreport=crashreport,
330 **Dummy._update_copy(Dummy.DEFAULT_DUMMY_LOG_FILE_VALUES, kwargs)
331 )
332
333 entity.save()
334 return entity
335
336 @staticmethod
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100337 def create_dummy_log_file_with_actual_file(crashreport, **kwargs):
338 """Create a dummy log file instance along with a file.
339
340 The dummy instance is created and saved to the database. The log file
341 is copied to the respective location in the media directory.
342
343 Args:
344 crashreport: The crashreport that the log file belongs to.
345 **kwargs: Optional arguments to extend/overwrite the default values.
346
347 Returns: The created log file instance and the path to the copied file.
348
349 """
350 logfile = Dummy.create_dummy_log_file(crashreport, **kwargs)
351 logfile_filename = os.path.basename(logfile.logfile.path)
352 test_logfile_path = os.path.join(
353 settings.MEDIA_ROOT,
354 crashreport_file_name(logfile, logfile_filename),
355 )
356 logfile.logfile = test_logfile_path
357 logfile.save()
358
359 os.makedirs(os.path.dirname(test_logfile_path))
360 shutil.copy(
361 os.path.join(
362 Dummy.DEFAULT_DUMMY_LOG_FILE_DIRECTORY, logfile_filename
363 ),
364 test_logfile_path,
365 )
366 return logfile, test_logfile_path
367
368 @staticmethod
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100369 def read_logfile_contents(path_to_zipfile, logfile_name):
370 """Read bytes of a zipped logfile."""
371 archive = zipfile.ZipFile(path_to_zipfile, "r")
372 return archive.read(logfile_name)
373
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200374
Mitja Nikolausbc03e682018-11-13 16:48:20 +0100375class HiccupCrashreportsTransactionTestCase(TransactionTestCase):
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200376 """Base class that offers a device registration method."""
377
378 REGISTER_DEVICE_URL = "api_v1_register_device"
379
380 def setUp(self):
Mitja Nikolause0e83772018-11-05 10:00:53 +0100381 """Create a Fairphone staff user for accessing the API.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200382
383 The APIClient that can be used to make authenticated requests to the
Mitja Nikolause0e83772018-11-05 10:00:53 +0100384 server is stored in self.fp_staff_client.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200385 """
Mitja Nikolause0e83772018-11-05 10:00:53 +0100386 fp_staff_group = Group.objects.get(name=FP_STAFF_GROUP_NAME)
387 fp_staff_user = User.objects.create_user(
388 "fp_staff", "somebody@fairphone.com", "thepassword"
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200389 )
Mitja Nikolause0e83772018-11-05 10:00:53 +0100390 fp_staff_user.groups.add(fp_staff_group)
391 self.fp_staff_client = APIClient()
392 self.fp_staff_client.force_login(fp_staff_user)
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200393
394 def _register_device(self, **kwargs):
395 """Register a new device.
396
397 Arguments:
398 **kwargs: The data to pass the dummy data creation
399 method `Dummy.device_register_data`.
400 Returns:
401 (UUID, APIClient, str): The uuid of the new device as well as an
402 authentication token and the associated user with credentials.
403
404 """
405 data = Dummy.device_register_data(**kwargs)
406 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
407 self.assertEqual(response.status_code, status.HTTP_200_OK)
408
409 uuid = response.data["uuid"]
410 token = response.data["token"]
411 user = APIClient()
412 user.credentials(HTTP_AUTHORIZATION="Token " + token)
413
414 return uuid, user, token
Mitja Nikolausbc03e682018-11-13 16:48:20 +0100415
416
417class HiccupCrashreportsAPITestCase(
418 HiccupCrashreportsTransactionTestCase, APITestCase
419):
420 """Base class combining device registration methods and API test methods."""
421
422 pass
423
424
425class RaceConditionsTestCase(HiccupCrashreportsTransactionTestCase):
426 """Test cases for race conditions."""
427
428 # Make data from migrations available in the test cases
429 serialized_rollback = True
430
431 def _test_create_multiple(
432 self, report_type, create_function, argslist, local_id_name
433 ):
434 """Test that no race condition occurs when creating instances."""
435 # Create multiple threads which send reports simultaneously
436 threads = []
437 for args in argslist:
438 thread = threading.Thread(target=create_function, args=args)
439 threads.append(thread)
440 thread.start()
441
442 # Wait until the threads have finished
443 for thread in threads:
444 thread.join()
445
446 # Assert that no duplicate local IDs have been assigned
447 reports = report_type.objects.all()
448 self.assertEqual(
449 reports.count(), reports.distinct(local_id_name).count()
450 )
451 self.assertEqual(reports.count(), len(argslist))