blob: 51860e92b9c277f39dfed9ce8fd4e244774eb951 [file] [log] [blame]
Mitja Nikolaus03e412b2018-09-18 17:50:15 +02001"""Tests for the stats management command module."""
2
3from io import StringIO
4from datetime import datetime, timedelta
5import unittest
6
7import pytz
8
9from django.core.management import call_command
10from django.test import TestCase
11
12from crashreport_stats.models import (
13 Version,
14 VersionDaily,
15 RadioVersion,
16 RadioVersionDaily,
17 StatsMetadata,
18)
19from crashreport_stats.tests.utils import Dummy
20
21from crashreports.models import Crashreport, HeartBeat
22
23# pylint: disable=too-many-public-methods
24
25
26class StatsCommandVersionsTestCase(TestCase):
27 """Test the generation of Version stats with the stats command."""
28
29 # The class of the version type to be tested
30 version_class = Version
31 # The attribute name characterising the unicity of a stats entry (the
32 # named identifier)
33 unique_entry_name = "build_fingerprint"
34 # The collection of unique entries to post
35 unique_entries = Dummy.BUILD_FINGERPRINTS
36
37 def _create_reports(
38 self, report_type, unique_entry_name, device, number, **kwargs
39 ):
40 # Create reports with distinct timestamps
41 now = datetime.now(pytz.utc)
42 for i in range(number):
43 report_date = now - timedelta(milliseconds=i)
44 report_attributes = {
45 self.unique_entry_name: unique_entry_name,
46 "device": device,
47 "date": report_date,
48 }
49 report_attributes.update(**kwargs)
50 Dummy.create_dummy_report(report_type, **report_attributes)
51
52 def test_stats_calculation(self):
53 """Test generation of a Version instance."""
54 user = Dummy.create_dummy_user()
55 device = Dummy.create_dummy_device(user=user)
56 heartbeat = Dummy.create_dummy_report(HeartBeat, device=device)
57
58 # Expect that we do not have the Version before updating the stats
59 get_params = {
60 self.unique_entry_name: getattr(heartbeat, self.unique_entry_name)
61 }
62 self.assertRaises(
63 self.version_class.DoesNotExist,
64 self.version_class.objects.get,
65 **get_params
66 )
67
68 # Run the command to update the database
69 call_command("stats", "update")
70
71 # Assume that a corresponding Version instance has been created
72 version = self.version_class.objects.get(**get_params)
73 self.assertIsNotNone(version)
74
75 def _assert_older_report_updates_version_date(self, report_type):
76 """Validate that older reports sent later affect the version date."""
77 user = Dummy.create_dummy_user()
78 device = Dummy.create_dummy_device(user=user)
79 report = Dummy.create_dummy_report(report_type, device=device)
80
81 # Run the command to update the database
82 call_command("stats", "update")
83
84 get_params = {
85 self.unique_entry_name: getattr(report, self.unique_entry_name)
86 }
87 version = self.version_class.objects.get(**get_params)
88
89 self.assertEqual(report.date.date(), version.first_seen_on)
90
91 # Create a new report from an earlier point in time
92 report_time_2 = report.date - timedelta(weeks=1)
93 Dummy.create_dummy_report(
94 report_type, device=device, date=report_time_2
95 )
96
97 # Run the command to update the database
98 call_command("stats", "update")
99
100 # Get the same version object from before
101 version = self.version_class.objects.get(**get_params)
102
103 # Validate that the date matches the report recently sent
104 self.assertEqual(report_time_2.date(), version.first_seen_on)
105
106 def test_older_heartbeat_updates_version_date(self):
107 """Validate updating version date with older heartbeats."""
108 self._assert_older_report_updates_version_date(HeartBeat)
109
110 def test_older_crash_report_updates_version_date(self):
111 """Validate updating version date with older crash reports."""
112 self._assert_older_report_updates_version_date(Crashreport)
113
114 def test_entries_are_unique(self):
115 """Validate the entries' unicity and value."""
116 # Create some reports
117 user = Dummy.create_dummy_user()
118 device = Dummy.create_dummy_device(user=user)
119 for unique_entry in self.unique_entries:
120 self._create_reports(HeartBeat, unique_entry, device, 10)
121
122 # Run the command to update the database
123 call_command("stats", "update")
124
125 # Check whether the correct amount of distinct versions have been
126 # created
127 versions = self.version_class.objects.all()
128 for version in versions:
129 self.assertIn(
130 getattr(version, self.unique_entry_name), self.unique_entries
131 )
132 self.assertEqual(len(versions), len(self.unique_entries))
133
134 def _assert_counter_distribution_is_correct(
135 self, report_type, numbers, counter_attribute_name, **kwargs
136 ):
137 """Validate a counter distribution in the database."""
138 if len(numbers) != len(self.unique_entries):
139 raise ValueError(
140 "The length of the numbers list must match the "
141 "length of self.unique_entries in the test class"
142 "({} != {})".format(len(numbers), len(self.unique_entries))
143 )
144 # Create some reports
145 user = Dummy.create_dummy_user()
146 device = Dummy.create_dummy_device(user=user)
147 for unique_entry, num in zip(self.unique_entries, numbers):
148 self._create_reports(
149 report_type, unique_entry, device, num, **kwargs
150 )
151
152 # Run the command to update the database
153 call_command("stats", "update")
154
155 # Check whether the numbers of reports match
156 for version in self.version_class.objects.all():
157 unique_entry_name = getattr(version, self.unique_entry_name)
158 num = numbers[self.unique_entries.index(unique_entry_name)]
159 self.assertEqual(num, getattr(version, counter_attribute_name))
160
161 def test_heartbeats_counter(self):
162 """Test the calculation of the heartbeats counter."""
163 numbers = [10, 7, 8, 5]
164 counter_attribute_name = "heartbeats"
165 self._assert_counter_distribution_is_correct(
166 HeartBeat, numbers, counter_attribute_name
167 )
168
169 def test_crash_reports_counter(self):
170 """Test the calculation of the crashreports counter."""
171 numbers = [2, 5, 0, 3]
172 counter_attribute_name = "prob_crashes"
173 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_UNKOWN}
174 self._assert_counter_distribution_is_correct(
175 Crashreport, numbers, counter_attribute_name, **boot_reason_param
176 )
177
178 def test_smpl_reports_counter(self):
179 """Test the calculation of the smpl reports counter."""
180 numbers = [1, 3, 4, 0]
181 counter_attribute_name = "smpl"
182 boot_reason_param = {"boot_reason": Crashreport.BOOT_REASON_RTC_ALARM}
183 self._assert_counter_distribution_is_correct(
184 Crashreport, numbers, counter_attribute_name, **boot_reason_param
185 )
186
187 def test_other_reports_counter(self):
188 """Test the calculation of the other reports counter."""
189 numbers = [0, 2, 1, 2]
190 counter_attribute_name = "other"
191 boot_reason_param = {"boot_reason": "random boot reason"}
192 self._assert_counter_distribution_is_correct(
193 Crashreport, numbers, counter_attribute_name, **boot_reason_param
194 )
195
196 def _assert_reports_with_same_timestamp_are_counted(
197 self, report_type, counter_attribute_name, **kwargs
198 ):
199 """Validate that reports with the same timestamp are counted.
200
201 Reports from different devices but the same timestamp should be
202 counted as independent reports.
203 """
204 # Create a report
205 device1 = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
206 report1 = Dummy.create_dummy_report(
207 report_type, device=device1, **kwargs
208 )
209
210 # Create a second report with the same timestamp but from another device
211 device2 = Dummy.create_dummy_device(
212 user=Dummy.create_dummy_user(username=Dummy.USERNAMES[1])
213 )
214 Dummy.create_dummy_report(
215 report_type, device=device2, date=report1.date, **kwargs
216 )
217
218 # Run the command to update the database
219 call_command("stats", "update")
220
221 # Get the corresponding version instance from the database
222 get_params = {
223 self.unique_entry_name: getattr(report1, self.unique_entry_name)
224 }
225 version = self.version_class.objects.get(**get_params)
226
227 # Assert that both reports are counted
228 self.assertEqual(getattr(version, counter_attribute_name), 2)
229
230 @unittest.skip(
231 "Duplicates are dropped based on their timestamp at the moment. This is"
232 "to be adapted so that they are dropped taking into account the device"
233 "UUID as well."
234 )
235 def test_heartbeats_with_same_timestamp_are_counted(self):
236 """Validate that heartbeats with same timestamp are counted."""
237 counter_attribute_name = "heartbeats"
238 self._assert_reports_with_same_timestamp_are_counted(
239 HeartBeat, counter_attribute_name
240 )
241
242 @unittest.skip(
243 "Duplicates are dropped based on their timestamp at the moment. This is"
244 "to be adapted so that they are dropped taking into account the device"
245 "UUID as well."
246 )
247 def test_crash_reports_with_same_timestamp_are_counted(self):
248 """Validate that crash report duplicates are ignored."""
249 counter_attribute_name = "prob_crashes"
250 for unique_entry, boot_reason in zip(
251 self.unique_entries, Crashreport.CRASH_BOOT_REASONS
252 ):
253 params = {
254 "boot_reason": boot_reason,
255 self.unique_entry_name: unique_entry,
256 }
257 self._assert_reports_with_same_timestamp_are_counted(
258 Crashreport, counter_attribute_name, **params
259 )
260
261 @unittest.skip(
262 "Duplicates are dropped based on their timestamp at the moment. This is"
263 "to be adapted so that they are dropped taking into account the device"
264 "UUID as well."
265 )
266 def test_smpl_reports_with_same_timestamp_are_counted(self):
267 """Validate that smpl report duplicates are ignored."""
268 counter_attribute_name = "smpl"
269 for unique_entry, boot_reason in zip(
270 self.unique_entries, Crashreport.SMPL_BOOT_REASONS
271 ):
272 params = {
273 "boot_reason": boot_reason,
274 self.unique_entry_name: unique_entry,
275 }
276 self._assert_reports_with_same_timestamp_are_counted(
277 Crashreport, counter_attribute_name, **params
278 )
279
280 @unittest.skip(
281 "Duplicates are dropped based on their timestamp at the moment. This is"
282 "to be adapted so that they are dropped taking into account the device"
283 "UUID as well."
284 )
285 def test_other_reports_with_same_timestamp_are_counted(self):
286 """Validate that other report duplicates are ignored."""
287 counter_attribute_name = "other"
288 params = {"boot_reason": "random boot reason"}
289 self._assert_reports_with_same_timestamp_are_counted(
290 Crashreport, counter_attribute_name, **params
291 )
292
293 def _assert_duplicates_are_ignored(
294 self, report_type, device, counter_attribute_name, **kwargs
295 ):
296 """Validate that reports with duplicate timestamps are ignored."""
297 # Create a report
298 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
299
300 # Create a second report with the same timestamp
301 Dummy.create_dummy_report(
302 report_type, device=device, date=report.date, **kwargs
303 )
304
305 # Run the command to update the database
306 call_command("stats", "update")
307
308 # Get the corresponding version instance from the database
309 get_params = {
310 self.unique_entry_name: getattr(report, self.unique_entry_name)
311 }
312 version = self.version_class.objects.get(**get_params)
313
314 # Assert that the report with the duplicate timestamp is not
315 # counted, i.e. only 1 report is counted.
316 self.assertEqual(getattr(version, counter_attribute_name), 1)
317
318 def test_heartbeat_duplicates_are_ignored(self):
319 """Validate that heartbeat duplicates are ignored."""
320 counter_attribute_name = "heartbeats"
321 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
322 self._assert_duplicates_are_ignored(
323 HeartBeat, device, counter_attribute_name
324 )
325
326 def test_crash_report_duplicates_are_ignored(self):
327 """Validate that crash report duplicates are ignored."""
328 counter_attribute_name = "prob_crashes"
329 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
330 for i, boot_reason in enumerate(Crashreport.CRASH_BOOT_REASONS):
331 params = {
332 "boot_reason": boot_reason,
333 self.unique_entry_name: self.unique_entries[i],
334 }
335 self._assert_duplicates_are_ignored(
336 Crashreport, device, counter_attribute_name, **params
337 )
338
339 def test_smpl_report_duplicates_are_ignored(self):
340 """Validate that smpl report duplicates are ignored."""
341 counter_attribute_name = "smpl"
342 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
343 for i, boot_reason in enumerate(Crashreport.SMPL_BOOT_REASONS):
344 params = {
345 "boot_reason": boot_reason,
346 self.unique_entry_name: self.unique_entries[i],
347 }
348 self._assert_duplicates_are_ignored(
349 Crashreport, device, counter_attribute_name, **params
350 )
351
352 def test_other_report_duplicates_are_ignored(self):
353 """Validate that other report duplicates are ignored."""
354 counter_attribute_name = "other"
355 params = {"boot_reason": "random boot reason"}
356 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
357 self._assert_duplicates_are_ignored(
358 Crashreport, device, counter_attribute_name, **params
359 )
360
361 def _assert_older_reports_update_released_on_date(
362 self, report_type, **kwargs
363 ):
364 """Test updating of the released_on date.
365
366 Validate that the released_on date is updated once an older report is
367 sent.
368 """
369 # Create a report
370 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
371 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
372
373 # Run the command to update the database
374 call_command("stats", "update")
375
376 # Get the corresponding version instance from the database
377 version = self.version_class.objects.get(
378 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
379 )
380
381 # Assert that the released_on date matches the first report date
382 self.assertEqual(version.released_on, report.date.date())
383
384 # Create a second report with the a timestamp earlier in time
385 report_2_date = report.date - timedelta(days=1)
386 Dummy.create_dummy_report(
387 report_type, device=device, date=report_2_date, **kwargs
388 )
389
390 # Run the command to update the database
391 call_command("stats", "update")
392
393 # Get the corresponding version instance from the database
394 version = self.version_class.objects.get(
395 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
396 )
397
398 # Assert that the released_on date matches the older report date
399 self.assertEqual(version.released_on, report_2_date.date())
400
401 def _assert_newer_reports_do_not_update_released_on_date(
402 self, report_type, **kwargs
403 ):
404 """Test updating of the released_on date.
405
406 Validate that the released_on date is not updated once a newer report is
407 sent.
408 """
409 # Create a report
410 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
411 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
412 report_1_date = report.date.date()
413
414 # Run the command to update the database
415 call_command("stats", "update")
416
417 # Get the corresponding version instance from the database
418 version = self.version_class.objects.get(
419 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
420 )
421
422 # Assert that the released_on date matches the first report date
423 self.assertEqual(version.released_on, report_1_date)
424
425 # Create a second report with the a timestamp later in time
426 report_2_date = report.date + timedelta(days=1)
427 Dummy.create_dummy_report(
428 report_type, device=device, date=report_2_date, **kwargs
429 )
430
431 # Run the command to update the database
432 call_command("stats", "update")
433
434 # Get the corresponding version instance from the database
435 version = self.version_class.objects.get(
436 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
437 )
438
439 # Assert that the released_on date matches the older report date
440 self.assertEqual(version.released_on, report_1_date)
441
442 def test_older_heartbeat_updates_released_on_date(self):
443 """Validate that older heartbeats update the release date."""
444 self._assert_older_reports_update_released_on_date(HeartBeat)
445
446 def test_older_crash_report_updates_released_on_date(self):
447 """Validate that older crash reports update the release date."""
448 self._assert_older_reports_update_released_on_date(Crashreport)
449
450 def test_newer_heartbeat_does_not_update_released_on_date(self):
451 """Validate that newer heartbeats don't update the release date."""
452 self._assert_newer_reports_do_not_update_released_on_date(HeartBeat)
453
454 def test_newer_crash_report_does_not_update_released_on_date(self):
455 """Validate that newer crash reports don't update the release date."""
456 self._assert_newer_reports_do_not_update_released_on_date(Crashreport)
457
458 def _assert_manually_changed_released_on_date_is_not_updated(
459 self, report_type, **kwargs
460 ):
461 """Test updating of manually changed released_on dates.
462
463 Validate that a manually changed released_on date is not updated when
464 new reports are sent.
465 """
466 # Create a report
467 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
468 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
469
470 # Run the command to update the database
471 call_command("stats", "update")
472
473 # Get the corresponding version instance from the database
474 version = self.version_class.objects.get(
475 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
476 )
477
478 # Assert that the released_on date matches the first report date
479 self.assertEqual(version.released_on, report.date.date())
480
481 # Create a second report with a timestamp earlier in time
482 report_2_date = report.date - timedelta(days=1)
483 Dummy.create_dummy_report(
484 report_type, device=device, date=report_2_date, **kwargs
485 )
486
487 # Manually change the released_on date
488 version_release_date = report.date + timedelta(days=1)
489 version.released_on = version_release_date
490 version.save()
491
492 # Run the command to update the database
493 call_command("stats", "update")
494
495 # Get the corresponding version instance from the database
496 version = self.version_class.objects.get(
497 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
498 )
499
500 # Assert that the released_on date still matches the date is was
501 # manually changed to
502 self.assertEqual(version.released_on, version_release_date.date())
503
504 def test_manually_changed_released_on_date_is_not_updated_by_heartbeat(
505 self
506 ):
507 """Test update of manually changed released_on date with heartbeat."""
508 self._assert_manually_changed_released_on_date_is_not_updated(HeartBeat)
509
510 def test_manually_changed_released_on_date_is_not_updated_by_crash_report(
511 self
512 ):
513 """Test update of manually changed released_on date with crashreport."""
514 self._assert_manually_changed_released_on_date_is_not_updated(
515 Crashreport
516 )
517
518
519# pylint: disable=too-many-ancestors
520class StatsCommandRadioVersionsTestCase(StatsCommandVersionsTestCase):
521 """Test the generation of RadioVersion stats with the stats command."""
522
523 version_class = RadioVersion
524 unique_entry_name = "radio_version"
525 unique_entries = Dummy.RADIO_VERSIONS
526
527
528class CommandDebugOutputTestCase(TestCase):
529 """Test the reset and update commands debug output."""
530
531 # Additional positional arguments to pass to the commands
532 _CMD_ARGS = ["--no-color", "-v 2"]
533
534 # The stats models
535 _STATS_MODELS = [Version, VersionDaily, RadioVersion, RadioVersionDaily]
536 # The models that will generate an output
537 _ALL_MODELS = _STATS_MODELS + [StatsMetadata]
538 _COUNTER_NAMES = ["heartbeats", "crashes", "smpl", "other"]
539 _COUNTER_ACTIONS = ["created", "updated"]
540
541 def _assert_command_output_matches(self, command, number, facts, models):
542 """Validate the debug output of a command.
543
544 The debug output is matched against the facts and models given in
545 the parameters.
546 """
547 buffer = StringIO()
548 call_command("stats", command, *self._CMD_ARGS, stdout=buffer)
549 output = buffer.getvalue().splitlines()
550
551 expected_output = "{number} {model} {fact}"
552 for model in models:
553 for fact in facts:
554 self.assertIn(
555 expected_output.format(
556 number=number, model=model.__name__, fact=fact
557 ),
558 output,
559 )
560
561 def test_reset_command_on_empty_db(self):
562 """Test the reset command on an empty database.
563
564 The reset command should yield nothing on an empty database.
565 """
566 self._assert_command_output_matches(
567 "reset", 0, ["deleted"], self._ALL_MODELS
568 )
569
570 def test_update_command_on_empty_db(self):
571 """Test the update command on an empty database.
572
573 The update command should yield nothing on an empty database.
574 """
575 pattern = "{action} for counter {counter}"
576 facts = [
577 pattern.format(action=counter_action, counter=counter_name)
578 for counter_action in self._COUNTER_ACTIONS
579 for counter_name in self._COUNTER_NAMES
580 ]
581 self._assert_command_output_matches(
582 "update", 0, facts, self._STATS_MODELS
583 )
584
585 def test_reset_command_deletion_of_instances(self):
586 """Test the deletion of stats model instances with the reset command.
587
588 This test validates that model instances get deleted when the
589 reset command is called on a database that only contains a single
590 model instance for each class.
591 """
592 # Create dummy version instances
593 version = Dummy.create_dummy_version()
594 radio_version = Dummy.create_dummy_radio_version()
595 Dummy.create_dummy_daily_version(version)
596 Dummy.create_dummy_daily_radio_version(radio_version)
597 Dummy.create_dummy_stats_metadata()
598
599 # We expect that the model instances get deleted
600 self._assert_command_output_matches(
601 "reset", 1, ["deleted"], self._ALL_MODELS
602 )