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 | |
| 8 | import datetime, logging, shelve, sys |
| 9 | |
| 10 | import common |
| 11 | from autotest_lib.client.common_lib import global_config, mail |
| 12 | from autotest_lib.database import database_connection |
| 13 | |
| 14 | |
| 15 | _GLOBAL_CONF = global_config.global_config |
| 16 | _CONF_SECTION = 'AUTOTEST_WEB' |
| 17 | |
| 18 | _MYSQL_READONLY_LOGIN_CREDENTIALS = { |
| 19 | 'host': _GLOBAL_CONF.get_config_value(_CONF_SECTION, 'readonly_host'), |
| 20 | 'username': _GLOBAL_CONF.get_config_value(_CONF_SECTION, 'readonly_user'), |
| 21 | 'password': _GLOBAL_CONF.get_config_value( |
| 22 | _CONF_SECTION, 'readonly_password'), |
| 23 | 'db_name': _GLOBAL_CONF.get_config_value(_CONF_SECTION, 'database'), |
| 24 | } |
| 25 | |
| 26 | _STORAGE_FILE = 'failure_storage' |
| 27 | _DAYS_TO_BE_FAILING_TOO_LONG = 60 |
| 28 | _TEST_PASS_STATUS_INDEX = 6 |
| 29 | _MAIL_RESULTS_FROM = 'chromeos-test-health@google.com' |
| 30 | _MAIL_RESULTS_TO = 'chromeos-lab-infrastructure@google.com' |
| 31 | |
| 32 | |
| 33 | def connect_to_db(): |
| 34 | """ |
| 35 | Create a readonly connection to the Autotest database. |
| 36 | |
| 37 | @return a readonly connection to the Autotest database. |
| 38 | |
| 39 | """ |
| 40 | db = database_connection.DatabaseConnection(_CONF_SECTION) |
| 41 | db.connect(**_MYSQL_READONLY_LOGIN_CREDENTIALS) |
| 42 | return db |
| 43 | |
| 44 | |
| 45 | def load_storage(): |
| 46 | """ |
| 47 | Loads the storage object from disk. |
| 48 | |
| 49 | This object keeps track of which tests we have already sent mail about so |
| 50 | we only send emails when the status of a test changes. |
| 51 | |
| 52 | @return the storage object. |
| 53 | |
| 54 | """ |
| 55 | return shelve.open(_STORAGE_FILE) |
| 56 | |
| 57 | |
| 58 | def save_storage(storage): |
| 59 | """ |
| 60 | Saves the storage object to disk. |
| 61 | |
| 62 | @param storage: The storage object to save to disk. |
| 63 | |
| 64 | """ |
| 65 | storage.close() |
| 66 | |
| 67 | |
| 68 | def get_last_pass_times(db): |
| 69 | """ |
| 70 | Get all the tests that have passed and the time they last passed. |
| 71 | |
| 72 | @param db: The Autotest database connection. |
| 73 | @return the dict of test_name:last_finish_time pairs for tests that have |
| 74 | passed. |
| 75 | |
| 76 | """ |
| 77 | query = ('SELECT test, MAX(started_time) FROM tko_tests ' |
| 78 | 'WHERE status = %s GROUP BY test' % _TEST_PASS_STATUS_INDEX) |
| 79 | |
| 80 | passed_tests = {result[0]: result[1] for result in db.execute(query)} |
| 81 | |
| 82 | return passed_tests |
| 83 | |
| 84 | |
| 85 | def get_all_test_names(db): |
| 86 | """ |
| 87 | Get all the test names from the database. |
| 88 | |
| 89 | @param db: The Autotest database connection. |
| 90 | @return a list of all the test names. |
| 91 | |
| 92 | """ |
| 93 | query = 'SELECT DISTINCT test FROM tko_tests' |
| 94 | return [row[0] for row in db.execute(query)] |
| 95 | |
| 96 | |
| 97 | def get_tests_to_analyze(db): |
| 98 | """ |
| 99 | Get all the tests as well as the last time they have passed. |
| 100 | |
| 101 | The minimum datetime is given as last pass time for tests that have never |
| 102 | passed. |
| 103 | |
| 104 | @param db: The Autotest database connection. |
| 105 | |
| 106 | @return the dict of test_name:last_finish_time pairs. |
| 107 | |
| 108 | """ |
| 109 | last_passes = get_last_pass_times(db) |
| 110 | all_test_names = get_all_test_names(db) |
| 111 | failures_names = (set(all_test_names) - set(last_passes.keys())) |
| 112 | always_failed = {test: datetime.datetime.min for test in failures_names} |
| 113 | return dict(always_failed.items() + last_passes.items()) |
| 114 | |
| 115 | |
| 116 | def email_about_test_failure(tests, storage): |
| 117 | """ |
| 118 | Send emails based on the last time tests has passed. |
| 119 | |
| 120 | This involves updating the storage and sending an email if a test has |
| 121 | failed for a long time and we have not already sent an email about that |
| 122 | test. |
| 123 | |
| 124 | @param tests: The test_name:time_of_last_pass pairs. |
| 125 | @param storage: The storage object. |
| 126 | |
| 127 | """ |
| 128 | failing_time_cutoff = datetime.timedelta(_DAYS_TO_BE_FAILING_TOO_LONG) |
| 129 | update_status = [] |
| 130 | |
| 131 | today = datetime.datetime.today() |
| 132 | for test, last_fail in tests.iteritems(): |
| 133 | if today - last_fail >= failing_time_cutoff: |
| 134 | if test not in storage: |
| 135 | update_status.append(test) |
| 136 | storage[test] = today |
| 137 | else: |
| 138 | try: |
| 139 | del storage[test] |
| 140 | except KeyError: |
| 141 | pass |
| 142 | |
| 143 | if update_status: |
| 144 | logging.info('Found %i new failing tests out %i, sending email.', |
| 145 | len(update_status), |
| 146 | len(tests)) |
| 147 | mail.send(_MAIL_RESULTS_FROM, |
| 148 | [_MAIL_RESULTS_TO], |
| 149 | [], |
| 150 | 'Long Failing Tests', |
| 151 | 'The following tests have been failing for ' |
| 152 | 'at least %s days:\n\n' % (_DAYS_TO_BE_FAILING_TOO_LONG) + |
| 153 | '\n'.join(update_status)) |
| 154 | |
| 155 | |
| 156 | def main(): |
| 157 | """ |
| 158 | The script code. |
| 159 | |
| 160 | Allows other python code to import and run this code. This will be more |
| 161 | important if a nice way to test this code can be determined. |
| 162 | |
| 163 | """ |
| 164 | db = connect_to_db() |
| 165 | storage = load_storage() |
| 166 | tests = get_tests_to_analyze(db) |
| 167 | email_about_test_failure(tests, storage) |
| 168 | save_storage(storage) |
| 169 | |
| 170 | return 0 |
| 171 | |
| 172 | |
| 173 | if __name__ == '__main__': |
| 174 | sys.exit(main()) |