blob: a1de2834ad1ffd1653d9791e9cebd745de4782f8 [file] [log] [blame]
# Copyright (c) 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import copy
import csv
import logging
import os
import re
import shutil
CONNECT_FAIL = object()
CONFIG_FAIL = object()
RESULTS_DIR = '/tmp/chaos'
class ChaosParser(object):
"""Defines a parser for chaos test results"""
def __init__(self, results_dir, create_file, print_config_failures):
""" Constructs a parser interface.
@param results_dir: complete path to restuls directory for a chaos test.
@param create_file: True to create csv files; False otherwise.
@param print_config_failures: True to print the config info to stdout;
False otherwise.
"""
self._test_results_dir = results_dir
self._create_file = create_file
self._print_config_failures = print_config_failures
def convert_set_to_string(self, set_list):
"""Converts a set to a single string.
@param set_list: a set to convert
@returns a string, which is all items separated by the word 'and'
"""
return_string = str()
for i in set_list:
return_string += str('%s and ' % i)
return return_string[:-5]
def create_csv(self, filename, data_list):
"""Creates a file in .csv format.
@param filename: name for the csv file
@param data_list: a list of all the info to write to a file
"""
if not os.path.exists(RESULTS_DIR):
os.mkdir(RESULTS_DIR)
try:
path = os.path.join(RESULTS_DIR, filename + '.csv')
with open(path, 'wb') as f:
writer = csv.writer(f)
writer.writerow(data_list)
logging.info('Created CSV file %s', path)
except IOError as e:
logging.error('File operation failed with %s: %s', e.errno,
e.strerror)
return
def get_ap_name(self, line):
"""Gets the router name from the string passed.
@param line: Test ERROR string from chaos status.log
@returns the router name or brand.
"""
router_info = re.search('Router name: ([\w\s]+)', line)
return router_info.group(1)
def get_ap_mode_chan_freq(self, ssid):
"""Gets the AP band from ssid using channel.
@param ssid: A valid chaos test SSID as a string
@returns the AP band, mode, and channel.
"""
channel_security_info = ssid.split('_')
channel_info = channel_security_info[-2]
mode = channel_security_info[-3]
channel = int(re.split('(\d+)', channel_info)[1])
# TODO Choose if we want to keep band, we never put it in the
# spreadsheet and is currently unused.
if channel in range(1, 15):
band = '2.4GHz'
else:
band = '5GHz'
return {'mode': mode.upper(), 'channel': channel,
'band': band}
def generate_percentage_string(self, passed_tests, total_tests):
"""Creates a pass percentage string in the formation x/y (zz%)
@param passed_tests: int of passed tests
@param total_tests: int of total tests
@returns a formatted string as described above.
"""
percent = float(passed_tests)/float(total_tests) * 100
percent_string = str(int(round(percent))) + '%'
return str('%d/%d (%s)' % (passed_tests, total_tests, percent_string))
def parse_keyval(self, filepath):
"""Parses the 'keyvalue' file to get device details.
@param filepath: the complete path to the keyval file
@returns a board with device name and OS version.
"""
f = open(filepath, 'r')
for line in f:
line = line.split('=')
if 'RELEASE_BOARD' in line[0]:
lsb_dict = {'board':line[1].rstrip()}
elif 'RELEASE_VERSION' in line[0]:
lsb_dict['version'] = line[1].rstrip()
else:
continue
f.close()
return lsb_dict
def parse_status_log(self, board, os_version, security, status_log_path):
"""Parses the entire status.log file from chaos test for test failures.
and creates two CSV files for connect fail and configuration fail
respectively.
@param board: the board the test was run against as a string
@param os_version: the version of ChromeOS as a string
@param security: the security used during the test as a string
@param status_log_path: complete path to the status.log file
"""
# Items that can have multiple values
modes = list()
channels = list()
test_fail_aps = list()
static_config_failures = list()
dynamic_config_failures = list()
kernel_version = ""
fw_version = ""
f = open(status_log_path, 'r')
total = 0
for line in f:
line = line.strip()
if line.startswith('START\tnetwork_WiFi'):
# Do not count PDU failures in total tests run.
if 'PDU' in line:
continue
total += 1
elif 'kernel_version' in line:
kernel_version = re.search('[\d.]+', line).group(0)
elif 'firmware_version' in line:
fw_version = re.search('firmware_version\': \'([\w\s:().]+)',
line).group(1)
elif line.startswith('ERROR') or line.startswith('FAIL'):
title_info = line.split()
if 'reboot' in title_info:
continue
# Get the hostname for the AP that failed configuration.
if 'PDU' in title_info[1]:
continue
else:
# Get the router name, band for the AP that failed
# connect.
if 'Config' in title_info[1]:
failure_type = CONFIG_FAIL
else:
failure_type = CONNECT_FAIL
if (failure_type == CONFIG_FAIL and
'chromeos' in title_info[1]):
ssid = title_info[1].split('.')[1].split('_')[0]
else:
ssid_info = title_info[1].split('.')
ssid = ssid_info[1]
network_dict = self.get_ap_mode_chan_freq(ssid)
modes.append(network_dict['mode'])
channels.append(network_dict['channel'])
# Security mismatches and Ping failures are not connect
# failures.
if (('Ping command' in line or 'correct security' in line)
or failure_type == CONFIG_FAIL):
if 'StaticAPConfigurator' in line:
static_config_failures.append(ssid)
else:
dynamic_config_failures.append(ssid)
else:
test_fail_aps.append(ssid)
elif ('END GOOD' in line and ('ChaosConnectDisconnect' in line or
'ChaosLongConnect' in line)):
test_name = line.split()[2]
ssid = test_name.split('.')[1]
network_dict = self.get_ap_mode_chan_freq(ssid)
modes.append(network_dict['mode'])
channels.append(network_dict['channel'])
else:
continue
config_pass = total - (len(dynamic_config_failures) +
len(static_config_failures))
config_pass_string = self.generate_percentage_string(config_pass,
total)
connect_pass = config_pass - len(test_fail_aps)
connect_pass_string = self.generate_percentage_string(connect_pass,
config_pass)
base_csv_list = [board, os_version, fw_version, kernel_version,
self.convert_set_to_string(set(modes)),
self.convert_set_to_string(set(channels)),
security]
static_config_csv_list = copy.deepcopy(base_csv_list)
static_config_csv_list.append(config_pass_string)
static_config_csv_list.extend(static_config_failures)
dynamic_config_csv_list = copy.deepcopy(base_csv_list)
dynamic_config_csv_list.append(config_pass_string)
dynamic_config_csv_list.extend(dynamic_config_failures)
connect_csv_list = copy.deepcopy(base_csv_list)
connect_csv_list.append(connect_pass_string)
connect_csv_list.extend(test_fail_aps)
print('Connect failure for security: %s' % security)
print ','.join(connect_csv_list)
print('\n')
if self._print_config_failures:
config_files = [('Static', static_config_csv_list),
('Dynamic', dynamic_config_csv_list)]
for config_data in config_files:
self.print_config_failures(config_data[0], security,
config_data[1])
if self._create_file:
self.create_csv('chaos_WiFi_dynamic_config_fail.' + security,
dynamic_config_csv_list)
self.create_csv('chaos_WiFi_static_config_fail.' + security,
static_config_csv_list)
self.create_csv('chaos_WiFi_connect_fail.' + security,
connect_csv_list)
def print_config_failures(self, config_type, security, config_csv_list):
"""Prints out the configuration failures.
@param config_type: string describing the configurator type
@param security: the security type as a string
@param config_csv_list: list of the configuration failures
"""
# 8 because that is the lenth of the base list
if len(config_csv_list) <= 8:
return
print('%s config failures for security: %s' % (config_type, security))
print ','.join(config_csv_list)
print('\n')
def traverse_results_dir(self, path):
"""Walks through the results directory and get the pathnames for the
status.log and the keyval files.
@param path: complete path to a specific test result directory.
@returns a dict with absolute pathnames for the 'status.log' and
'keyfile' files.
"""
status = None
keyval = None
for root, dir_name, file_name in os.walk(path):
for name in file_name:
if name == 'status.log':
path = os.path.join(root, name)
if not status:
status = path
elif name == 'keyval' and 'CHROMEOS_BUILD' in open(os.path.join(
root, name)).read():
path = os.path.join(root, name)
keyval = path
break
else:
continue
return {'status_file': status, 'keyval_file': keyval}
def parse_results_dir(self):
"""Parses each result directory.
For each results directory created by test_that, parse it and
create summary files.
"""
if os.path.exists(RESULTS_DIR):
shutil.rmtree(RESULTS_DIR)
test_processed = False
for results_dir in os.listdir(self._test_results_dir):
if 'results' in results_dir:
path = os.path.join(self._test_results_dir, results_dir)
test = results_dir.split('.')[1]
status_key_dict = self.traverse_results_dir(path)
status_log_path = status_key_dict['status_file']
lsb_info = self.parse_keyval(status_key_dict['keyval_file'])
if test is not None:
self.parse_status_log(lsb_info['board'],
lsb_info['version'],
test,
status_log_path)
test_processed = True
if not test_processed:
raise RuntimeError('chaos_parse: Did not find any results directory'
'to process')
def main():
"""Main function to call the parser."""
logging.basicConfig(level=logging.INFO)
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument('-d', '--directory', dest='dir_name',
help='Pathname to results generated by test_that',
required=True)
arg_parser.add_argument('--create_file', dest='create_file',
action='store_true', default=False)
arg_parser.add_argument('--print_config_failures',
dest='print_config_failures',
action='store_true',
default=False)
arguments = arg_parser.parse_args()
if not arguments.dir_name:
raise RuntimeError('chaos_parser: No directory name supplied. Use -h'
' for help')
if not os.path.exists(arguments.dir_name):
raise RuntimeError('chaos_parser: Invalid directory name supplied.')
parser = ChaosParser(arguments.dir_name, arguments.create_file,
arguments.print_config_failures)
parser.parse_results_dir()
if __name__ == '__main__':
main()