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