generate-waterfall-reports.py revision faa3c555dd0b636cdd19a42399b152fc77b8cbe3
1#!/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 argparse
24import getpass
25import json
26import os
27import shutil
28import sys
29import time
30
31from cros_utils import command_executer
32
33# All the test suites whose data we might want for the reports.
34TESTS = (
35    ('bvt-inline', 'HWTest'),
36    ('bvt-cq', 'HWTest'),
37    ('toolchain-tests', 'HWTest'),
38    ('security', 'HWTest'),
39    ('kernel_daily_regression', 'HWTest'),
40    ('kernel_daily_benchmarks', 'HWTest'),)
41
42# The main waterfall builders, IN THE ORDER IN WHICH WE WANT THEM
43# LISTED IN THE REPORT.
44WATERFALL_BUILDERS = [
45    'amd64-gcc-toolchain', 'arm-gcc-toolchain', 'arm64-gcc-toolchain',
46    'x86-gcc-toolchain', 'amd64-llvm-toolchain', 'arm-llvm-toolchain',
47    'arm64-llvm-toolchain', 'x86-llvm-toolchain', 'amd64-llvm-next-toolchain',
48    'arm-llvm-next-toolchain', 'arm64-llvm-next-toolchain',
49    'x86-llvm-next-toolchain'
50]
51
52DATA_DIR = '/usr/local/google2/cmtice/toolchain-utils/waterfall-report-data/'
53ARCHIVE_DIR = '/usr/local/google2/cmtice/toolchain-utils/waterfall-reports/'
54#DATA_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/waterfall-report-data/'
55#ARCHIVE_DIR = '/google/data/rw/users/mo/mobiletc-prebuild/waterfall-reports/'
56DOWNLOAD_DIR = '/tmp/waterfall-logs'
57MAX_SAVE_RECORDS = 7
58BUILD_DATA_FILE = '%s/build-data.txt' % DATA_DIR
59GCC_ROTATING_BUILDER = 'gcc_toolchain'
60LLVM_ROTATING_BUILDER = 'llvm_next_toolchain'
61ROTATING_BUILDERS = [GCC_ROTATING_BUILDER, LLVM_ROTATING_BUILDER]
62
63# For int-to-string date conversion.  Note, the index of the month in this
64# list needs to correspond to the month's integer value.  i.e. 'Sep' must
65# be as MONTHS[9].
66MONTHS = [
67    '', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct',
68    'Nov', 'Dec'
69]
70
71
72def format_date(int_date):
73  """Convert an integer date to a string date. YYYYMMDD -> YYYY-MMM-DD"""
74
75  if int_date == 0:
76    return 'today'
77
78  tmp_date = int_date
79  day = tmp_date % 100
80  tmp_date = tmp_date / 100
81  month = tmp_date % 100
82  year = tmp_date / 100
83
84  month_str = MONTHS[month]
85  date_str = '%d-%s-%d' % (year, month_str, day)
86  return date_str
87
88
89def EmailReport(report_file, report_type, date):
90  subject = '%s Waterfall Summary report, %s' % (report_type, date)
91  email_to = getpass.getuser()
92  sendgmr_path = '/google/data/ro/projects/gws-sre/sendgmr'
93  command = ('%s --to=%s@google.com --subject="%s" --body_file=%s' %
94             (sendgmr_path, email_to, subject, report_file))
95  command_executer.GetCommandExecuter().RunCommand(command)
96
97
98def PruneOldFailures(failure_dict, int_date):
99  earliest_date = int_date - MAX_SAVE_RECORDS
100  for suite in failure_dict:
101    suite_dict = failure_dict[suite]
102    test_keys_to_remove = []
103    for test in suite_dict:
104      test_dict = suite_dict[test]
105      msg_keys_to_remove = []
106      for msg in test_dict:
107        fails = test_dict[msg]
108        i = 0
109        while i < len(fails) and fails[i][0] <= earliest_date:
110          i += 1
111        new_fails = fails[i:]
112        test_dict[msg] = new_fails
113        if len(new_fails) == 0:
114          msg_keys_to_remove.append(msg)
115
116      for k in msg_keys_to_remove:
117        del test_dict[k]
118
119      suite_dict[test] = test_dict
120      if len(test_dict) == 0:
121        test_keys_to_remove.append(test)
122
123    for k in test_keys_to_remove:
124      del suite_dict[k]
125
126    failure_dict[suite] = suite_dict
127
128
129def GenerateFailuresReport(fail_dict, date):
130  filename = 'waterfall_report.failures.%s.txt' % date
131  date_string = format_date(date)
132  with open(filename, 'w') as out_file:
133    # Write failure report section.
134    out_file.write('\n\nSummary of Test Failures as of %s\n\n' % date_string)
135
136    # We want to sort the errors and output them in order of the ones that occur
137    # most often.  So we have to collect the data about all of them, then sort
138    # it.
139    error_groups = []
140    for suite in fail_dict:
141      suite_dict = fail_dict[suite]
142      if suite_dict:
143        for test in suite_dict:
144          test_dict = suite_dict[test]
145          for err_msg in test_dict:
146            err_list = test_dict[err_msg]
147            sorted_list = sorted(err_list, key=lambda x: x[0], reverse=True)
148            err_group = [len(sorted_list), suite, test, err_msg, sorted_list]
149            error_groups.append(err_group)
150
151    # Sort the errors by the number of errors of each type. Then output them in
152    # order.
153    sorted_errors = sorted(error_groups, key=lambda x: x[0], reverse=True)
154    for i in range(0, len(sorted_errors)):
155      err_group = sorted_errors[i]
156      suite = err_group[1]
157      test = err_group[2]
158      err_msg = err_group[3]
159      err_list = err_group[4]
160      out_file.write('Suite: %s\n' % suite)
161      out_file.write('    %s (%d failures)\n' % (test, len(err_list)))
162      out_file.write('    (%s)\n' % err_msg)
163      for i in range(0, len(err_list)):
164        err = err_list[i]
165        out_file.write('        %s, %s, %s\n' % (format_date(err[0]), err[1],
166                                                 err[2]))
167      out_file.write('\n')
168
169  print('Report generated in %s.' % filename)
170  return filename
171
172
173def GenerateWaterfallReport(report_dict, fail_dict, waterfall_type, date,
174                            omit_failures):
175  """Write out the actual formatted report."""
176
177  filename = 'waterfall_report.%s_waterfall.%s.txt' % (waterfall_type, date)
178
179  date_string = ''
180  date_list = report_dict['date']
181  num_dates = len(date_list)
182  i = 0
183  for d in date_list:
184    date_string += d
185    if i < num_dates - 1:
186      date_string += ', '
187    i += 1
188
189  if waterfall_type == 'main':
190    report_list = WATERFALL_BUILDERS
191  else:
192    report_list = report_dict.keys()
193
194  with open(filename, 'w') as out_file:
195    # Write Report Header
196    out_file.write('\nStatus of %s Waterfall Builds from %s\n\n' %
197                   (waterfall_type, date_string))
198    out_file.write('                                                          '
199                   '                          kernel       kernel\n')
200    out_file.write('                         Build    bvt-         bvt-cq     '
201                   'toolchain-   security     daily        daily\n')
202    out_file.write('                         status  inline                   '
203                   '  tests                 regression   benchmarks\n')
204    out_file.write('                               [P/ F/ DR]*   [P/ F /DR]*  '
205                   '[P/ F/ DR]* [P/ F/ DR]* [P/ F/ DR]* [P/ F/ DR]*\n\n')
206
207    # Write daily waterfall status section.
208    for i in range(0, len(report_list)):
209      builder = report_list[i]
210      if builder == 'date':
211        continue
212
213      if builder not in report_dict:
214        out_file.write('Unable to find information for %s.\n\n' % builder)
215        continue
216
217      build_dict = report_dict[builder]
218      status = build_dict.get('build_status', 'bad')
219      inline = build_dict.get('bvt-inline', '[??/ ?? /??]')
220      cq = build_dict.get('bvt-cq', '[??/ ?? /??]')
221      inline_color = build_dict.get('bvt-inline-color', '')
222      cq_color = build_dict.get('bvt-cq-color', '')
223      if 'x86' not in builder:
224        toolchain = build_dict.get('toolchain-tests', '[??/ ?? /??]')
225        security = build_dict.get('security', '[??/ ?? /??]')
226        toolchain_color = build_dict.get('toolchain-tests-color', '')
227        security_color = build_dict.get('security-color', '')
228        if 'gcc' in builder:
229          regression = build_dict.get('kernel_daily_regression', '[??/ ?? /??]')
230          bench = build_dict.get('kernel_daily_benchmarks', '[??/ ?? /??]')
231          regression_color = build_dict.get('kernel_daily_regression-color', '')
232          bench_color = build_dict.get('kernel_daily_benchmarks-color', '')
233          out_file.write('                                  %6s        %6s'
234                         '       %6s      %6s      %6s      %6s\n' %
235                         (inline_color, cq_color, toolchain_color,
236                          security_color, regression_color, bench_color))
237          out_file.write('%25s %3s  %s %s %s %s %s %s\n' % (builder, status,
238                                                            inline, cq,
239                                                            toolchain, security,
240                                                            regression, bench))
241        else:
242          out_file.write('                                  %6s        %6s'
243                         '       %6s      %6s\n' % (inline_color, cq_color,
244                                                    toolchain_color,
245                                                    security_color))
246          out_file.write('%25s %3s  %s %s %s %s\n' % (builder, status, inline,
247                                                      cq, toolchain, security))
248      else:
249        out_file.write('                                  %6s        %6s\n' %
250                       (inline_color, cq_color))
251        out_file.write('%25s %3s  %s %s\n' % (builder, status, inline, cq))
252      if 'build_link' in build_dict:
253        out_file.write('%s\n\n' % build_dict['build_link'])
254
255    out_file.write('\n\n*P = Number of tests in suite that Passed; F = '
256                   'Number of tests in suite that Failed; DR = Number of tests'
257                   ' in suite that Didn\'t Run.\n')
258
259    if omit_failures:
260      print('Report generated in %s.' % filename)
261      return filename
262
263    # Write failure report section.
264    out_file.write('\n\nSummary of Test Failures as of %s\n\n' % date_string)
265
266    # We want to sort the errors and output them in order of the ones that occur
267    # most often.  So we have to collect the data about all of them, then sort
268    # it.
269    error_groups = []
270    for suite in fail_dict:
271      suite_dict = fail_dict[suite]
272      if suite_dict:
273        for test in suite_dict:
274          test_dict = suite_dict[test]
275          for err_msg in test_dict:
276            err_list = test_dict[err_msg]
277            sorted_list = sorted(err_list, key=lambda x: x[0], reverse=True)
278            err_group = [len(sorted_list), suite, test, err_msg, sorted_list]
279            error_groups.append(err_group)
280
281    # Sort the errors by the number of errors of each type. Then output them in
282    # order.
283    sorted_errors = sorted(error_groups, key=lambda x: x[0], reverse=True)
284    for i in range(0, len(sorted_errors)):
285      err_group = sorted_errors[i]
286      suite = err_group[1]
287      test = err_group[2]
288      err_msg = err_group[3]
289      err_list = err_group[4]
290      out_file.write('Suite: %s\n' % suite)
291      out_file.write('    %s (%d failures)\n' % (test, len(err_list)))
292      out_file.write('    (%s)\n' % err_msg)
293      for i in range(0, len(err_list)):
294        err = err_list[i]
295        out_file.write('        %s, %s, %s\n' % (format_date(err[0]), err[1],
296                                                 err[2]))
297      out_file.write('\n')
298
299  print('Report generated in %s.' % filename)
300  return filename
301
302
303def UpdateReport(report_dict, builder, test, report_date, build_link,
304                 test_summary, board, color):
305  """Update the data in our report dictionary with current test's data."""
306
307  if 'date' not in report_dict:
308    report_dict['date'] = [report_date]
309  elif report_date not in report_dict['date']:
310    # It is possible that some of the builders started/finished on different
311    # days, so we allow for multiple dates in the reports.
312    report_dict['date'].append(report_date)
313
314  build_key = ''
315  if builder == GCC_ROTATING_BUILDER:
316    build_key = '%s-gcc-toolchain' % board
317  elif builder == LLVM_ROTATING_BUILDER:
318    build_key = '%s-llvm-next-toolchain' % board
319  else:
320    build_key = builder
321
322  if build_key not in report_dict.keys():
323    build_dict = dict()
324  else:
325    build_dict = report_dict[build_key]
326
327  if 'build_link' not in build_dict:
328    build_dict['build_link'] = build_link
329
330  if 'date' not in build_dict:
331    build_dict['date'] = report_date
332
333  if 'board' in build_dict and build_dict['board'] != board:
334    raise RuntimeError('Error: Two different boards (%s,%s) in one build (%s)!'
335                       % (board, build_dict['board'], build_link))
336  build_dict['board'] = board
337
338  color_key = '%s-color' % test
339  build_dict[color_key] = color
340
341  # Check to see if we already have a build status for this build_key
342  status = ''
343  if 'build_status' in build_dict.keys():
344    # Use current build_status, unless current test failed (see below).
345    status = build_dict['build_status']
346
347  if not test_summary:
348    # Current test data was not available, so something was bad with build.
349    build_dict['build_status'] = 'bad'
350    build_dict[test] = '[  no data  ]'
351  else:
352    build_dict[test] = test_summary
353    if not status:
354      # Current test ok; no other data, so assume build was ok.
355      build_dict['build_status'] = 'ok'
356
357  report_dict[build_key] = build_dict
358
359
360def UpdateBuilds(builds):
361  """Update the data in our build-data.txt file."""
362
363  # The build data file records the last build number for which we
364  # generated a report.  When we generate the next report, we read
365  # this data and increment it to get the new data; when we finish
366  # generating the reports, we write the updated values into this file.
367  # NOTE: One side effect of doing this at the end:  If the script
368  # fails in the middle of generating a report, this data does not get
369  # updated.
370  with open(BUILD_DATA_FILE, 'w') as fp:
371    gcc_max = 0
372    llvm_max = 0
373    for b in builds:
374      if b[0] == GCC_ROTATING_BUILDER:
375        gcc_max = max(gcc_max, b[1])
376      elif b[0] == LLVM_ROTATING_BUILDER:
377        llvm_max = max(llvm_max, b[1])
378      else:
379        fp.write('%s,%d\n' % (b[0], b[1]))
380    if gcc_max > 0:
381      fp.write('%s,%d\n' % (GCC_ROTATING_BUILDER, gcc_max))
382    if llvm_max > 0:
383      fp.write('%s,%d\n' % (LLVM_ROTATING_BUILDER, llvm_max))
384
385
386def GetBuilds():
387  """Read build-data.txt to determine values for current report."""
388
389  # Read the values of the last builds used to generate a report, and
390  # increment them appropriately, to get values for generating the
391  # current report.  (See comments in UpdateBuilds).
392  with open(BUILD_DATA_FILE, 'r') as fp:
393    lines = fp.readlines()
394
395  builds = []
396  for l in lines:
397    l = l.rstrip()
398    words = l.split(',')
399    builder = words[0]
400    build = int(words[1])
401    builds.append((builder, build + 1))
402    # NOTE: We are assuming here that there are always 2 daily builds in
403    # each of the rotating builders.  I am not convinced this is a valid
404    # assumption.
405    if builder in ROTATING_BUILDERS:
406      builds.append((builder, build + 2))
407
408  return builds
409
410
411def RecordFailures(failure_dict, platform, suite, builder, int_date, log_file,
412                   build_num, failed):
413  """Read and update the stored data about test  failures."""
414
415  # Get the dictionary for this particular test suite from the failures
416  # dictionary.
417  suite_dict = failure_dict[suite]
418
419  # Read in the entire log file for this test/build.
420  with open(log_file, 'r') as in_file:
421    lines = in_file.readlines()
422
423  # Update the entries in the failure dictionary for each test within this suite
424  # that failed.
425  for test in failed:
426    # Check to see if there is already an entry in the suite dictionary for this
427    # test; if so use that, otherwise create a new entry.
428    if test in suite_dict:
429      test_dict = suite_dict[test]
430    else:
431      test_dict = dict()
432    # Parse the lines from the log file, looking for lines that indicate this
433    # test failed.
434    msg = ''
435    for l in lines:
436      words = l.split()
437      if len(words) < 3:
438        continue
439      if ((words[0] == test and words[1] == 'ERROR:') or
440          (words[0] == 'provision' and words[1] == 'FAIL:')):
441        words = words[2:]
442        # Get the error message for the failure.
443        msg = ' '.join(words)
444    if not msg:
445      msg = 'Unknown_Error'
446
447    # Look for an existing entry for this error message in the test dictionary.
448    # If found use that, otherwise create a new entry for this error message.
449    if msg in test_dict:
450      error_list = test_dict[msg]
451    else:
452      error_list = list()
453    # Create an entry for this new failure
454    new_item = [int_date, platform, builder, build_num]
455    # Add this failure to the error list if it's not already there.
456    if new_item not in error_list:
457      error_list.append([int_date, platform, builder, build_num])
458    # Sort the error list by date.
459    error_list.sort(key=lambda x: x[0])
460    # Calculate the earliest date to save; delete records for older failures.
461    earliest_date = int_date - MAX_SAVE_RECORDS
462    i = 0
463    while i < len(error_list) and error_list[i][0] <= earliest_date:
464      i += 1
465    if i > 0:
466      error_list = error_list[i:]
467    # Save the error list in the test's dictionary, keyed on error_msg.
468    test_dict[msg] = error_list
469
470    # Save the updated test dictionary in the test_suite dictionary.
471    suite_dict[test] = test_dict
472
473  # Save the updated test_suite dictionary in the failure dictionary.
474  failure_dict[suite] = suite_dict
475
476
477def ParseLogFile(log_file, test_data_dict, failure_dict, test, builder,
478                 build_num, build_link):
479  """Parse the log file from the given builder, build_num and test.
480
481     Also adds the results for this test to our test results dictionary,
482     and calls RecordFailures, to update our test failure data.
483  """
484
485  lines = []
486  with open(log_file, 'r') as infile:
487    lines = infile.readlines()
488
489  passed = {}
490  failed = {}
491  not_run = {}
492  date = ''
493  status = ''
494  board = ''
495  num_provision_errors = 0
496  build_ok = True
497  afe_line = ''
498
499  for line in lines:
500    if line.rstrip() == '<title>404 Not Found</title>':
501      print('Warning: File for %s (build number %d), %s was not found.' %
502            (builder, build_num, test))
503      build_ok = False
504      break
505    if '[ PASSED ]' in line:
506      test_name = line.split()[0]
507      if test_name != 'Suite':
508        passed[test_name] = True
509    elif '[ FAILED ]' in line:
510      test_name = line.split()[0]
511      if test_name == 'provision':
512        num_provision_errors += 1
513        not_run[test_name] = True
514      elif test_name != 'Suite':
515        failed[test_name] = True
516    elif line.startswith('started: '):
517      date = line.rstrip()
518      date = date[9:]
519      date_obj = time.strptime(date, '%a %b %d %H:%M:%S %Y')
520      int_date = (
521          date_obj.tm_year * 10000 + date_obj.tm_mon * 100 + date_obj.tm_mday)
522      date = time.strftime('%a %b %d %Y', date_obj)
523    elif not status and line.startswith('status: '):
524      status = line.rstrip()
525      words = status.split(':')
526      status = words[-1]
527    elif line.find('Suite passed with a warning') != -1:
528      status = 'WARNING'
529    elif line.startswith('@@@STEP_LINK@Link to suite@'):
530      afe_line = line.rstrip()
531      words = afe_line.split('@')
532      for w in words:
533        if w.startswith('http'):
534          afe_line = w
535          afe_line = afe_line.replace('&amp;', '&')
536    elif 'INFO: RunCommand:' in line:
537      words = line.split()
538      for i in range(0, len(words) - 1):
539        if words[i] == '--board':
540          board = words[i + 1]
541
542  test_dict = test_data_dict[test]
543  test_list = test_dict['tests']
544
545  if build_ok:
546    for t in test_list:
547      if not t in passed and not t in failed:
548        not_run[t] = True
549
550    total_pass = len(passed)
551    total_fail = len(failed)
552    total_notrun = len(not_run)
553
554  else:
555    total_pass = 0
556    total_fail = 0
557    total_notrun = 0
558    status = 'Not found.'
559  if not build_ok:
560    return [], date, board, 0, '     '
561
562  build_dict = dict()
563  build_dict['id'] = build_num
564  build_dict['builder'] = builder
565  build_dict['date'] = date
566  build_dict['build_link'] = build_link
567  build_dict['total_pass'] = total_pass
568  build_dict['total_fail'] = total_fail
569  build_dict['total_not_run'] = total_notrun
570  build_dict['afe_job_link'] = afe_line
571  build_dict['provision_errors'] = num_provision_errors
572  if status.strip() == 'SUCCESS':
573    build_dict['color'] = 'green '
574  elif status.strip() == 'FAILURE':
575    build_dict['color'] = ' red  '
576  elif status.strip() == 'WARNING':
577    build_dict['color'] = 'orange'
578  else:
579    build_dict['color'] = '      '
580
581  # Use YYYYMMDD (integer) as the build record key
582  if build_ok:
583    if board in test_dict:
584      board_dict = test_dict[board]
585    else:
586      board_dict = dict()
587    board_dict[int_date] = build_dict
588
589  # Only keep the last 5 records (based on date)
590  keys_list = board_dict.keys()
591  if len(keys_list) > MAX_SAVE_RECORDS:
592    min_key = min(keys_list)
593    del board_dict[min_key]
594
595  # Make sure changes get back into the main dictionary
596  test_dict[board] = board_dict
597  test_data_dict[test] = test_dict
598
599  if len(failed) > 0:
600    RecordFailures(failure_dict, board, test, builder, int_date, log_file,
601                   build_num, failed)
602
603  summary_result = '[%2d/ %2d/ %2d]' % (total_pass, total_fail, total_notrun)
604
605  return summary_result, date, board, int_date, build_dict['color']
606
607
608def DownloadLogFile(builder, buildnum, test, test_family):
609
610  ce = command_executer.GetCommandExecuter()
611  os.system('mkdir -p %s/%s/%s' % (DOWNLOAD_DIR, builder, test))
612  if builder in ROTATING_BUILDERS:
613    source = ('https://uberchromegw.corp.google.com/i/chromiumos.tryserver'
614              '/builders/%s/builds/%d/steps/%s%%20%%5B%s%%5D/logs/stdio' %
615              (builder, buildnum, test_family, test))
616    build_link = ('https://uberchromegw.corp.google.com/i/chromiumos.tryserver'
617                  '/builders/%s/builds/%d' % (builder, buildnum))
618  else:
619    source = ('https://uberchromegw.corp.google.com/i/chromeos/builders/%s/'
620              'builds/%d/steps/%s%%20%%5B%s%%5D/logs/stdio' %
621              (builder, buildnum, test_family, test))
622    build_link = ('https://uberchromegw.corp.google.com/i/chromeos/builders/%s'
623                  '/builds/%d' % (builder, buildnum))
624
625  target = '%s/%s/%s/%d' % (DOWNLOAD_DIR, builder, test, buildnum)
626  if not os.path.isfile(target) or os.path.getsize(target) == 0:
627    cmd = 'sso_client %s > %s' % (source, target)
628    status = ce.RunCommand(cmd)
629    if status != 0:
630      return '', ''
631
632  return target, build_link
633
634
635# Check for prodaccess.
636def CheckProdAccess():
637  status, output, _ = command_executer.GetCommandExecuter().RunCommandWOutput(
638      'prodcertstatus')
639  if status != 0:
640    return False
641  # Verify that status is not expired
642  if 'expires' in output:
643    return True
644  return False
645
646
647def ValidOptions(parser, options):
648  too_many_options = False
649  if options.main:
650    if options.rotating or options.failures_report:
651      too_many_options = True
652  elif options.rotating and options.failures_report:
653    too_many_options = True
654
655  if too_many_options:
656    parser.error('Can only specify one of --main, --rotating or'
657                 ' --failures_report.')
658
659  conflicting_failure_options = False
660  if options.failures_report and options.omit_failures:
661    conflicting_failure_options = True
662    parser.error('Cannot specify both --failures_report and --omit_failures.')
663
664  return not too_many_options and not conflicting_failure_options
665
666
667def Main(argv):
668  """Main function for this script."""
669  parser = argparse.ArgumentParser()
670  parser.add_argument(
671      '--main',
672      dest='main',
673      default=False,
674      action='store_true',
675      help='Generate report only for main waterfall '
676      'builders.')
677  parser.add_argument(
678      '--rotating',
679      dest='rotating',
680      default=False,
681      action='store_true',
682      help='Generate report only for rotating builders.')
683  parser.add_argument(
684      '--failures_report',
685      dest='failures_report',
686      default=False,
687      action='store_true',
688      help='Only generate the failures section of the report.')
689  parser.add_argument(
690      '--omit_failures',
691      dest='omit_failures',
692      default=False,
693      action='store_true',
694      help='Do not generate the failures section of the report.')
695  parser.add_argument(
696      '--no_update',
697      dest='no_update',
698      default=False,
699      action='store_true',
700      help='Run reports, but do not update the data files.')
701
702  options = parser.parse_args(argv)
703
704  if not ValidOptions(parser, options):
705    return 1
706
707  main_only = options.main
708  rotating_only = options.rotating
709  failures_report = options.failures_report
710  omit_failures = options.omit_failures
711
712  test_data_dict = dict()
713  failure_dict = dict()
714
715  prod_access = CheckProdAccess()
716  if not prod_access:
717    print('ERROR: Please run prodaccess first.')
718    return
719
720  with open('%s/waterfall-test-data.json' % DATA_DIR, 'r') as input_file:
721    test_data_dict = json.load(input_file)
722
723  with open('%s/test-failure-data.json' % DATA_DIR, 'r') as fp:
724    failure_dict = json.load(fp)
725
726  builds = GetBuilds()
727
728  waterfall_report_dict = dict()
729  rotating_report_dict = dict()
730  int_date = 0
731  for test_desc in TESTS:
732    test, test_family = test_desc
733    for build in builds:
734      (builder, buildnum) = build
735      if test.startswith('kernel') and 'llvm' in builder:
736        continue
737      if 'x86' in builder and not test.startswith('bvt'):
738        continue
739      target, build_link = DownloadLogFile(builder, buildnum, test, test_family)
740
741      if os.path.exists(target):
742        test_summary, report_date, board, tmp_date, color = ParseLogFile(
743            target, test_data_dict, failure_dict, test, builder, buildnum,
744            build_link)
745
746        if tmp_date != 0:
747          int_date = tmp_date
748
749        if builder in ROTATING_BUILDERS:
750          UpdateReport(rotating_report_dict, builder, test, report_date,
751                       build_link, test_summary, board, color)
752        else:
753          UpdateReport(waterfall_report_dict, builder, test, report_date,
754                       build_link, test_summary, board, color)
755
756  PruneOldFailures(failure_dict, int_date)
757
758  if waterfall_report_dict and not rotating_only and not failures_report:
759    main_report = GenerateWaterfallReport(waterfall_report_dict, failure_dict,
760                                          'main', int_date, omit_failures)
761    EmailReport(main_report, 'Main', format_date(int_date))
762    shutil.copy(main_report, ARCHIVE_DIR)
763  if rotating_report_dict and not main_only and not failures_report:
764    rotating_report = GenerateWaterfallReport(rotating_report_dict,
765                                              failure_dict, 'rotating',
766                                              int_date, omit_failures)
767    EmailReport(rotating_report, 'Rotating', format_date(int_date))
768    shutil.copy(rotating_report, ARCHIVE_DIR)
769
770  if failures_report:
771    failures_report = GenerateFailuresReport(failure_dict, int_date)
772    EmailReport(failures_report, 'Failures', format_date(int_date))
773    shutil.copy(failures_report, ARCHIVE_DIR)
774
775  if not options.no_update:
776    with open('%s/waterfall-test-data.json' % DATA_DIR, 'w') as out_file:
777      json.dump(test_data_dict, out_file, indent=2)
778
779    with open('%s/test-failure-data.json' % DATA_DIR, 'w') as out_file:
780      json.dump(failure_dict, out_file, indent=2)
781
782    UpdateBuilds(builds)
783
784
785if __name__ == '__main__':
786  Main(sys.argv[1:])
787  sys.exit(0)
788