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