blob: dfb667008372906f50c01475f39b442c95c6e406 [file] [log] [blame]
Caroline Tice48462062016-11-18 16:49:00 -08001#!/usr/bin/env python2
2"""Generate summary report for ChromeOS toolchain waterfalls."""
3
4# Desired future features (to be added):
5# - arguments to allow generating only the main waterfall report,
6# or only the rotating builder reports, or only the failures
7# report; or the waterfall reports without the failures report.
8# - Better way of figuring out which dates/builds to generate
9# reports for: probably an argument specifying a date or a date
10# range, then use something like the new buildbot utils to
11# query the build logs to find the right build numbers for the
12# builders for the specified dates.
13# - Store/get the json/data files in mobiletc-prebuild's x20 area.
14# - Update data in json file to reflect, for each testsuite, which
15# tests are not expected to run on which boards; update this
16# script to use that data appropriately.
17# - Make sure user's prodaccess is up-to-date before trying to use
18# this script.
19# - Add some nice formatting/highlighting to reports.
20
21from __future__ import print_function
22
23import json
24import os
25import sys
26import time
27
28from cros_utils import command_executer
29
30# All the test suites whose data we might want for the reports.
31TESTS = (
32 ('bvt-inline', 'HWTest'),
33 ('bvt-cq', 'HWTest'),
34 ('toolchain-tests', 'HWTest'),
35 ('security', 'HWTest'),
36 ('kernel_daily_regression', 'HWTest'),
37 ('kernel_daily_benchmarks', 'HWTest'),)
38
39# The main waterfall builders, IN THE ORDER IN WHICH WE WANT THEM
40# LISTED IN THE REPORT.
41WATERFALL_BUILDERS = [
42 'amd64-gcc-toolchain', 'arm-gcc-toolchain', 'arm64-gcc-toolchain',
43 'x86-gcc-toolchain', 'amd64-llvm-toolchain', 'arm-llvm-toolchain',
44 'arm64-llvm-toolchain', 'x86-llvm-toolchain', 'amd64-llvm-next-toolchain',
45 'arm-llvm-next-toolchain', 'arm64-llvm-next-toolchain',
46 'x86-llvm-next-toolchain'
47]
48
49ROLE_ACCOUNT = 'mobiletc-prebuild'
50DATA_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/waterfall-report-data/'
51DOWNLOAD_DIR = '/tmp/waterfall-logs'
52MAX_SAVE_RECORDS = 5
53BUILD_DATA_FILE = '%s/build-data.txt' % DATA_DIR
54ROTATING_BUILDERS = ['gcc_toolchain', 'llvm_toolchain']
55
56# For int-to-string date conversion. Note, the index of the month in this
57# list needs to correspond to the month's integer value. i.e. 'Sep' must
58# be as MONTHS[9].
59MONTHS = [
60 '', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct',
61 'Nov', 'Dec'
62]
63
64
65def format_date(int_date):
66 """Convert an integer date to a string date. YYYYMMDD -> YYYY-MMM-DD"""
67
68 if int_date == 0:
69 return 'today'
70
71 tmp_date = int_date
72 day = tmp_date % 100
73 tmp_date = tmp_date / 100
74 month = tmp_date % 100
75 year = tmp_date / 100
76
77 month_str = MONTHS[month]
78 date_str = '%d-%s-%d' % (year, month_str, day)
79 return date_str
80
81
Caroline Tice48462062016-11-18 16:49:00 -080082def GenerateWaterfallReport(report_dict, fail_dict, waterfall_type, date):
83 """Write out the actual formatted report."""
84
85 filename = 'waterfall_report.%s_waterfall.%s.txt' % (waterfall_type, date)
86
87 date_string = ''
88 date_list = report_dict['date']
89 num_dates = len(date_list)
90 i = 0
91 for d in date_list:
92 date_string += d
93 if i < num_dates - 1:
94 date_string += ', '
95 i += 1
96
97 if waterfall_type == 'main':
98 report_list = WATERFALL_BUILDERS
99 else:
100 report_list = report_dict.keys()
101
102 with open(filename, 'w') as out_file:
103 # Write Report Header
104 out_file.write('\nStatus of %s Waterfall Builds from %s\n\n' %
105 (waterfall_type, date_string))
106 out_file.write(' '
107 ' kernel kernel\n')
108 out_file.write(' Build bvt- bvt-cq '
109 'toolchain- security daily daily\n')
110 out_file.write(' status inline '
111 ' tests regression benchmarks\n')
112 out_file.write(' [P/ F/ DR]* [P/ F /DR]* '
113 '[P/ F/ DR]* [P/ F/ DR]* [P/ F/ DR]* [P/ F/ DR]*\n\n')
114
115 # Write daily waterfall status section.
116 for i in range(0, len(report_list)):
117 builder = report_list[i]
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800118 if builder == 'date':
119 continue
Caroline Tice48462062016-11-18 16:49:00 -0800120
121 if builder not in report_dict:
122 out_file.write('Unable to find information for %s.\n\n' % builder)
123 continue
124
125 build_dict = report_dict[builder]
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800126 status = build_dict.get('build_status', 'bad')
127 inline = build_dict.get('bvt-inline', '[??/ ?? /??]')
128 cq = build_dict.get('bvt-cq', '[??/ ?? /??]')
129 inline_color = build_dict.get('bvt-inline-color', '')
130 cq_color = build_dict.get('bvt-cq-color', '')
Caroline Tice48462062016-11-18 16:49:00 -0800131 if 'x86' not in builder:
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800132 toolchain = build_dict.get('toolchain-tests', '[??/ ?? /??]')
133 security = build_dict.get('security', '[??/ ?? /??]')
134 toolchain_color = build_dict.get('toolchain-tests-color', '')
135 security_color = build_dict.get('security-color', '')
Caroline Tice48462062016-11-18 16:49:00 -0800136 if 'gcc' in builder:
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800137 regression = build_dict.get('kernel_daily_regression', '[??/ ?? /??]')
138 bench = build_dict.get('kernel_daily_benchmarks', '[??/ ?? /??]')
139 regression_color = build_dict.get('kernel_daily_regression-color', '')
140 bench_color = build_dict.get('kernel_daily_benchmarks-color', '')
141 out_file.write(' %6s %6s'
142 ' %6s %6s %6s %6s\n' %
143 (inline_color, cq_color, toolchain_color,
144 security_color, regression_color, bench_color))
Caroline Tice48462062016-11-18 16:49:00 -0800145 out_file.write('%25s %3s %s %s %s %s %s %s\n' % (builder, status,
146 inline, cq,
147 toolchain, security,
148 regression, bench))
149 else:
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800150 out_file.write(' %6s %6s'
151 ' %6s %6s\n' % (inline_color, cq_color,
152 toolchain_color,
153 security_color))
Caroline Tice48462062016-11-18 16:49:00 -0800154 out_file.write('%25s %3s %s %s %s %s\n' % (builder, status, inline,
155 cq, toolchain, security))
156 else:
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800157 out_file.write(' %6s %6s\n' %
158 (inline_color, cq_color))
Caroline Tice48462062016-11-18 16:49:00 -0800159 out_file.write('%25s %3s %s %s\n' % (builder, status, inline, cq))
160 if 'build_link' in build_dict:
161 out_file.write('%s\n\n' % build_dict['build_link'])
162
163 out_file.write('\n\n*P = Number of tests in suite that Passed; F = '
164 'Number of tests in suite that Failed; DR = Number of tests'
165 ' in suite that Didn\'t Run.\n')
166
167 # Write failure report section.
168 out_file.write('\n\nSummary of Test Failures as of %s\n\n' % date_string)
169
170 # We want to sort the errors and output them in order of the ones that occur
171 # most often. So we have to collect the data about all of them, then sort
172 # it.
173 error_groups = []
174 for suite in fail_dict:
175 suite_dict = fail_dict[suite]
176 if suite_dict:
177 for test in suite_dict:
178 test_dict = suite_dict[test]
179 for err_msg in test_dict:
180 err_list = test_dict[err_msg]
181 sorted_list = sorted(err_list, key=lambda x: x[0], reverse=True)
182 err_group = [len(sorted_list), suite, test, err_msg, sorted_list]
183 error_groups.append(err_group)
184
185 # Sort the errors by the number of errors of each type. Then output them in
186 # order.
187 sorted_errors = sorted(error_groups, key=lambda x: x[0], reverse=True)
188 for i in range(0, len(sorted_errors)):
189 err_group = sorted_errors[i]
190 suite = err_group[1]
191 test = err_group[2]
192 err_msg = err_group[3]
193 err_list = err_group[4]
194 out_file.write('Suite: %s\n' % suite)
195 out_file.write(' %s (%d failures)\n' % (test, len(err_list)))
196 out_file.write(' (%s)\n' % err_msg)
197 for i in range(0, len(err_list)):
198 err = err_list[i]
199 out_file.write(' %s, %s, %s\n' % (format_date(err[0]), err[1],
200 err[2]))
201 out_file.write('\n')
202
203 print('Report generated in %s.' % filename)
204
205
206def UpdateReport(report_dict, builder, test, report_date, build_link,
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800207 test_summary, board, color):
Caroline Tice48462062016-11-18 16:49:00 -0800208 """Update the data in our report dictionary with current test's data."""
209
210 if 'date' not in report_dict:
211 report_dict['date'] = [report_date]
212 elif report_date not in report_dict['date']:
213 # It is possible that some of the builders started/finished on different
214 # days, so we allow for multiple dates in the reports.
215 report_dict['date'].append(report_date)
216
217 build_key = ''
218 if builder == 'gcc_toolchain':
219 build_key = '%s-gcc-toolchain' % board
220 elif builder == 'llvm_toolchain':
221 build_key = '%s-llvm-toolchain' % board
222 else:
223 build_key = builder
224
225 if build_key not in report_dict.keys():
226 build_dict = dict()
227 else:
228 build_dict = report_dict[build_key]
229
230 if 'build_link' not in build_dict:
231 build_dict['build_link'] = build_link
232
233 if 'date' not in build_dict:
234 build_dict['date'] = report_date
235
236 if 'board' in build_dict and build_dict['board'] != board:
237 raise RuntimeError('Error: Two different boards (%s,%s) in one build (%s)!'
238 % (board, build_dict['board'], build_link))
239 build_dict['board'] = board
240
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800241 color_key = '%s-color' % test
242 build_dict[color_key] = color
243
Caroline Tice48462062016-11-18 16:49:00 -0800244 # Check to see if we already have a build status for this build_key
245 status = ''
246 if 'build_status' in build_dict.keys():
247 # Use current build_status, unless current test failed (see below).
248 status = build_dict['build_status']
249
250 if not test_summary:
251 # Current test data was not available, so something was bad with build.
252 build_dict['build_status'] = 'bad'
253 build_dict[test] = '[ no data ]'
254 else:
255 build_dict[test] = test_summary
256 if not status:
257 # Current test ok; no other data, so assume build was ok.
258 build_dict['build_status'] = 'ok'
259
260 report_dict[build_key] = build_dict
261
262
263def UpdateBuilds(builds):
264 """Update the data in our build-data.txt file."""
265
266 # The build data file records the last build number for which we
267 # generated a report. When we generate the next report, we read
268 # this data and increment it to get the new data; when we finish
269 # generating the reports, we write the updated values into this file.
270 # NOTE: One side effect of doing this at the end: If the script
271 # fails in the middle of generating a report, this data does not get
272 # updated.
273 with open(BUILD_DATA_FILE, 'w') as fp:
274 gcc_max = 0
275 llvm_max = 0
276 for b in builds:
277 if b[0] == 'gcc_toolchain':
278 gcc_max = max(gcc_max, b[1])
279 elif b[0] == 'llvm_toolchain':
280 llvm_max = max(llvm_max, b[1])
281 else:
282 fp.write('%s,%d\n' % (b[0], b[1]))
283 if gcc_max > 0:
284 fp.write('gcc_toolchain,%d\n' % gcc_max)
285 if llvm_max > 0:
286 fp.write('llvm_toolchain,%d\n' % llvm_max)
287
288
289def GetBuilds():
290 """Read build-data.txt to determine values for current report."""
291
292 # Read the values of the last builds used to generate a report, and
293 # increment them appropriately, to get values for generating the
294 # current report. (See comments in UpdateBuilds).
295 with open(BUILD_DATA_FILE, 'r') as fp:
296 lines = fp.readlines()
297
298 builds = []
299 for l in lines:
300 l = l.rstrip()
301 words = l.split(',')
302 builder = words[0]
303 build = int(words[1])
304 builds.append((builder, build + 1))
305 # NOTE: We are assuming here that there are always 2 daily builds in
306 # each of the rotating builders. I am not convinced this is a valid
307 # assumption.
308 if builder == 'gcc_toolchain' or builder == 'llvm_toolchain':
309 builds.append((builder, build + 2))
310
311 return builds
312
313
314def RecordFailures(failure_dict, platform, suite, builder, int_date, log_file,
315 build_num, failed):
316 """Read and update the stored data about test failures."""
317
318 # Get the dictionary for this particular test suite from the failures
319 # dictionary.
320 suite_dict = failure_dict[suite]
321
322 # Read in the entire log file for this test/build.
323 with open(log_file, 'r') as in_file:
324 lines = in_file.readlines()
325
326 # Update the entries in the failure dictionary for each test within this suite
327 # that failed.
328 for test in failed:
329 # Check to see if there is already an entry in the suite dictionary for this
330 # test; if so use that, otherwise create a new entry.
331 if test in suite_dict:
332 test_dict = suite_dict[test]
333 else:
334 test_dict = dict()
335 # Parse the lines from the log file, looking for lines that indicate this
336 # test failed.
337 msg = ''
338 for l in lines:
339 words = l.split()
340 if len(words) < 3:
341 continue
342 if ((words[0] == test and words[1] == 'ERROR:') or
343 (words[0] == 'provision' and words[1] == 'FAIL:')):
344 words = words[2:]
345 # Get the error message for the failure.
346 msg = ' '.join(words)
347 if not msg:
348 msg = 'Unknown_Error'
349
350 # Look for an existing entry for this error message in the test dictionary.
351 # If found use that, otherwise create a new entry for this error message.
352 if msg in test_dict:
353 error_list = test_dict[msg]
354 else:
355 error_list = list()
356 # Create an entry for this new failure
357 new_item = [int_date, platform, builder, build_num]
358 # Add this failure to the error list if it's not already there.
359 if new_item not in error_list:
360 error_list.append([int_date, platform, builder, build_num])
361 # Sort the error list by date.
362 error_list.sort(key=lambda x: x[0])
363 # Calculate the earliest date to save; delete records for older failures.
364 earliest_date = int_date - MAX_SAVE_RECORDS
365 i = 0
366 while error_list[i][0] <= earliest_date and i < len(error_list):
367 i += 1
368 if i > 0:
369 error_list = error_list[i:]
370 # Save the error list in the test's dictionary, keyed on error_msg.
371 test_dict[msg] = error_list
372
373 # Save the updated test dictionary in the test_suite dictionary.
374 suite_dict[test] = test_dict
375
376 # Save the updated test_suite dictionary in the failure dictionary.
377 failure_dict[suite] = suite_dict
378
379
380def ParseLogFile(log_file, test_data_dict, failure_dict, test, builder,
381 build_num, build_link):
382 """Parse the log file from the given builder, build_num and test.
383
384 Also adds the results for this test to our test results dictionary,
385 and calls RecordFailures, to update our test failure data.
386 """
387
388 lines = []
389 with open(log_file, 'r') as infile:
390 lines = infile.readlines()
391
392 passed = {}
393 failed = {}
394 not_run = {}
395 date = ''
396 status = ''
397 board = ''
398 num_provision_errors = 0
399 build_ok = True
400 afe_line = ''
401
402 for line in lines:
403 if line.rstrip() == '<title>404 Not Found</title>':
404 print('Warning: File for %s (build number %d), %s was not found.' %
405 (builder, build_num, test))
406 build_ok = False
407 break
408 if '[ PASSED ]' in line:
409 test_name = line.split()[0]
410 if test_name != 'Suite':
411 passed[test_name] = True
412 elif '[ FAILED ]' in line:
413 test_name = line.split()[0]
414 if test_name == 'provision':
415 num_provision_errors += 1
416 not_run[test_name] = True
417 elif test_name != 'Suite':
418 failed[test_name] = True
419 elif line.startswith('started: '):
420 date = line.rstrip()
421 date = date[9:]
422 date_obj = time.strptime(date, '%a %b %d %H:%M:%S %Y')
423 int_date = (
424 date_obj.tm_year * 10000 + date_obj.tm_mon * 100 + date_obj.tm_mday)
425 date = time.strftime('%a %b %d %Y', date_obj)
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800426 elif not status and line.startswith('status: '):
Caroline Tice48462062016-11-18 16:49:00 -0800427 status = line.rstrip()
428 words = status.split(':')
429 status = words[-1]
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800430 elif line.find('Suite passed with a warning') != -1:
431 status = 'WARNING'
Caroline Tice48462062016-11-18 16:49:00 -0800432 elif line.startswith('@@@STEP_LINK@Link to suite@'):
433 afe_line = line.rstrip()
434 words = afe_line.split('@')
435 for w in words:
436 if w.startswith('http'):
437 afe_line = w
438 afe_line = afe_line.replace('&amp;', '&')
439 elif 'INFO: RunCommand:' in line:
440 words = line.split()
441 for i in range(0, len(words) - 1):
442 if words[i] == '--board':
443 board = words[i + 1]
444
445 test_dict = test_data_dict[test]
446 test_list = test_dict['tests']
447
448 if build_ok:
449 for t in test_list:
450 if not t in passed and not t in failed:
451 not_run[t] = True
452
453 total_pass = len(passed)
454 total_fail = len(failed)
455 total_notrun = len(not_run)
456
457 else:
458 total_pass = 0
459 total_fail = 0
460 total_notrun = 0
461 status = 'Not found.'
462 if not build_ok:
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800463 return [], date, board, 0, ' '
Caroline Tice48462062016-11-18 16:49:00 -0800464
465 build_dict = dict()
466 build_dict['id'] = build_num
467 build_dict['builder'] = builder
468 build_dict['date'] = date
469 build_dict['build_link'] = build_link
470 build_dict['total_pass'] = total_pass
471 build_dict['total_fail'] = total_fail
472 build_dict['total_not_run'] = total_notrun
473 build_dict['afe_job_link'] = afe_line
474 build_dict['provision_errors'] = num_provision_errors
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800475 if status.strip() == 'SUCCESS':
476 build_dict['color'] = 'green '
477 elif status.strip() == 'FAILURE':
478 build_dict['color'] = ' red '
479 elif status.strip() == 'WARNING':
480 build_dict['color'] = 'orange'
481 else:
482 build_dict['color'] = ' '
Caroline Tice48462062016-11-18 16:49:00 -0800483
484 # Use YYYYMMDD (integer) as the build record key
485 if build_ok:
486 if board in test_dict:
487 board_dict = test_dict[board]
488 else:
489 board_dict = dict()
490 board_dict[int_date] = build_dict
491
492 # Only keep the last 5 records (based on date)
493 keys_list = board_dict.keys()
494 if len(keys_list) > MAX_SAVE_RECORDS:
495 min_key = min(keys_list)
496 del board_dict[min_key]
497
498 # Make sure changes get back into the main dictionary
499 test_dict[board] = board_dict
500 test_data_dict[test] = test_dict
501
502 if len(failed) > 0:
503 RecordFailures(failure_dict, board, test, builder, int_date, log_file,
504 build_num, failed)
505
506 summary_result = '[%2d/ %2d/ %2d]' % (total_pass, total_fail, total_notrun)
507
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800508 return summary_result, date, board, int_date, build_dict['color']
Caroline Tice48462062016-11-18 16:49:00 -0800509
510
511def DownloadLogFile(builder, buildnum, test, test_family):
512
513 ce = command_executer.GetCommandExecuter()
514 os.system('mkdir -p %s/%s/%s' % (DOWNLOAD_DIR, builder, test))
515 if builder == 'gcc_toolchain' or builder == 'llvm_toolchain':
516 source = ('https://uberchromegw.corp.google.com/i/chromiumos.tryserver'
517 '/builders/%s/builds/%d/steps/%s%%20%%5B%s%%5D/logs/stdio' %
518 (builder, buildnum, test_family, test))
519 build_link = ('https://uberchromegw.corp.google.com/i/chromiumos.tryserver'
520 '/builders/%s/builds/%d' % (builder, buildnum))
521 else:
522 source = ('https://uberchromegw.corp.google.com/i/chromeos/builders/%s/'
523 'builds/%d/steps/%s%%20%%5B%s%%5D/logs/stdio' %
524 (builder, buildnum, test_family, test))
525 build_link = ('https://uberchromegw.corp.google.com/i/chromeos/builders/%s'
526 '/builds/%d' % (builder, buildnum))
527
528 target = '%s/%s/%s/%d' % (DOWNLOAD_DIR, builder, test, buildnum)
529 if not os.path.isfile(target) or os.path.getsize(target) == 0:
530 cmd = 'sso_client %s > %s' % (source, target)
531 status = ce.RunCommand(cmd)
532 if status != 0:
533 return '', ''
534
535 return target, build_link
536
537
538def Main():
539 """Main function for this script."""
540
541 test_data_dict = dict()
542 failure_dict = dict()
543 with open('%s/waterfall-test-data.json' % DATA_DIR, 'r') as input_file:
544 test_data_dict = json.load(input_file)
545
546 with open('%s/test-failure-data.json' % DATA_DIR, 'r') as fp:
547 failure_dict = json.load(fp)
548
549 builds = GetBuilds()
550
551 waterfall_report_dict = dict()
552 rotating_report_dict = dict()
553 int_date = 0
554 for test_desc in TESTS:
555 test, test_family = test_desc
556 for build in builds:
557 (builder, buildnum) = build
558 if test.startswith('kernel') and 'llvm' in builder:
559 continue
560 if 'x86' in builder and not test.startswith('bvt'):
561 continue
562 target, build_link = DownloadLogFile(builder, buildnum, test, test_family)
563
564 if os.path.exists(target):
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800565 test_summary, report_date, board, tmp_date, color = ParseLogFile(
Caroline Tice48462062016-11-18 16:49:00 -0800566 target, test_data_dict, failure_dict, test, builder, buildnum,
567 build_link)
568
569 if tmp_date != 0:
570 int_date = tmp_date
571
572 if builder in ROTATING_BUILDERS:
573 UpdateReport(rotating_report_dict, builder, test, report_date,
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800574 build_link, test_summary, board, color)
Caroline Tice48462062016-11-18 16:49:00 -0800575 else:
576 UpdateReport(waterfall_report_dict, builder, test, report_date,
Caroline Ticef0ad65c2016-11-29 10:40:23 -0800577 build_link, test_summary, board, color)
Caroline Tice48462062016-11-18 16:49:00 -0800578
579 if waterfall_report_dict:
580 GenerateWaterfallReport(waterfall_report_dict, failure_dict, 'main',
581 int_date)
582 if rotating_report_dict:
583 GenerateWaterfallReport(rotating_report_dict, failure_dict, 'rotating',
584 int_date)
585
586 with open('%s/waterfall-test-data.json' % DATA_DIR, 'w') as out_file:
587 json.dump(test_data_dict, out_file, indent=2)
588
589 with open('%s/test-failure-data.json' % DATA_DIR, 'w') as out_file:
590 json.dump(failure_dict, out_file, indent=2)
591
592 UpdateBuilds(builds)
593
594
595if __name__ == '__main__':
596 Main()
597 sys.exit(0)