Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | # |
| 3 | # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | |
| 7 | |
Keyar Hood | 4d5f9d1 | 2013-06-28 15:36:45 -0700 | [diff] [blame^] | 8 | import datetime, shelve, sys |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 9 | |
| 10 | import common |
Keyar Hood | f9a3651 | 2013-06-13 18:39:56 -0700 | [diff] [blame] | 11 | from autotest_lib.client.common_lib import mail |
| 12 | from autotest_lib.frontend import setup_django_readonly_environment |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 13 | |
Keyar Hood | f9a3651 | 2013-06-13 18:39:56 -0700 | [diff] [blame] | 14 | # Django and the models are only setup after |
| 15 | # the setup_django_readonly_environment module is imported. |
| 16 | from autotest_lib.frontend.tko import models as tko_models |
| 17 | from django.db import models as django_models |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 18 | |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 19 | |
| 20 | _STORAGE_FILE = 'failure_storage' |
Keyar Hood | e7b1184 | 2013-06-25 15:41:59 -0700 | [diff] [blame] | 21 | # Mark a test as failing too long if it has not passed in this many days |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 22 | _DAYS_TO_BE_FAILING_TOO_LONG = 60 |
Keyar Hood | e7b1184 | 2013-06-25 15:41:59 -0700 | [diff] [blame] | 23 | # Ignore any tests that have not ran in this many days |
| 24 | _DAYS_NOT_RUNNING_CUTOFF = 60 |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 25 | _TEST_PASS_STATUS_INDEX = 6 |
| 26 | _MAIL_RESULTS_FROM = 'chromeos-test-health@google.com' |
| 27 | _MAIL_RESULTS_TO = 'chromeos-lab-infrastructure@google.com' |
| 28 | |
| 29 | |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 30 | def load_storage(): |
| 31 | """ |
| 32 | Loads the storage object from disk. |
| 33 | |
| 34 | This object keeps track of which tests we have already sent mail about so |
| 35 | we only send emails when the status of a test changes. |
| 36 | |
| 37 | @return the storage object. |
| 38 | |
| 39 | """ |
| 40 | return shelve.open(_STORAGE_FILE) |
| 41 | |
| 42 | |
| 43 | def save_storage(storage): |
| 44 | """ |
| 45 | Saves the storage object to disk. |
| 46 | |
| 47 | @param storage: The storage object to save to disk. |
| 48 | |
| 49 | """ |
| 50 | storage.close() |
| 51 | |
| 52 | |
Keyar Hood | 005539d | 2013-06-24 14:27:57 -0700 | [diff] [blame] | 53 | def is_valid_test_name(name): |
| 54 | """ |
| 55 | Returns if a test name is valid or not. |
| 56 | |
| 57 | There is a bunch of entries in the tko_test table that are not actually |
| 58 | test names. They are there as a side effect of how Autotest uses this |
| 59 | table. |
| 60 | |
| 61 | Two examples of bad tests names are as follows: |
| 62 | link-release/R29-4228.0.0/faft_ec/firmware_ECPowerG3_SERVER_JOB |
| 63 | try_new_image-chormeos1-rack2-host2 |
| 64 | |
| 65 | @param name: The candidate test names to check. |
| 66 | @return True if name is a valid test name and false otherwise. |
| 67 | |
| 68 | """ |
| 69 | return not '/' in name and not name.startswith('try_new_image') |
| 70 | |
| 71 | |
Keyar Hood | f9a3651 | 2013-06-13 18:39:56 -0700 | [diff] [blame] | 72 | def get_last_pass_times(): |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 73 | """ |
| 74 | Get all the tests that have passed and the time they last passed. |
| 75 | |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 76 | @return the dict of test_name:last_finish_time pairs for tests that have |
| 77 | passed. |
| 78 | |
| 79 | """ |
Keyar Hood | f9a3651 | 2013-06-13 18:39:56 -0700 | [diff] [blame] | 80 | results = tko_models.Test.objects.values('test').filter( |
| 81 | status=_TEST_PASS_STATUS_INDEX).annotate( |
| 82 | last_pass=django_models.Max('started_time')) |
Keyar Hood | 005539d | 2013-06-24 14:27:57 -0700 | [diff] [blame] | 83 | results_dict = {result['test']: result['last_pass'] |
| 84 | for result in results} |
| 85 | valid_test_names = filter(is_valid_test_name, results_dict) |
| 86 | # The shelve module does not accept Unicode objects as keys but does |
| 87 | # accept utf-8 strings. |
| 88 | return {name.encode('utf8'): results_dict[name] |
| 89 | for name in valid_test_names} |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 90 | |
| 91 | |
Keyar Hood | e7b1184 | 2013-06-25 15:41:59 -0700 | [diff] [blame] | 92 | def get_recently_ran_test_names(): |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 93 | """ |
Keyar Hood | e7b1184 | 2013-06-25 15:41:59 -0700 | [diff] [blame] | 94 | Get all the test names from the database that have been recently ran. |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 95 | |
Keyar Hood | e7b1184 | 2013-06-25 15:41:59 -0700 | [diff] [blame] | 96 | @return a set of the recently ran tests. |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 97 | |
| 98 | """ |
Keyar Hood | e7b1184 | 2013-06-25 15:41:59 -0700 | [diff] [blame] | 99 | cutoff_delta = datetime.timedelta(_DAYS_NOT_RUNNING_CUTOFF) |
| 100 | cutoff_date = datetime.datetime.today() - cutoff_delta |
| 101 | results = tko_models.Test.objects.filter( |
| 102 | started_time__gte=cutoff_date).values('test').distinct() |
Keyar Hood | 005539d | 2013-06-24 14:27:57 -0700 | [diff] [blame] | 103 | test_names = [test['test'] for test in results] |
| 104 | valid_test_names = filter(is_valid_test_name, test_names) |
Keyar Hood | e7b1184 | 2013-06-25 15:41:59 -0700 | [diff] [blame] | 105 | return {test.encode('utf8') for test in valid_test_names} |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 106 | |
| 107 | |
Keyar Hood | f9a3651 | 2013-06-13 18:39:56 -0700 | [diff] [blame] | 108 | def get_tests_to_analyze(): |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 109 | """ |
Keyar Hood | e7b1184 | 2013-06-25 15:41:59 -0700 | [diff] [blame] | 110 | Get all the recently ran tests as well as the last time they have passed. |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 111 | |
| 112 | The minimum datetime is given as last pass time for tests that have never |
| 113 | passed. |
| 114 | |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 115 | @return the dict of test_name:last_finish_time pairs. |
| 116 | |
| 117 | """ |
Keyar Hood | e7b1184 | 2013-06-25 15:41:59 -0700 | [diff] [blame] | 118 | recent_test_names = get_recently_ran_test_names() |
Keyar Hood | f9a3651 | 2013-06-13 18:39:56 -0700 | [diff] [blame] | 119 | last_passes = get_last_pass_times() |
Keyar Hood | e7b1184 | 2013-06-25 15:41:59 -0700 | [diff] [blame] | 120 | |
| 121 | running_passes = {} |
| 122 | for test, pass_time in last_passes.items(): |
| 123 | if test in recent_test_names: |
| 124 | running_passes[test] = pass_time |
| 125 | |
| 126 | failures_names = recent_test_names.difference(running_passes) |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 127 | always_failed = {test: datetime.datetime.min for test in failures_names} |
Keyar Hood | e7b1184 | 2013-06-25 15:41:59 -0700 | [diff] [blame] | 128 | return dict(always_failed.items() + running_passes.items()) |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 129 | |
| 130 | |
| 131 | def email_about_test_failure(tests, storage): |
| 132 | """ |
| 133 | Send emails based on the last time tests has passed. |
| 134 | |
| 135 | This involves updating the storage and sending an email if a test has |
Keyar Hood | 4d5f9d1 | 2013-06-28 15:36:45 -0700 | [diff] [blame^] | 136 | failed for a long time. |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 137 | |
| 138 | @param tests: The test_name:time_of_last_pass pairs. |
| 139 | @param storage: The storage object. |
| 140 | |
| 141 | """ |
| 142 | failing_time_cutoff = datetime.timedelta(_DAYS_TO_BE_FAILING_TOO_LONG) |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 143 | |
| 144 | today = datetime.datetime.today() |
| 145 | for test, last_fail in tests.iteritems(): |
| 146 | if today - last_fail >= failing_time_cutoff: |
| 147 | if test not in storage: |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 148 | storage[test] = today |
| 149 | else: |
| 150 | try: |
| 151 | del storage[test] |
| 152 | except KeyError: |
| 153 | pass |
| 154 | |
Keyar Hood | 4d5f9d1 | 2013-06-28 15:36:45 -0700 | [diff] [blame^] | 155 | if storage: |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 156 | mail.send(_MAIL_RESULTS_FROM, |
| 157 | [_MAIL_RESULTS_TO], |
| 158 | [], |
| 159 | 'Long Failing Tests', |
| 160 | 'The following tests have been failing for ' |
| 161 | 'at least %s days:\n\n' % (_DAYS_TO_BE_FAILING_TOO_LONG) + |
Keyar Hood | 4d5f9d1 | 2013-06-28 15:36:45 -0700 | [diff] [blame^] | 162 | '\n'.join(storage.keys())) |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 163 | |
| 164 | |
| 165 | def main(): |
| 166 | """ |
| 167 | The script code. |
| 168 | |
| 169 | Allows other python code to import and run this code. This will be more |
| 170 | important if a nice way to test this code can be determined. |
| 171 | |
| 172 | """ |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 173 | storage = load_storage() |
Keyar Hood | f9a3651 | 2013-06-13 18:39:56 -0700 | [diff] [blame] | 174 | tests = get_tests_to_analyze() |
Keyar Hood | 1a3c8dd | 2013-05-29 17:41:50 -0700 | [diff] [blame] | 175 | email_about_test_failure(tests, storage) |
| 176 | save_storage(storage) |
| 177 | |
| 178 | return 0 |
| 179 | |
| 180 | |
| 181 | if __name__ == '__main__': |
| 182 | sys.exit(main()) |