blob: 2bd5df95f6de0663606ff97c06c1d6bfc7d682f6 [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
Mitja Nikolause0df5a32018-09-21 15:59:54 +0200196 def _assert_accumulated_counters_are_correct(
197 self, report_type, counter_attribute_name, **kwargs
198 ):
199 """Validate a counter distribution with reports of different devices."""
200 # Create some devices and corresponding reports
201 devices = [
202 Dummy.create_dummy_device(Dummy.create_dummy_user(username=name))
203 for name in Dummy.USERNAMES
204 ]
205 num_reports = 5
206 for device in devices:
207 self._create_reports(
208 report_type,
209 self.unique_entries[0],
210 device,
211 num_reports,
212 **kwargs
213 )
214
215 # Run the command to update the database
216 call_command("stats", "update")
217
218 # Check whether the numbers of reports match
219 version = self.version_class.objects.get(
220 **{self.unique_entry_name: self.unique_entries[0]}
221 )
222 self.assertEqual(
223 len(Dummy.USERNAMES) * num_reports,
224 getattr(version, counter_attribute_name),
225 )
226
227 def test_accumulated_heartbeats_counter(self):
228 """Test heartbeats counter with reports from different devices."""
229 report_type = HeartBeat
230 counter_attribute_name = "heartbeats"
231 self._assert_accumulated_counters_are_correct(
232 report_type, counter_attribute_name
233 )
234
235 def test_accumulated_crash_reports_counter(self):
236 """Test crash reports counter with reports from different devices."""
237 report_type = Crashreport
238 counter_attribute_name = "prob_crashes"
239 boot_reason_param = {"boot_reason": Crashreport.CRASH_BOOT_REASONS[0]}
240 self._assert_accumulated_counters_are_correct(
241 report_type, counter_attribute_name, **boot_reason_param
242 )
243
244 def test_accumulated_smpl_reports_counter(self):
245 """Test smpl reports counter with reports from different devices."""
246 report_type = Crashreport
247 counter_attribute_name = "smpl"
248 boot_reason_param = {"boot_reason": Crashreport.SMPL_BOOT_REASONS[0]}
249 self._assert_accumulated_counters_are_correct(
250 report_type, counter_attribute_name, **boot_reason_param
251 )
252
253 def test_accumulated_other_reports_counter(self):
254 """Test other reports counter with reports from different devices."""
255 report_type = Crashreport
256 counter_attribute_name = "other"
257 boot_reason_param = {"boot_reason": "random boot reason"}
258 self._assert_accumulated_counters_are_correct(
259 report_type, counter_attribute_name, **boot_reason_param
260 )
261
Mitja Nikolaus03e412b2018-09-18 17:50:15 +0200262 def _assert_reports_with_same_timestamp_are_counted(
263 self, report_type, counter_attribute_name, **kwargs
264 ):
265 """Validate that reports with the same timestamp are counted.
266
267 Reports from different devices but the same timestamp should be
268 counted as independent reports.
269 """
270 # Create a report
271 device1 = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
272 report1 = Dummy.create_dummy_report(
273 report_type, device=device1, **kwargs
274 )
275
276 # Create a second report with the same timestamp but from another device
277 device2 = Dummy.create_dummy_device(
278 user=Dummy.create_dummy_user(username=Dummy.USERNAMES[1])
279 )
280 Dummy.create_dummy_report(
281 report_type, device=device2, date=report1.date, **kwargs
282 )
283
284 # Run the command to update the database
285 call_command("stats", "update")
286
287 # Get the corresponding version instance from the database
288 get_params = {
289 self.unique_entry_name: getattr(report1, self.unique_entry_name)
290 }
291 version = self.version_class.objects.get(**get_params)
292
293 # Assert that both reports are counted
294 self.assertEqual(getattr(version, counter_attribute_name), 2)
295
296 @unittest.skip(
297 "Duplicates are dropped based on their timestamp at the moment. This is"
298 "to be adapted so that they are dropped taking into account the device"
299 "UUID as well."
300 )
301 def test_heartbeats_with_same_timestamp_are_counted(self):
302 """Validate that heartbeats with same timestamp are counted."""
303 counter_attribute_name = "heartbeats"
304 self._assert_reports_with_same_timestamp_are_counted(
305 HeartBeat, counter_attribute_name
306 )
307
308 @unittest.skip(
309 "Duplicates are dropped based on their timestamp at the moment. This is"
310 "to be adapted so that they are dropped taking into account the device"
311 "UUID as well."
312 )
313 def test_crash_reports_with_same_timestamp_are_counted(self):
314 """Validate that crash report duplicates are ignored."""
315 counter_attribute_name = "prob_crashes"
316 for unique_entry, boot_reason in zip(
317 self.unique_entries, Crashreport.CRASH_BOOT_REASONS
318 ):
319 params = {
320 "boot_reason": boot_reason,
321 self.unique_entry_name: unique_entry,
322 }
323 self._assert_reports_with_same_timestamp_are_counted(
324 Crashreport, counter_attribute_name, **params
325 )
326
327 @unittest.skip(
328 "Duplicates are dropped based on their timestamp at the moment. This is"
329 "to be adapted so that they are dropped taking into account the device"
330 "UUID as well."
331 )
332 def test_smpl_reports_with_same_timestamp_are_counted(self):
333 """Validate that smpl report duplicates are ignored."""
334 counter_attribute_name = "smpl"
335 for unique_entry, boot_reason in zip(
336 self.unique_entries, Crashreport.SMPL_BOOT_REASONS
337 ):
338 params = {
339 "boot_reason": boot_reason,
340 self.unique_entry_name: unique_entry,
341 }
342 self._assert_reports_with_same_timestamp_are_counted(
343 Crashreport, counter_attribute_name, **params
344 )
345
346 @unittest.skip(
347 "Duplicates are dropped based on their timestamp at the moment. This is"
348 "to be adapted so that they are dropped taking into account the device"
349 "UUID as well."
350 )
351 def test_other_reports_with_same_timestamp_are_counted(self):
352 """Validate that other report duplicates are ignored."""
353 counter_attribute_name = "other"
354 params = {"boot_reason": "random boot reason"}
355 self._assert_reports_with_same_timestamp_are_counted(
356 Crashreport, counter_attribute_name, **params
357 )
358
359 def _assert_duplicates_are_ignored(
360 self, report_type, device, counter_attribute_name, **kwargs
361 ):
362 """Validate that reports with duplicate timestamps are ignored."""
363 # Create a report
364 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
365
366 # Create a second report with the same timestamp
367 Dummy.create_dummy_report(
368 report_type, device=device, date=report.date, **kwargs
369 )
370
371 # Run the command to update the database
372 call_command("stats", "update")
373
374 # Get the corresponding version instance from the database
375 get_params = {
376 self.unique_entry_name: getattr(report, self.unique_entry_name)
377 }
378 version = self.version_class.objects.get(**get_params)
379
380 # Assert that the report with the duplicate timestamp is not
381 # counted, i.e. only 1 report is counted.
382 self.assertEqual(getattr(version, counter_attribute_name), 1)
383
384 def test_heartbeat_duplicates_are_ignored(self):
385 """Validate that heartbeat duplicates are ignored."""
386 counter_attribute_name = "heartbeats"
387 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
388 self._assert_duplicates_are_ignored(
389 HeartBeat, device, counter_attribute_name
390 )
391
392 def test_crash_report_duplicates_are_ignored(self):
393 """Validate that crash report duplicates are ignored."""
394 counter_attribute_name = "prob_crashes"
395 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
396 for i, boot_reason in enumerate(Crashreport.CRASH_BOOT_REASONS):
397 params = {
398 "boot_reason": boot_reason,
399 self.unique_entry_name: self.unique_entries[i],
400 }
401 self._assert_duplicates_are_ignored(
402 Crashreport, device, counter_attribute_name, **params
403 )
404
405 def test_smpl_report_duplicates_are_ignored(self):
406 """Validate that smpl report duplicates are ignored."""
407 counter_attribute_name = "smpl"
408 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
409 for i, boot_reason in enumerate(Crashreport.SMPL_BOOT_REASONS):
410 params = {
411 "boot_reason": boot_reason,
412 self.unique_entry_name: self.unique_entries[i],
413 }
414 self._assert_duplicates_are_ignored(
415 Crashreport, device, counter_attribute_name, **params
416 )
417
418 def test_other_report_duplicates_are_ignored(self):
419 """Validate that other report duplicates are ignored."""
420 counter_attribute_name = "other"
421 params = {"boot_reason": "random boot reason"}
422 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
423 self._assert_duplicates_are_ignored(
424 Crashreport, device, counter_attribute_name, **params
425 )
426
427 def _assert_older_reports_update_released_on_date(
428 self, report_type, **kwargs
429 ):
430 """Test updating of the released_on date.
431
432 Validate that the released_on date is updated once an older report is
433 sent.
434 """
435 # Create a report
436 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
437 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
438
439 # Run the command to update the database
440 call_command("stats", "update")
441
442 # Get the corresponding version instance from the database
443 version = self.version_class.objects.get(
444 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
445 )
446
447 # Assert that the released_on date matches the first report date
448 self.assertEqual(version.released_on, report.date.date())
449
450 # Create a second report with the a timestamp earlier in time
451 report_2_date = report.date - timedelta(days=1)
452 Dummy.create_dummy_report(
453 report_type, device=device, date=report_2_date, **kwargs
454 )
455
456 # Run the command to update the database
457 call_command("stats", "update")
458
459 # Get the corresponding version instance from the database
460 version = self.version_class.objects.get(
461 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
462 )
463
464 # Assert that the released_on date matches the older report date
465 self.assertEqual(version.released_on, report_2_date.date())
466
467 def _assert_newer_reports_do_not_update_released_on_date(
468 self, report_type, **kwargs
469 ):
470 """Test updating of the released_on date.
471
472 Validate that the released_on date is not updated once a newer report is
473 sent.
474 """
475 # Create a report
476 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
477 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
478 report_1_date = report.date.date()
479
480 # Run the command to update the database
481 call_command("stats", "update")
482
483 # Get the corresponding version instance from the database
484 version = self.version_class.objects.get(
485 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
486 )
487
488 # Assert that the released_on date matches the first report date
489 self.assertEqual(version.released_on, report_1_date)
490
491 # Create a second report with the a timestamp later in time
492 report_2_date = report.date + timedelta(days=1)
493 Dummy.create_dummy_report(
494 report_type, device=device, date=report_2_date, **kwargs
495 )
496
497 # Run the command to update the database
498 call_command("stats", "update")
499
500 # Get the corresponding version instance from the database
501 version = self.version_class.objects.get(
502 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
503 )
504
505 # Assert that the released_on date matches the older report date
506 self.assertEqual(version.released_on, report_1_date)
507
508 def test_older_heartbeat_updates_released_on_date(self):
509 """Validate that older heartbeats update the release date."""
510 self._assert_older_reports_update_released_on_date(HeartBeat)
511
512 def test_older_crash_report_updates_released_on_date(self):
513 """Validate that older crash reports update the release date."""
514 self._assert_older_reports_update_released_on_date(Crashreport)
515
516 def test_newer_heartbeat_does_not_update_released_on_date(self):
517 """Validate that newer heartbeats don't update the release date."""
518 self._assert_newer_reports_do_not_update_released_on_date(HeartBeat)
519
520 def test_newer_crash_report_does_not_update_released_on_date(self):
521 """Validate that newer crash reports don't update the release date."""
522 self._assert_newer_reports_do_not_update_released_on_date(Crashreport)
523
524 def _assert_manually_changed_released_on_date_is_not_updated(
525 self, report_type, **kwargs
526 ):
527 """Test updating of manually changed released_on dates.
528
529 Validate that a manually changed released_on date is not updated when
530 new reports are sent.
531 """
532 # Create a report
533 device = Dummy.create_dummy_device(user=Dummy.create_dummy_user())
534 report = Dummy.create_dummy_report(report_type, device=device, **kwargs)
535
536 # Run the command to update the database
537 call_command("stats", "update")
538
539 # Get the corresponding version instance from the database
540 version = self.version_class.objects.get(
541 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
542 )
543
544 # Assert that the released_on date matches the first report date
545 self.assertEqual(version.released_on, report.date.date())
546
547 # Create a second report with a timestamp earlier in time
548 report_2_date = report.date - timedelta(days=1)
549 Dummy.create_dummy_report(
550 report_type, device=device, date=report_2_date, **kwargs
551 )
552
553 # Manually change the released_on date
554 version_release_date = report.date + timedelta(days=1)
555 version.released_on = version_release_date
556 version.save()
557
558 # Run the command to update the database
559 call_command("stats", "update")
560
561 # Get the corresponding version instance from the database
562 version = self.version_class.objects.get(
563 **{self.unique_entry_name: getattr(report, self.unique_entry_name)}
564 )
565
566 # Assert that the released_on date still matches the date is was
567 # manually changed to
568 self.assertEqual(version.released_on, version_release_date.date())
569
570 def test_manually_changed_released_on_date_is_not_updated_by_heartbeat(
571 self
572 ):
573 """Test update of manually changed released_on date with heartbeat."""
574 self._assert_manually_changed_released_on_date_is_not_updated(HeartBeat)
575
576 def test_manually_changed_released_on_date_is_not_updated_by_crash_report(
577 self
578 ):
579 """Test update of manually changed released_on date with crashreport."""
580 self._assert_manually_changed_released_on_date_is_not_updated(
581 Crashreport
582 )
583
584
585# pylint: disable=too-many-ancestors
586class StatsCommandRadioVersionsTestCase(StatsCommandVersionsTestCase):
587 """Test the generation of RadioVersion stats with the stats command."""
588
589 version_class = RadioVersion
590 unique_entry_name = "radio_version"
591 unique_entries = Dummy.RADIO_VERSIONS
592
593
594class CommandDebugOutputTestCase(TestCase):
595 """Test the reset and update commands debug output."""
596
597 # Additional positional arguments to pass to the commands
598 _CMD_ARGS = ["--no-color", "-v 2"]
599
600 # The stats models
601 _STATS_MODELS = [Version, VersionDaily, RadioVersion, RadioVersionDaily]
602 # The models that will generate an output
603 _ALL_MODELS = _STATS_MODELS + [StatsMetadata]
604 _COUNTER_NAMES = ["heartbeats", "crashes", "smpl", "other"]
605 _COUNTER_ACTIONS = ["created", "updated"]
606
607 def _assert_command_output_matches(self, command, number, facts, models):
608 """Validate the debug output of a command.
609
610 The debug output is matched against the facts and models given in
611 the parameters.
612 """
613 buffer = StringIO()
614 call_command("stats", command, *self._CMD_ARGS, stdout=buffer)
615 output = buffer.getvalue().splitlines()
616
617 expected_output = "{number} {model} {fact}"
618 for model in models:
619 for fact in facts:
620 self.assertIn(
621 expected_output.format(
622 number=number, model=model.__name__, fact=fact
623 ),
624 output,
625 )
626
627 def test_reset_command_on_empty_db(self):
628 """Test the reset command on an empty database.
629
630 The reset command should yield nothing on an empty database.
631 """
632 self._assert_command_output_matches(
633 "reset", 0, ["deleted"], self._ALL_MODELS
634 )
635
636 def test_update_command_on_empty_db(self):
637 """Test the update command on an empty database.
638
639 The update command should yield nothing on an empty database.
640 """
641 pattern = "{action} for counter {counter}"
642 facts = [
643 pattern.format(action=counter_action, counter=counter_name)
644 for counter_action in self._COUNTER_ACTIONS
645 for counter_name in self._COUNTER_NAMES
646 ]
647 self._assert_command_output_matches(
648 "update", 0, facts, self._STATS_MODELS
649 )
650
651 def test_reset_command_deletion_of_instances(self):
652 """Test the deletion of stats model instances with the reset command.
653
654 This test validates that model instances get deleted when the
655 reset command is called on a database that only contains a single
656 model instance for each class.
657 """
658 # Create dummy version instances
659 version = Dummy.create_dummy_version()
660 radio_version = Dummy.create_dummy_radio_version()
661 Dummy.create_dummy_daily_version(version)
662 Dummy.create_dummy_daily_radio_version(radio_version)
663 Dummy.create_dummy_stats_metadata()
664
665 # We expect that the model instances get deleted
666 self._assert_command_output_matches(
667 "reset", 1, ["deleted"], self._ALL_MODELS
668 )