blob: 0105a28effe64b6c2b2950e9f2b9f458a59f762f [file] [log] [blame]
mbligh96cf0512008-04-17 15:25:38 +00001#!/usr/bin/python -u
mblighc2514542008-02-19 15:54:26 +00002
mblighb33e53e2008-06-17 19:41:26 +00003import os, sys, optparse, fcntl, errno, traceback, socket
mblighbb7b8912006-10-08 03:59:02 +00004
mbligh96cf0512008-04-17 15:25:38 +00005import common
Fang Deng9ec66802014-04-28 19:04:33 +00006from autotest_lib.frontend import setup_django_environment
jadmanskidb4f9b52008-12-03 22:52:53 +00007from autotest_lib.client.common_lib import mail, pidfile
Fang Deng9ec66802014-04-28 19:04:33 +00008from autotest_lib.frontend.tko import models as tko_models
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -07009from autotest_lib.tko import db as tko_db, utils as tko_utils
10from autotest_lib.tko import models, status_lib
11from autotest_lib.tko.perf_upload import perf_uploader
mbligh9e936402009-05-13 20:42:17 +000012from autotest_lib.client.common_lib import utils
Fang Deng9ec66802014-04-28 19:04:33 +000013from autotest_lib.server.cros.dynamic_suite import constants
mbligh74fc0462007-11-05 20:24:17 +000014
15
mbligh96cf0512008-04-17 15:25:38 +000016def parse_args():
jadmanski0afbb632008-06-06 21:10:57 +000017 # build up our options parser and parse sys.argv
18 parser = optparse.OptionParser()
19 parser.add_option("-m", help="Send mail for FAILED tests",
20 dest="mailit", action="store_true")
21 parser.add_option("-r", help="Reparse the results of a job",
22 dest="reparse", action="store_true")
23 parser.add_option("-o", help="Parse a single results directory",
24 dest="singledir", action="store_true")
25 parser.add_option("-l", help=("Levels of subdirectories to include "
26 "in the job name"),
27 type="int", dest="level", default=1)
28 parser.add_option("-n", help="No blocking on an existing parse",
29 dest="noblock", action="store_true")
30 parser.add_option("-s", help="Database server hostname",
31 dest="db_host", action="store")
32 parser.add_option("-u", help="Database username", dest="db_user",
33 action="store")
34 parser.add_option("-p", help="Database password", dest="db_pass",
35 action="store")
36 parser.add_option("-d", help="Database name", dest="db_name",
37 action="store")
jadmanskid5ab8c52008-12-03 16:27:07 +000038 parser.add_option("--write-pidfile",
39 help="write pidfile (.parser_execute)",
40 dest="write_pidfile", action="store_true",
41 default=False)
jadmanski0afbb632008-06-06 21:10:57 +000042 options, args = parser.parse_args()
mbligh74fc0462007-11-05 20:24:17 +000043
jadmanski0afbb632008-06-06 21:10:57 +000044 # we need a results directory
45 if len(args) == 0:
46 tko_utils.dprint("ERROR: at least one results directory must "
47 "be provided")
48 parser.print_help()
49 sys.exit(1)
mbligh74fc0462007-11-05 20:24:17 +000050
jadmanski0afbb632008-06-06 21:10:57 +000051 # pass the options back
52 return options, args
mbligh74fc0462007-11-05 20:24:17 +000053
54
mbligh96cf0512008-04-17 15:25:38 +000055def format_failure_message(jobname, kernel, testname, status, reason):
jadmanski0afbb632008-06-06 21:10:57 +000056 format_string = "%-12s %-20s %-12s %-10s %s"
57 return format_string % (jobname, kernel, testname, status, reason)
mblighb85e6b02006-10-08 17:20:56 +000058
mblighbb7b8912006-10-08 03:59:02 +000059
mbligh96cf0512008-04-17 15:25:38 +000060def mailfailure(jobname, job, message):
jadmanski0afbb632008-06-06 21:10:57 +000061 message_lines = [""]
62 message_lines.append("The following tests FAILED for this job")
63 message_lines.append("http://%s/results/%s" %
64 (socket.gethostname(), jobname))
65 message_lines.append("")
66 message_lines.append(format_failure_message("Job name", "Kernel",
67 "Test name", "FAIL/WARN",
68 "Failure reason"))
69 message_lines.append(format_failure_message("=" * 8, "=" * 6, "=" * 8,
70 "=" * 8, "=" * 14))
71 message_header = "\n".join(message_lines)
mbligh96cf0512008-04-17 15:25:38 +000072
jadmanski0afbb632008-06-06 21:10:57 +000073 subject = "AUTOTEST: FAILED tests from job %s" % jobname
74 mail.send("", job.user, "", subject, message_header + message)
mbligh006f2302007-09-13 20:46:46 +000075
76
Fang Deng9ec66802014-04-28 19:04:33 +000077def _invalidate_original_tests(orig_job_idx, retry_job_idx):
78 """Retry tests invalidates original tests.
79
80 Whenever a retry job is complete, we want to invalidate the original
81 job's test results, such that the consumers of the tko database
82 (e.g. tko frontend, wmatrix) could figure out which results are the latest.
83
84 When a retry job is parsed, we retrieve the original job's afe_job_id
85 from the retry job's keyvals, which is then converted to tko job_idx and
86 passed into this method as |orig_job_idx|.
87
88 In this method, we are going to invalidate the rows in tko_tests that are
89 associated with the original job by flipping their 'invalid' bit to True.
90 In addition, in tko_tests, we also maintain a pointer from the retry results
91 to the original results, so that later we can always know which rows in
92 tko_tests are retries and which are the corresponding original results.
93 This is done by setting the field 'invalidates_test_idx' of the tests
94 associated with the retry job.
95
96 For example, assume Job(job_idx=105) are retried by Job(job_idx=108), after
97 this method is run, their tko_tests rows will look like:
98 __________________________________________________________________________
99 test_idx| job_idx | test | ... | invalid | invalidates_test_idx
100 10 | 105 | dummy_Fail.Error| ... | 1 | NULL
101 11 | 105 | dummy_Fail.Fail | ... | 1 | NULL
102 ...
103 20 | 108 | dummy_Fail.Error| ... | 0 | 10
104 21 | 108 | dummy_Fail.Fail | ... | 0 | 11
105 __________________________________________________________________________
106 Note the invalid bits of the rows for Job(job_idx=105) are set to '1'.
107 And the 'invalidates_test_idx' fields of the rows for Job(job_idx=108)
108 are set to 10 and 11 (the test_idx of the rows for the original job).
109
110 @param orig_job_idx: An integer representing the original job's
111 tko job_idx. Tests associated with this job will
112 be marked as 'invalid'.
113 @param retry_job_idx: An integer representing the retry job's
114 tko job_idx. The field 'invalidates_test_idx'
115 of the tests associated with this job will be updated.
116
117 """
118 msg = 'orig_job_idx: %s, retry_job_idx: %s' % (orig_job_idx, retry_job_idx)
119 if not orig_job_idx or not retry_job_idx:
120 tko_utils.dprint('ERROR: Could not invalidate tests: ' + msg)
121 # Using django models here makes things easier, but make sure that
122 # before this method is called, all other relevant transactions have been
123 # committed to avoid race condition. In the long run, we might consider
124 # to make the rest of parser use django models.
125 orig_tests = tko_models.Test.objects.filter(job__job_idx=orig_job_idx)
126 retry_tests = tko_models.Test.objects.filter(job__job_idx=retry_job_idx)
127
128 # Invalidate original tests.
129 orig_tests.update(invalid=True)
130
131 # Maintain a dictionary that maps (test, subdir) to original tests.
132 # Note that within the scope of a job, (test, subdir) uniquelly
133 # identifies a test run, but 'test' does not.
134 # In a control file, one could run the same test with different
135 # 'subdir_tag', for example,
136 # job.run_test('dummy_Fail', tag='Error', subdir_tag='subdir_1')
137 # job.run_test('dummy_Fail', tag='Error', subdir_tag='subdir_2')
138 # In tko, we will get
139 # (test='dummy_Fail.Error', subdir='dummy_Fail.Error.subdir_1')
140 # (test='dummy_Fail.Error', subdir='dummy_Fail.Error.subdir_2')
141 invalidated_tests = {(orig_test.test, orig_test.subdir): orig_test
142 for orig_test in orig_tests}
143 for retry in retry_tests:
144 # It is possible that (retry.test, retry.subdir) doesn't exist
145 # in invalidated_tests. This could happen when the original job
146 # didn't run some of its tests. For example, a dut goes offline
147 # since the beginning of the job, in which case invalidated_tests
148 # will only have one entry for 'SERVER_JOB'.
149 orig_test = invalidated_tests.get((retry.test, retry.subdir), None)
150 if orig_test:
151 retry.invalidates_test = orig_test
152 retry.save()
153 tko_utils.dprint('DEBUG: Invalidated tests associated to job: ' + msg)
154
155
mbligh96cf0512008-04-17 15:25:38 +0000156def parse_one(db, jobname, path, reparse, mail_on_failure):
jadmanski0afbb632008-06-06 21:10:57 +0000157 """
158 Parse a single job. Optionally send email on failure.
159 """
160 tko_utils.dprint("\nScanning %s (%s)" % (jobname, path))
jadmanski9b6babf2009-04-21 17:57:40 +0000161 old_job_idx = db.find_job(jobname)
showard0fec8a02009-12-04 01:19:54 +0000162 # old tests is a dict from tuple (test_name, subdir) to test_idx
163 old_tests = {}
164 if old_job_idx is not None:
165 if not reparse:
166 tko_utils.dprint("! Job is already parsed, done")
167 return
168
showardeab66ce2009-12-23 00:03:56 +0000169 raw_old_tests = db.select("test_idx,subdir,test", "tko_tests",
showard0fec8a02009-12-04 01:19:54 +0000170 {"job_idx": old_job_idx})
171 if raw_old_tests:
172 old_tests = dict(((test, subdir), test_idx)
173 for test_idx, subdir, test in raw_old_tests)
mbligh96cf0512008-04-17 15:25:38 +0000174
jadmanski0afbb632008-06-06 21:10:57 +0000175 # look up the status version
jadmanskidb4f9b52008-12-03 22:52:53 +0000176 job_keyval = models.job.read_keyval(path)
177 status_version = job_keyval.get("status_version", 0)
jadmanski6e8bf752008-05-14 00:17:48 +0000178
jadmanski0afbb632008-06-06 21:10:57 +0000179 # parse out the job
180 parser = status_lib.parser(status_version)
181 job = parser.make_job(path)
182 status_log = os.path.join(path, "status.log")
183 if not os.path.exists(status_log):
184 status_log = os.path.join(path, "status")
185 if not os.path.exists(status_log):
186 tko_utils.dprint("! Unable to parse job, no status file")
187 return
mbligh96cf0512008-04-17 15:25:38 +0000188
jadmanski0afbb632008-06-06 21:10:57 +0000189 # parse the status logs
190 tko_utils.dprint("+ Parsing dir=%s, jobname=%s" % (path, jobname))
191 status_lines = open(status_log).readlines()
192 parser.start(job)
193 tests = parser.end(status_lines)
jadmanski9b6babf2009-04-21 17:57:40 +0000194
195 # parser.end can return the same object multiple times, so filter out dups
196 job.tests = []
197 already_added = set()
198 for test in tests:
199 if test not in already_added:
200 already_added.add(test)
201 job.tests.append(test)
202
showard0fec8a02009-12-04 01:19:54 +0000203 # try and port test_idx over from the old tests, but if old tests stop
jadmanski9b6babf2009-04-21 17:57:40 +0000204 # matching up with new ones just give up
showard0fec8a02009-12-04 01:19:54 +0000205 if reparse and old_job_idx is not None:
206 job.index = old_job_idx
207 for test in job.tests:
208 test_idx = old_tests.pop((test.testname, test.subdir), None)
209 if test_idx is not None:
210 test.test_idx = test_idx
211 else:
212 tko_utils.dprint("! Reparse returned new test "
213 "testname=%r subdir=%r" %
214 (test.testname, test.subdir))
215 for test_idx in old_tests.itervalues():
216 where = {'test_idx' : test_idx}
jamesrene660ed82010-08-05 19:57:46 +0000217 db.delete('tko_iteration_result', where)
Dennis Jeffrey368c54b2013-07-24 11:19:03 -0700218 db.delete('tko_iteration_perf_value', where)
jamesrene660ed82010-08-05 19:57:46 +0000219 db.delete('tko_iteration_attributes', where)
220 db.delete('tko_test_attributes', where)
221 db.delete('tko_test_labels_tests', {'test_id': test_idx})
222 db.delete('tko_tests', where)
mbligh96cf0512008-04-17 15:25:38 +0000223
jadmanski0afbb632008-06-06 21:10:57 +0000224 # check for failures
225 message_lines = [""]
226 for test in job.tests:
227 if not test.subdir:
228 continue
229 tko_utils.dprint("* testname, status, reason: %s %s %s"
230 % (test.subdir, test.status, test.reason))
231 if test.status in ("FAIL", "WARN"):
232 message_lines.append(format_failure_message(
233 jobname, test.kernel.base, test.subdir,
234 test.status, test.reason))
235 message = "\n".join(message_lines)
mbligh96cf0512008-04-17 15:25:38 +0000236
jadmanski0afbb632008-06-06 21:10:57 +0000237 # send out a email report of failure
238 if len(message) > 2 and mail_on_failure:
239 tko_utils.dprint("Sending email report of failure on %s to %s"
240 % (jobname, job.user))
241 mailfailure(jobname, job, message)
mbligh96cf0512008-04-17 15:25:38 +0000242
Fang Deng9ec66802014-04-28 19:04:33 +0000243 # write the job into the database.
jadmanski0afbb632008-06-06 21:10:57 +0000244 db.insert_job(jobname, job)
jamesren7a522042010-06-10 22:53:55 +0000245
Dennis Jeffreyf9bef6c2013-08-05 11:01:27 -0700246 # Upload perf values to the perf dashboard, if applicable.
247 for test in job.tests:
248 perf_uploader.upload_test(job, test)
249
Fang Deng9ec66802014-04-28 19:04:33 +0000250 # Although the cursor has autocommit, we still need to force it to commit
251 # existing changes before we can use django models, otherwise it
252 # will go into deadlock when django models try to start a new trasaction
253 # while the current one has not finished yet.
254 db.commit()
255
256 # Handle retry job.
257 orig_afe_job_id = job_keyval.get(constants.RETRY_ORIGINAL_JOB_ID, None)
258 if orig_afe_job_id:
259 orig_job_idx = tko_models.Job.objects.get(
260 afe_job_id=orig_afe_job_id).job_idx
261 _invalidate_original_tests(orig_job_idx, job.index)
262
jamesren7a522042010-06-10 22:53:55 +0000263 # Serializing job into a binary file
264 try:
265 from autotest_lib.tko import tko_pb2
266 from autotest_lib.tko import job_serializer
267
268 serializer = job_serializer.JobSerializer()
jamesren4826cc42010-06-15 20:33:22 +0000269 binary_file_name = os.path.join(path, "job.serialize")
270 serializer.serialize_to_binary(job, jobname, binary_file_name)
271
272 if reparse:
273 site_export_file = "autotest_lib.tko.site_export"
274 site_export = utils.import_site_function(__file__,
275 site_export_file,
276 "site_export",
277 _site_export_dummy)
278 site_export(binary_file_name)
279
jamesren7a522042010-06-10 22:53:55 +0000280 except ImportError:
281 tko_utils.dprint("DEBUG: tko_pb2.py doesn't exist. Create by "
282 "compiling tko/tko.proto.")
283
jadmanski0afbb632008-06-06 21:10:57 +0000284 db.commit()
mbligh26b992b2008-02-19 15:46:21 +0000285
jamesren4826cc42010-06-15 20:33:22 +0000286def _site_export_dummy(binary_file_name):
287 pass
mbligh26b992b2008-02-19 15:46:21 +0000288
jadmanski8e9c2572008-11-11 00:29:02 +0000289def _get_job_subdirs(path):
290 """
291 Returns a list of job subdirectories at path. Returns None if the test
292 is itself a job directory. Does not recurse into the subdirs.
293 """
294 # if there's a .machines file, use it to get the subdirs
jadmanski0afbb632008-06-06 21:10:57 +0000295 machine_list = os.path.join(path, ".machines")
296 if os.path.exists(machine_list):
jadmanski42fbd072009-01-30 15:07:05 +0000297 subdirs = set(line.strip() for line in file(machine_list))
298 existing_subdirs = set(subdir for subdir in subdirs
299 if os.path.exists(os.path.join(path, subdir)))
300 if len(existing_subdirs) != 0:
301 return existing_subdirs
jadmanski8e9c2572008-11-11 00:29:02 +0000302
303 # if this dir contains ONLY subdirectories, return them
304 contents = set(os.listdir(path))
305 contents.discard(".parse.lock")
306 subdirs = set(sub for sub in contents if
307 os.path.isdir(os.path.join(path, sub)))
308 if len(contents) == len(subdirs) != 0:
309 return subdirs
310
311 # this is a job directory, or something else we don't understand
312 return None
313
314
mbligha48eeb22009-03-11 16:44:43 +0000315def parse_leaf_path(db, path, level, reparse, mail_on_failure):
316 job_elements = path.split("/")[-level:]
317 jobname = "/".join(job_elements)
318 try:
319 db.run_with_retry(parse_one, db, jobname, path, reparse,
320 mail_on_failure)
321 except Exception:
322 traceback.print_exc()
323
324
jadmanski8e9c2572008-11-11 00:29:02 +0000325def parse_path(db, path, level, reparse, mail_on_failure):
326 job_subdirs = _get_job_subdirs(path)
327 if job_subdirs is not None:
mbligha48eeb22009-03-11 16:44:43 +0000328 # parse status.log in current directory, if it exists. multi-machine
329 # synchronous server side tests record output in this directory. without
330 # this check, we do not parse these results.
331 if os.path.exists(os.path.join(path, 'status.log')):
332 parse_leaf_path(db, path, level, reparse, mail_on_failure)
jadmanski0afbb632008-06-06 21:10:57 +0000333 # multi-machine job
jadmanski8e9c2572008-11-11 00:29:02 +0000334 for subdir in job_subdirs:
335 jobpath = os.path.join(path, subdir)
336 parse_path(db, jobpath, level + 1, reparse, mail_on_failure)
jadmanski0afbb632008-06-06 21:10:57 +0000337 else:
338 # single machine job
mbligha48eeb22009-03-11 16:44:43 +0000339 parse_leaf_path(db, path, level, reparse, mail_on_failure)
mblighbb7b8912006-10-08 03:59:02 +0000340
341
mbligh96cf0512008-04-17 15:25:38 +0000342def main():
jadmanski0afbb632008-06-06 21:10:57 +0000343 options, args = parse_args()
344 results_dir = os.path.abspath(args[0])
345 assert os.path.exists(results_dir)
mbligh96cf0512008-04-17 15:25:38 +0000346
jadmanskid5ab8c52008-12-03 16:27:07 +0000347 pid_file_manager = pidfile.PidFileManager("parser", results_dir)
mbligh96cf0512008-04-17 15:25:38 +0000348
jadmanskid5ab8c52008-12-03 16:27:07 +0000349 if options.write_pidfile:
350 pid_file_manager.open_file()
mbligh96cf0512008-04-17 15:25:38 +0000351
jadmanskid5ab8c52008-12-03 16:27:07 +0000352 try:
353 # build up the list of job dirs to parse
354 if options.singledir:
355 jobs_list = [results_dir]
356 else:
357 jobs_list = [os.path.join(results_dir, subdir)
358 for subdir in os.listdir(results_dir)]
359
360 # build up the database
361 db = tko_db.db(autocommit=False, host=options.db_host,
362 user=options.db_user, password=options.db_pass,
363 database=options.db_name)
364
365 # parse all the jobs
366 for path in jobs_list:
367 lockfile = open(os.path.join(path, ".parse.lock"), "w")
368 flags = fcntl.LOCK_EX
369 if options.noblock:
mblighdb18b0e2009-01-30 00:34:32 +0000370 flags |= fcntl.LOCK_NB
jadmanskid5ab8c52008-12-03 16:27:07 +0000371 try:
372 fcntl.flock(lockfile, flags)
373 except IOError, e:
mblighdb18b0e2009-01-30 00:34:32 +0000374 # lock is not available and nonblock has been requested
jadmanskid5ab8c52008-12-03 16:27:07 +0000375 if e.errno == errno.EWOULDBLOCK:
376 lockfile.close()
377 continue
378 else:
379 raise # something unexpected happened
380 try:
381 parse_path(db, path, options.level, options.reparse,
382 options.mailit)
mbligh9e936402009-05-13 20:42:17 +0000383
jadmanskid5ab8c52008-12-03 16:27:07 +0000384 finally:
385 fcntl.flock(lockfile, fcntl.LOCK_UN)
jadmanski0afbb632008-06-06 21:10:57 +0000386 lockfile.close()
mblighe97e0e62009-05-21 01:41:58 +0000387
jadmanskid5ab8c52008-12-03 16:27:07 +0000388 except:
389 pid_file_manager.close_file(1)
390 raise
391 else:
392 pid_file_manager.close_file(0)
mbligh71d340d2008-03-05 15:51:16 +0000393
mbligh532cb272007-11-26 18:54:20 +0000394
mbligh96cf0512008-04-17 15:25:38 +0000395if __name__ == "__main__":
jadmanski0afbb632008-06-06 21:10:57 +0000396 main()