blob: 0cf3fc73f062c20db86fd11e2cb9160f897daa1a [file] [log] [blame]
Keyar Hood1a3c8dd2013-05-29 17:41:50 -07001#!/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
8import datetime, logging, shelve, sys
9
10import common
11from autotest_lib.client.common_lib import global_config, mail
12from 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
33def 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
45def 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
58def 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
68def 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
85def 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
97def 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
116def 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
156def 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
173if __name__ == '__main__':
174 sys.exit(main())