blob: 8f71cad91d87fab6ee0a9d61306b95192862fc67 [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
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
160 def _update_copy(original, update):
161 """Merge fields of update into a copy of original."""
162 data = original.copy()
163 data.update(update)
164 return data
165
166 @staticmethod
167 def device_register_data(**kwargs):
168 """Return the data required to register a device.
169
170 Use the values passed as keyword arguments or default to the ones
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100171 from `Dummy.DEFAULT_DEVICE_REGISTER_VALUES`.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200172 """
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100173 return Dummy._update_copy(Dummy.DEFAULT_DEVICE_REGISTER_VALUES, kwargs)
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200174
175 @staticmethod
176 def heartbeat_data(**kwargs):
177 """Return the data required to create a heartbeat.
178
179 Use the values passed as keyword arguments or default to the ones
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100180 from `Dummy.DEFAULT_HEARTBEAT_VALUES`.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200181 """
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100182 return Dummy._update_copy(Dummy.DEFAULT_HEARTBEAT_VALUES, kwargs)
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200183
184 @staticmethod
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100185 def alternative_heartbeat_data(**kwargs):
186 """Return the alternative data required to create a heartbeat.
187
188 Use the values passed as keyword arguments or default to the ones
189 from `Dummy.ALTERNATIVE_HEARTBEAT_VALUES`.
190 """
191 return Dummy._update_copy(Dummy.ALTERNATIVE_HEARTBEAT_VALUES, kwargs)
192
193 @staticmethod
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200194 def crashreport_data(report_type: Optional[str] = None, **kwargs):
195 """Return the data required to create a crashreport.
196
197 Use the values passed as keyword arguments or default to the ones
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100198 from `Dummy.DEFAULT_CRASHREPORTS_VALUES`.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200199
200 Args:
201 report_type: A valid value from
202 `Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.keys()` that will
203 define the boot reason if not explicitly defined in the
204 keyword arguments already.
205 """
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100206 data = Dummy._update_copy(Dummy.DEFAULT_CRASHREPORT_VALUES, kwargs)
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200207 if report_type and "boot_reason" not in kwargs:
208 if report_type not in Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP:
209 raise InvalidCrashTypeError(report_type)
210 data["boot_reason"] = Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.get(
211 report_type
212 )
213 return data
214
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100215 @staticmethod
Mitja Nikolausfd452f82018-11-07 11:53:59 +0100216 def alternative_crashreport_data(**kwargs):
217 """Return the alternative data required to create a crashreport.
218
219 Use the values passed as keyword arguments or default to the ones
220 from `Dummy.ALTERNATIVE_CRASHREPORT_VALUES`.
221 """
222 return Dummy._update_copy(Dummy.ALTERNATIVE_CRASHREPORT_VALUES, kwargs)
223
224 @staticmethod
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100225 def create_user(**kwargs):
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100226 """Create a dummy user instance.
227
228 The dummy instance is created and saved to the database.
229 Args:
230 **kwargs:
231 Optional arguments to extend/overwrite the default values.
232
233 Returns: The created user instance.
234
235 """
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100236 entity = User(**Dummy._update_copy(Dummy.DEFAULT_USER_VALUES, kwargs))
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100237 entity.save()
238 return entity
239
240 @staticmethod
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100241 def create_device(user, **kwargs):
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100242 """Create a dummy device instance.
243
244 The dummy instance is created and saved to the database.
245 Args:
246 user: The user instance that the device should relate to
247 **kwargs:
248 Optional arguments to extend/overwrite the default values.
249
250 Returns: The created device instance.
251
252 """
253 entity = Device(
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100254 user=user, **Dummy._update_copy(Dummy.DEFAULT_DEVICE_VALUES, kwargs)
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100255 )
256 entity.save()
257 return entity
258
259 @staticmethod
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100260 def create_report(report_type, device, **kwargs):
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100261 """Create a dummy report instance of the given report class type.
262
263 The dummy instance is created and saved to the database.
264 Args:
265 report_type: The class of the report type to be created.
266 user: The device instance that the heartbeat should relate to
267 **kwargs:
268 Optional arguments to extend/overwrite the default values.
269
270 Returns: The created report instance.
271
272 Raises:
273 RuntimeError: If report_type is not a report class type.
274
275 """
276 if report_type == HeartBeat:
277 entity = HeartBeat(
278 device=device,
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100279 **Dummy._update_copy(Dummy.DEFAULT_HEARTBEAT_VALUES, kwargs)
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100280 )
281 elif report_type == Crashreport:
282 entity = Crashreport(
283 device=device,
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100284 **Dummy._update_copy(Dummy.DEFAULT_CRASHREPORT_VALUES, kwargs)
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100285 )
286 else:
287 raise RuntimeError(
288 "No dummy report instance can be created for {}".format(
289 report_type.__name__
290 )
291 )
292 entity.save()
293 return entity
294
295 @staticmethod
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100296 def create_log_file(crashreport, **kwargs):
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100297 """Create a dummy log file instance.
298
299 The dummy instance is created and saved to the database.
300
301 Args:
302 crashreport: The crashreport that the log file belongs to.
303 **kwargs: Optional arguments to extend/overwrite the default values.
304
305 Returns: The created log file instance.
306
307 """
308 entity = LogFile(
309 crashreport=crashreport,
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100310 **Dummy._update_copy(Dummy.DEFAULT_LOG_FILE_VALUES, kwargs)
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100311 )
312
313 entity.save()
314 return entity
315
316 @staticmethod
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100317 def create_log_file_with_actual_file(crashreport, **kwargs):
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100318 """Create a dummy log file instance along with a file.
319
320 The dummy instance is created and saved to the database. The log file
321 is copied to the respective location in the media directory.
322
323 Args:
324 crashreport: The crashreport that the log file belongs to.
325 **kwargs: Optional arguments to extend/overwrite the default values.
326
327 Returns: The created log file instance and the path to the copied file.
328
329 """
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100330 logfile = Dummy.create_log_file(crashreport, **kwargs)
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100331 logfile_filename = os.path.basename(logfile.logfile.path)
332 test_logfile_path = os.path.join(
333 settings.MEDIA_ROOT,
334 crashreport_file_name(logfile, logfile_filename),
335 )
336 logfile.logfile = test_logfile_path
337 logfile.save()
338
339 os.makedirs(os.path.dirname(test_logfile_path))
340 shutil.copy(
Mitja Nikolaus77dd5652018-12-06 11:27:01 +0100341 os.path.join(Dummy.DEFAULT_LOG_FILE_DIRECTORY, logfile_filename),
Mitja Nikolauscc90d572018-11-22 16:40:15 +0100342 test_logfile_path,
343 )
344 return logfile, test_logfile_path
345
346 @staticmethod
Mitja Nikolaus7dc86722018-11-27 14:57:39 +0100347 def read_logfile_contents(path_to_zipfile, logfile_name):
348 """Read bytes of a zipped logfile."""
349 archive = zipfile.ZipFile(path_to_zipfile, "r")
350 return archive.read(logfile_name)
351
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200352
Mitja Nikolausbc03e682018-11-13 16:48:20 +0100353class HiccupCrashreportsTransactionTestCase(TransactionTestCase):
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200354 """Base class that offers a device registration method."""
355
356 REGISTER_DEVICE_URL = "api_v1_register_device"
357
358 def setUp(self):
Mitja Nikolause0e83772018-11-05 10:00:53 +0100359 """Create a Fairphone staff user for accessing the API.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200360
361 The APIClient that can be used to make authenticated requests to the
Mitja Nikolause0e83772018-11-05 10:00:53 +0100362 server is stored in self.fp_staff_client.
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200363 """
Mitja Nikolause0e83772018-11-05 10:00:53 +0100364 fp_staff_group = Group.objects.get(name=FP_STAFF_GROUP_NAME)
365 fp_staff_user = User.objects.create_user(
366 "fp_staff", "somebody@fairphone.com", "thepassword"
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200367 )
Mitja Nikolause0e83772018-11-05 10:00:53 +0100368 fp_staff_user.groups.add(fp_staff_group)
369 self.fp_staff_client = APIClient()
370 self.fp_staff_client.force_login(fp_staff_user)
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200371
372 def _register_device(self, **kwargs):
373 """Register a new device.
374
375 Arguments:
376 **kwargs: The data to pass the dummy data creation
377 method `Dummy.device_register_data`.
378 Returns:
379 (UUID, APIClient, str): The uuid of the new device as well as an
380 authentication token and the associated user with credentials.
381
382 """
383 data = Dummy.device_register_data(**kwargs)
384 response = self.client.post(reverse(self.REGISTER_DEVICE_URL), data)
385 self.assertEqual(response.status_code, status.HTTP_200_OK)
386
387 uuid = response.data["uuid"]
388 token = response.data["token"]
389 user = APIClient()
390 user.credentials(HTTP_AUTHORIZATION="Token " + token)
391
392 return uuid, user, token
Mitja Nikolausbc03e682018-11-13 16:48:20 +0100393
394
395class HiccupCrashreportsAPITestCase(
396 HiccupCrashreportsTransactionTestCase, APITestCase
397):
398 """Base class combining device registration methods and API test methods."""
399
400 pass
401
402
403class RaceConditionsTestCase(HiccupCrashreportsTransactionTestCase):
404 """Test cases for race conditions."""
405
406 # Make data from migrations available in the test cases
407 serialized_rollback = True
408
409 def _test_create_multiple(
410 self, report_type, create_function, argslist, local_id_name
411 ):
412 """Test that no race condition occurs when creating instances."""
413 # Create multiple threads which send reports simultaneously
414 threads = []
415 for args in argslist:
416 thread = threading.Thread(target=create_function, args=args)
417 threads.append(thread)
418 thread.start()
419
420 # Wait until the threads have finished
421 for thread in threads:
422 thread.join()
423
424 # Assert that no duplicate local IDs have been assigned
425 reports = report_type.objects.all()
426 self.assertEqual(
427 reports.count(), reports.distinct(local_id_name).count()
428 )
429 self.assertEqual(reports.count(), len(argslist))