Home | History | Annotate | Download | only in chaos_lib
      1 # Copyright (c) 2014 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import argparse
      6 import copy
      7 import csv
      8 import logging
      9 import os
     10 import re
     11 import shutil
     12 
     13 CONNECT_FAIL = object()
     14 CONFIG_FAIL = object()
     15 RESULTS_DIR = '/tmp/chaos'
     16 
     17 
     18 class ChaosParser(object):
     19     """Defines a parser for chaos test results"""
     20 
     21     def __init__(self, results_dir, create_file, print_config_failures):
     22         """ Constructs a parser interface.
     23 
     24         @param results_dir: complete path to restuls directory for a chaos test.
     25         @param create_file: True to create csv files; False otherwise.
     26         @param print_config_failures: True to print the config info to stdout;
     27                                       False otherwise.
     28 
     29         """
     30         self._test_results_dir = results_dir
     31         self._create_file = create_file
     32         self._print_config_failures = print_config_failures
     33 
     34 
     35     def convert_set_to_string(self, set_list):
     36         """Converts a set to a single string.
     37 
     38         @param set_list: a set to convert
     39 
     40         @returns a string, which is all items separated by the word 'and'
     41 
     42         """
     43         return_string = str()
     44         for i in set_list:
     45             return_string += str('%s and ' % i)
     46         return return_string[:-5]
     47 
     48 
     49     def create_csv(self, filename, data_list):
     50         """Creates a file in .csv format.
     51 
     52         @param filename: name for the csv file
     53         @param data_list: a list of all the info to write to a file
     54 
     55         """
     56         if not os.path.exists(RESULTS_DIR):
     57             os.mkdir(RESULTS_DIR)
     58         try:
     59             path = os.path.join(RESULTS_DIR, filename + '.csv')
     60             with open(path, 'wb') as f:
     61                 writer = csv.writer(f)
     62                 writer.writerow(data_list)
     63                 logging.info('Created CSV file %s', path)
     64         except IOError as e:
     65             logging.error('File operation failed with %s: %s', e.errno,
     66                            e.strerror)
     67             return
     68 
     69 
     70     def get_ap_name(self, line):
     71         """Gets the router name from the string passed.
     72 
     73         @param line: Test ERROR string from chaos status.log
     74 
     75         @returns the router name or brand.
     76 
     77         """
     78         router_info = re.search('Router name: ([\w\s]+)', line)
     79         return router_info.group(1)
     80 
     81 
     82     def get_ap_mode_chan_freq(self, ssid):
     83         """Gets the AP band from ssid using channel.
     84 
     85         @param ssid: A valid chaos test SSID as a string
     86 
     87         @returns the AP band, mode, and channel.
     88 
     89         """
     90         channel_security_info = ssid.split('_')
     91         channel_info = channel_security_info[-2]
     92         mode = channel_security_info[-3]
     93         channel = int(re.split('(\d+)', channel_info)[1])
     94         # TODO Choose if we want to keep band, we never put it in the
     95         # spreadsheet and is currently unused.
     96         if channel in range(1, 15):
     97             band = '2.4GHz'
     98         else:
     99             band = '5GHz'
    100         return {'mode': mode.upper(), 'channel': channel,
    101                 'band': band}
    102 
    103 
    104     def generate_percentage_string(self, passed_tests, total_tests):
    105         """Creates a pass percentage string in the formation x/y (zz%)
    106 
    107         @param passed_tests: int of passed tests
    108         @param total_tests: int of total tests
    109 
    110         @returns a formatted string as described above.
    111 
    112         """
    113         percent = float(passed_tests)/float(total_tests) * 100
    114         percent_string = str(int(round(percent))) + '%'
    115         return str('%d/%d (%s)' % (passed_tests, total_tests, percent_string))
    116 
    117 
    118     def parse_keyval(self, filepath):
    119         """Parses the 'keyvalue' file to get device details.
    120 
    121         @param filepath: the complete path to the keyval file
    122 
    123         @returns a board with device name and OS version.
    124 
    125         """
    126         # Android information does not exist in the keyfile, add temporary
    127         # information into the dictionary.  crbug.com/570408
    128         lsb_dict = {'board': 'unknown',
    129                     'version': 'unknown'}
    130         f = open(filepath, 'r')
    131         for line in f:
    132             line = line.split('=')
    133             if 'RELEASE_BOARD' in line[0]:
    134                 lsb_dict = {'board':line[1].rstrip()}
    135             elif 'RELEASE_VERSION' in line[0]:
    136                 lsb_dict['version'] = line[1].rstrip()
    137             else:
    138                 continue
    139         f.close()
    140         return lsb_dict
    141 
    142 
    143     def parse_status_log(self, board, os_version, security, status_log_path):
    144         """Parses the entire status.log file from chaos test for test failures.
    145            and creates two CSV files for connect fail and configuration fail
    146            respectively.
    147 
    148         @param board: the board the test was run against as a string
    149         @param os_version: the version of ChromeOS as a string
    150         @param security: the security used during the test as a string
    151         @param status_log_path: complete path to the status.log file
    152 
    153         """
    154         # Items that can have multiple values
    155         modes = list()
    156         channels = list()
    157         test_fail_aps = list()
    158         static_config_failures = list()
    159         dynamic_config_failures = list()
    160         kernel_version = ""
    161         fw_version = ""
    162         f = open(status_log_path, 'r')
    163         total = 0
    164         for line in f:
    165             line = line.strip()
    166             if line.startswith('START\tnetwork_WiFi'):
    167                # Do not count PDU failures in total tests run.
    168                if 'PDU' in line:
    169                    continue
    170                total += 1
    171             elif 'kernel_version' in line:
    172                 kernel_version = re.search('[\d.]+', line).group(0)
    173             elif 'firmware_version' in line:
    174                fw_version = re.search('firmware_version\': \'([\w\s:().]+)',
    175                                       line).group(1)
    176             elif line.startswith('ERROR') or line.startswith('FAIL'):
    177                 title_info = line.split()
    178                 if 'reboot' in title_info:
    179                     continue
    180                 # Get the hostname for the AP that failed configuration.
    181                 if 'PDU' in title_info[1]:
    182                     continue
    183                 else:
    184                     # Get the router name, band for the AP that failed
    185                     # connect.
    186                     if 'Config' in title_info[1]:
    187                         failure_type = CONFIG_FAIL
    188                     else:
    189                         failure_type = CONNECT_FAIL
    190 
    191                     if (failure_type == CONFIG_FAIL and
    192                         'chromeos' in title_info[1]):
    193                         ssid = title_info[1].split('.')[1].split('_')[0]
    194                     else:
    195                         ssid_info = title_info[1].split('.')
    196                         ssid = ssid_info[1]
    197                         network_dict = self.get_ap_mode_chan_freq(ssid)
    198                         modes.append(network_dict['mode'])
    199                         channels.append(network_dict['channel'])
    200 
    201                     # Security mismatches and Ping failures are not connect
    202                     # failures.
    203                     if (('Ping command' in line or 'correct security' in line)
    204                         or failure_type == CONFIG_FAIL):
    205                         if 'StaticAPConfigurator' in line:
    206                             static_config_failures.append(ssid)
    207                         else:
    208                             dynamic_config_failures.append(ssid)
    209                     else:
    210                         test_fail_aps.append(ssid)
    211             elif ('END GOOD' in line and ('ChaosConnectDisconnect' in line or
    212                                           'ChaosLongConnect' in line)):
    213                     test_name = line.split()[2]
    214                     ssid = test_name.split('.')[1]
    215                     network_dict = self.get_ap_mode_chan_freq(ssid)
    216                     modes.append(network_dict['mode'])
    217                     channels.append(network_dict['channel'])
    218             else:
    219                 continue
    220 
    221         config_pass = total - (len(dynamic_config_failures) +
    222                                len(static_config_failures))
    223         config_pass_string = self.generate_percentage_string(config_pass,
    224                                                              total)
    225         connect_pass = config_pass - len(test_fail_aps)
    226         connect_pass_string = self.generate_percentage_string(connect_pass,
    227                                                               config_pass)
    228 
    229         base_csv_list = [board, os_version, fw_version, kernel_version,
    230                          self.convert_set_to_string(set(modes)),
    231                          self.convert_set_to_string(set(channels)),
    232                          security]
    233 
    234         static_config_csv_list = copy.deepcopy(base_csv_list)
    235         static_config_csv_list.append(config_pass_string)
    236         static_config_csv_list.extend(static_config_failures)
    237 
    238         dynamic_config_csv_list = copy.deepcopy(base_csv_list)
    239         dynamic_config_csv_list.append(config_pass_string)
    240         dynamic_config_csv_list.extend(dynamic_config_failures)
    241 
    242         connect_csv_list = copy.deepcopy(base_csv_list)
    243         connect_csv_list.append(connect_pass_string)
    244         connect_csv_list.extend(test_fail_aps)
    245 
    246         print('Connect failure for security: %s' % security)
    247         print ','.join(connect_csv_list)
    248         print('\n')
    249 
    250         if self._print_config_failures:
    251             config_files = [('Static', static_config_csv_list),
    252                             ('Dynamic', dynamic_config_csv_list)]
    253             for config_data in config_files:
    254                 self.print_config_failures(config_data[0], security,
    255                                            config_data[1])
    256 
    257         if self._create_file:
    258             self.create_csv('chaos_WiFi_dynamic_config_fail.' + security,
    259                             dynamic_config_csv_list)
    260             self.create_csv('chaos_WiFi_static_config_fail.' + security,
    261                             static_config_csv_list)
    262             self.create_csv('chaos_WiFi_connect_fail.' + security,
    263                             connect_csv_list)
    264 
    265 
    266     def print_config_failures(self, config_type, security, config_csv_list):
    267         """Prints out the configuration failures.
    268 
    269         @param config_type: string describing the configurator type
    270         @param security: the security type as a string
    271         @param config_csv_list: list of the configuration failures
    272 
    273         """
    274         # 8 because that is the lenth of the base list
    275         if len(config_csv_list) <= 8:
    276             return
    277         print('%s config failures for security: %s' % (config_type, security))
    278         print ','.join(config_csv_list)
    279         print('\n')
    280 
    281 
    282     def traverse_results_dir(self, path):
    283         """Walks through the results directory and get the pathnames for the
    284            status.log and the keyval files.
    285 
    286         @param path: complete path to a specific test result directory.
    287 
    288         @returns a dict with absolute pathnames for the 'status.log' and
    289                 'keyfile' files.
    290 
    291         """
    292         status = None
    293         keyval = None
    294 
    295         for root, dir_name, file_name in os.walk(path):
    296             for name in file_name:
    297                 current_path = os.path.join(root, name)
    298                 if name == 'status.log' and not status:
    299                        status = current_path
    300                 elif name == 'keyval' and ('param-debug_info' in
    301                                            open(current_path).read()):
    302                     # This is a keyval file for a single test and not a suite.
    303                     keyval = os.path.join(root, name)
    304                     break
    305                 else:
    306                     continue
    307         if not keyval:
    308             raise Exception('Did Chaos tests complete successfully? Rerun tests'
    309                             ' with missing results.')
    310         return {'status_file': status, 'keyval_file': keyval}
    311 
    312 
    313     def parse_results_dir(self):
    314         """Parses each result directory.
    315 
    316         For each results directory created by test_that, parse it and
    317         create summary files.
    318 
    319         """
    320         if os.path.exists(RESULTS_DIR):
    321             shutil.rmtree(RESULTS_DIR)
    322         test_processed = False
    323         for results_dir in os.listdir(self._test_results_dir):
    324             if 'results' in results_dir:
    325                 path = os.path.join(self._test_results_dir, results_dir)
    326                 test = results_dir.split('.')[1]
    327                 status_key_dict = self.traverse_results_dir(path)
    328                 status_log_path = status_key_dict['status_file']
    329                 lsb_info = self.parse_keyval(status_key_dict['keyval_file'])
    330                 if test is not None:
    331                     self.parse_status_log(lsb_info['board'],
    332                                           lsb_info['version'],
    333                                           test,
    334                                           status_log_path)
    335                     test_processed = True
    336         if not test_processed:
    337             raise RuntimeError('chaos_parse: Did not find any results directory'
    338                                'to process')
    339 
    340 
    341 def main():
    342     """Main function to call the parser."""
    343     logging.basicConfig(level=logging.INFO)
    344     arg_parser = argparse.ArgumentParser()
    345     arg_parser.add_argument('-d', '--directory', dest='dir_name',
    346                             help='Pathname to results generated by test_that',
    347                             required=True)
    348     arg_parser.add_argument('--create_file', dest='create_file',
    349                             action='store_true', default=False)
    350     arg_parser.add_argument('--print_config_failures',
    351                             dest='print_config_failures',
    352                             action='store_true',
    353                             default=False)
    354     arguments = arg_parser.parse_args()
    355     if not arguments.dir_name:
    356         raise RuntimeError('chaos_parser: No directory name supplied. Use -h'
    357                            ' for help')
    358     if not os.path.exists(arguments.dir_name):
    359         raise RuntimeError('chaos_parser: Invalid directory name supplied.')
    360     parser = ChaosParser(arguments.dir_name, arguments.create_file,
    361                          arguments.print_config_failures)
    362     parser.parse_results_dir()
    363 
    364 
    365 if __name__ == '__main__':
    366     main()
    367