diff options
Diffstat (limited to 'util')
-rwxr-xr-x | util/build_with_clang.py | 42 | ||||
-rw-r--r-- | util/chargen | 25 | ||||
-rwxr-xr-x | util/config_option_check.py | 693 | ||||
-rwxr-xr-x | util/ec3po/console.py | 2231 | ||||
-rwxr-xr-x | util/ec3po/console_unittest.py | 2954 | ||||
-rw-r--r-- | util/ec3po/interpreter.py | 818 | ||||
-rwxr-xr-x | util/ec3po/interpreter_unittest.py | 728 | ||||
-rw-r--r-- | util/ec3po/threadproc_shim.py | 31 | ||||
-rwxr-xr-x | util/ec_openocd.py | 24 | ||||
-rwxr-xr-x | util/flash_jlink.py | 144 | ||||
-rwxr-xr-x | util/fptool.py | 17 | ||||
-rwxr-xr-x | util/inject-keys.py | 149 | ||||
-rwxr-xr-x | util/kconfig_check.py | 230 | ||||
-rw-r--r-- | util/kconfiglib.py | 1581 | ||||
-rw-r--r-- | util/run_ects.py | 144 | ||||
-rw-r--r-- | util/test_kconfig_check.py | 181 | ||||
-rwxr-xr-x | util/uart_stress_tester.py | 1015 | ||||
-rwxr-xr-x | util/unpack_ftb.py | 124 | ||||
-rwxr-xr-x | util/update_release_branch.py | 251 |
19 files changed, 6052 insertions, 5330 deletions
diff --git a/util/build_with_clang.py b/util/build_with_clang.py index a38ade2cb8..98da942152 100755 --- a/util/build_with_clang.py +++ b/util/build_with_clang.py @@ -12,15 +12,14 @@ import multiprocessing import os import subprocess import sys - from concurrent.futures import ThreadPoolExecutor # Add to this list as compilation errors are fixed for boards. BOARDS_THAT_COMPILE_SUCCESSFULLY_WITH_CLANG = [ - 'dartmonkey', - 'bloonchipper', - 'nucleo-f412zg', - 'nucleo-h743zi', + "dartmonkey", + "bloonchipper", + "nucleo-f412zg", + "nucleo-h743zi", ] @@ -29,35 +28,29 @@ def build(board_name: str) -> None: logging.debug('Building board: "%s"', board_name) cmd = [ - 'make', - 'BOARD=' + board_name, - '-j', + "make", + "BOARD=" + board_name, + "-j", ] - logging.debug('Running command: "%s"', ' '.join(cmd)) - subprocess.run(cmd, env=dict(os.environ, CC='clang'), check=True) + logging.debug('Running command: "%s"', " ".join(cmd)) + subprocess.run(cmd, env=dict(os.environ, CC="clang"), check=True) def main() -> int: parser = argparse.ArgumentParser() - log_level_choices = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] - parser.add_argument( - '--log_level', '-l', - choices=log_level_choices, - default='DEBUG' - ) + log_level_choices = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + parser.add_argument("--log_level", "-l", choices=log_level_choices, default="DEBUG") parser.add_argument( - '--num_threads', '-j', - type=int, - default=multiprocessing.cpu_count() + "--num_threads", "-j", type=int, default=multiprocessing.cpu_count() ) args = parser.parse_args() logging.basicConfig(level=args.log_level) - logging.debug('Building with %d threads', args.num_threads) + logging.debug("Building with %d threads", args.num_threads) failed_boards = [] with ThreadPoolExecutor(max_workers=args.num_threads) as executor: @@ -73,13 +66,14 @@ def main() -> int: failed_boards.append(board) if len(failed_boards) > 0: - logging.error('The following boards failed to compile:\n%s', - '\n'.join(failed_boards)) + logging.error( + "The following boards failed to compile:\n%s", "\n".join(failed_boards) + ) return 1 - logging.info('All boards compiled successfully!') + logging.info("All boards compiled successfully!") return 0 -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(main()) diff --git a/util/chargen b/util/chargen index 9ba14d3d6a..a1f7947a14 100644 --- a/util/chargen +++ b/util/chargen @@ -5,6 +5,7 @@ import sys + def chargen(modulo, max_chars): """Generate a stream of characters on the console. @@ -18,7 +19,7 @@ def chargen(modulo, max_chars): zero, if zero - print indefinitely """ - base = '0' + base = "0" c = base counter = 0 while True: @@ -26,25 +27,25 @@ def chargen(modulo, max_chars): counter = counter + 1 if (max_chars != 0) and (counter == max_chars): - sys.stdout.write('\n') + sys.stdout.write("\n") return if modulo and ((counter % modulo) == 0): c = base continue - if c == 'z': + if c == "z": c = base - elif c == 'Z': - c = 'a' - elif c == '9': - c = 'A' + elif c == "Z": + c = "a" + elif c == "9": + c = "A" else: - c = '%c' % (ord(c) + 1) + c = "%c" % (ord(c) + 1) def main(args): - '''Process command line arguments and invoke chargen if args are valid''' + """Process command line arguments and invoke chargen if args are valid""" modulo = 0 max_chars = 0 @@ -55,8 +56,7 @@ def main(args): if len(args) > 1: max_chars = int(args[1]) except ValueError: - sys.stderr.write('usage %s:' - "['seq_length' ['max_chars']]\n") + sys.stderr.write("usage %s:" "['seq_length' ['max_chars']]\n") sys.exit(1) try: @@ -64,6 +64,7 @@ def main(args): except KeyboardInterrupt: print() -if __name__ == '__main__': + +if __name__ == "__main__": main(sys.argv[1:]) sys.exit(0) diff --git a/util/config_option_check.py b/util/config_option_check.py index 8bd8ecb1f0..0b05b64091 100755 --- a/util/config_option_check.py +++ b/util/config_option_check.py @@ -13,6 +13,7 @@ Script to ensure that all configuration options for the Chrome EC are defined in config.h. """ from __future__ import print_function + import enum import os import re @@ -21,368 +22,392 @@ import sys class Line(object): - """Class for each changed line in diff output. + """Class for each changed line in diff output. - Attributes: - line_num: The integer line number that this line appears in the file. - string: The literal string of this line. - line_type: '+' or '-' indicating if this line was an addition or - deletion. - """ + Attributes: + line_num: The integer line number that this line appears in the file. + string: The literal string of this line. + line_type: '+' or '-' indicating if this line was an addition or + deletion. + """ - def __init__(self, line_num, string, line_type): - """Inits Line with the line number and the actual string.""" - self.line_num = line_num - self.string = string - self.line_type = line_type + def __init__(self, line_num, string, line_type): + """Inits Line with the line number and the actual string.""" + self.line_num = line_num + self.string = string + self.line_type = line_type class Hunk(object): - """Class for a git diff hunk. + """Class for a git diff hunk. - Attributes: - filename: The name of the file that this hunk belongs to. - lines: A list of Line objects that are a part of this hunk. - """ + Attributes: + filename: The name of the file that this hunk belongs to. + lines: A list of Line objects that are a part of this hunk. + """ - def __init__(self, filename, lines): - """Inits Hunk with the filename and the list of lines of the hunk.""" - self.filename = filename - self.lines = lines + def __init__(self, filename, lines): + """Inits Hunk with the filename and the list of lines of the hunk.""" + self.filename = filename + self.lines = lines # Master file which is supposed to include all CONFIG_xxxx descriptions. -CONFIG_FILE = 'include/config.h' +CONFIG_FILE = "include/config.h" # Specific files which the checker should ignore. -ALLOWLIST = [CONFIG_FILE, 'util/config_option_check.py'] +ALLOWLIST = [CONFIG_FILE, "util/config_option_check.py"] # Specific directories which the checker should ignore. -ALLOW_PATTERN = re.compile('zephyr/.*') +ALLOW_PATTERN = re.compile("zephyr/.*") # Specific CONFIG_* flags which the checker should ignore. -ALLOWLIST_CONFIGS = ['CONFIG_ZTEST'] +ALLOWLIST_CONFIGS = ["CONFIG_ZTEST"] + def obtain_current_config_options(): - """Obtains current config options from include/config.h. - - Scans through the main config file defined in CONFIG_FILE for all CONFIG_* - options. - - Returns: - config_options: A list of all the config options in the main CONFIG_FILE. - """ - - config_options = [] - config_option_re = re.compile(r'^#(define|undef)\s+(CONFIG_[A-Z0-9_]+)') - with open(CONFIG_FILE, 'r') as config_file: - for line in config_file: - result = config_option_re.search(line) - if not result: - continue - word = result.groups()[1] - if word not in config_options: - config_options.append(word) - return config_options + """Obtains current config options from include/config.h. + + Scans through the main config file defined in CONFIG_FILE for all CONFIG_* + options. + + Returns: + config_options: A list of all the config options in the main CONFIG_FILE. + """ + + config_options = [] + config_option_re = re.compile(r"^#(define|undef)\s+(CONFIG_[A-Z0-9_]+)") + with open(CONFIG_FILE, "r") as config_file: + for line in config_file: + result = config_option_re.search(line) + if not result: + continue + word = result.groups()[1] + if word not in config_options: + config_options.append(word) + return config_options + def obtain_config_options_in_use(): - """Obtains all the config options in use in the repo. - - Scans through the entire repo looking for all CONFIG_* options actively used. - - Returns: - options_in_use: A set of all the config options in use in the repo. - """ - file_list = [] - cwd = os.getcwd() - config_option_re = re.compile(r'\b(CONFIG_[a-zA-Z0-9_]+)') - config_debug_option_re = re.compile(r'\b(CONFIG_DEBUG_[a-zA-Z0-9_]+)') - options_in_use = set() - for (dirpath, dirnames, filenames) in os.walk(cwd, topdown=True): - # Ignore the build and private directories (taken from .gitignore) - if 'build' in dirnames: - dirnames.remove('build') - if 'private' in dirnames: - dirnames.remove('private') - for f in filenames: - # Ignore hidden files. - if f.startswith('.'): - continue - # Only consider C source, assembler, and Make-style files. - if (os.path.splitext(f)[1] in ('.c', '.h', '.inc', '.S', '.mk') or - 'Makefile' in f): - file_list.append(os.path.join(dirpath, f)) - - # Search through each file and build a set of the CONFIG_* options being - # used. - - for f in file_list: - if CONFIG_FILE in f: - continue - with open(f, 'r') as cur_file: - for line in cur_file: - match = config_option_re.findall(line) - if match: - for option in match: - if not in_comment(f, line, option): - if option not in options_in_use: - options_in_use.add(option) - - # Since debug options can be turned on at any time, assume that they are - # always in use in case any aren't being used. - - with open(CONFIG_FILE, 'r') as config_file: - for line in config_file: - match = config_debug_option_re.findall(line) - if match: - for option in match: - if not in_comment(CONFIG_FILE, line, option): - if option not in options_in_use: - options_in_use.add(option) - - return options_in_use + """Obtains all the config options in use in the repo. + + Scans through the entire repo looking for all CONFIG_* options actively used. + + Returns: + options_in_use: A set of all the config options in use in the repo. + """ + file_list = [] + cwd = os.getcwd() + config_option_re = re.compile(r"\b(CONFIG_[a-zA-Z0-9_]+)") + config_debug_option_re = re.compile(r"\b(CONFIG_DEBUG_[a-zA-Z0-9_]+)") + options_in_use = set() + for (dirpath, dirnames, filenames) in os.walk(cwd, topdown=True): + # Ignore the build and private directories (taken from .gitignore) + if "build" in dirnames: + dirnames.remove("build") + if "private" in dirnames: + dirnames.remove("private") + for f in filenames: + # Ignore hidden files. + if f.startswith("."): + continue + # Only consider C source, assembler, and Make-style files. + if ( + os.path.splitext(f)[1] in (".c", ".h", ".inc", ".S", ".mk") + or "Makefile" in f + ): + file_list.append(os.path.join(dirpath, f)) + + # Search through each file and build a set of the CONFIG_* options being + # used. + + for f in file_list: + if CONFIG_FILE in f: + continue + with open(f, "r") as cur_file: + for line in cur_file: + match = config_option_re.findall(line) + if match: + for option in match: + if not in_comment(f, line, option): + if option not in options_in_use: + options_in_use.add(option) + + # Since debug options can be turned on at any time, assume that they are + # always in use in case any aren't being used. + + with open(CONFIG_FILE, "r") as config_file: + for line in config_file: + match = config_debug_option_re.findall(line) + if match: + for option in match: + if not in_comment(CONFIG_FILE, line, option): + if option not in options_in_use: + options_in_use.add(option) + + return options_in_use + def print_missing_config_options(hunks, config_options): - """Searches thru all the changes in hunks for missing options and prints them. - - Args: - hunks: A list of Hunk objects which represent the hunks from the git - diff output. - config_options: A list of all the config options in the main CONFIG_FILE. - - Returns: - missing_config_option: A boolean indicating if any CONFIG_* options - are missing from the main CONFIG_FILE in this commit or if any CONFIG_* - options removed are no longer being used in the repo. - """ - missing_config_option = False - print_banner = True - deprecated_options = set() - # Determine longest CONFIG_* length to be used for formatting. - max_option_length = max(len(option) for option in config_options) - config_option_re = re.compile(r'\b(CONFIG_[a-zA-Z0-9_]+)') - - # Search for all CONFIG_* options in use in the repo. - options_in_use = obtain_config_options_in_use() - - # Check each hunk's line for a missing config option. - for h in hunks: - for l in h.lines: - # Check for the existence of a CONFIG_* in the line. - match = filter(lambda opt: opt in ALLOWLIST_CONFIGS, - config_option_re.findall(l.string)) - if not match: - continue - - # At this point, an option was found in the line. However, we need to - # verify that it is not within a comment. - violations = set() - - for option in match: - if not in_comment(h.filename, l.string, option): - # Since the CONFIG_* option is not within a comment, we've found a - # violation. We now need to determine if this line is a deletion or - # not. For deletions, we will need to verify if this CONFIG_* option - # is no longer being used in the entire repo. - - if l.line_type == '-': - if option not in options_in_use and option in config_options: - deprecated_options.add(option) - else: - violations.add(option) - - # Check to see if the CONFIG_* option is in the config file and print the - # violations. - for option in match: - if option not in config_options and option in violations: - # Print the banner once. - if print_banner: - print('The following config options were found to be missing ' - 'from %s.\n' - 'Please add new config options there along with ' - 'descriptions.\n\n' % CONFIG_FILE) - print_banner = False + """Searches thru all the changes in hunks for missing options and prints them. + + Args: + hunks: A list of Hunk objects which represent the hunks from the git + diff output. + config_options: A list of all the config options in the main CONFIG_FILE. + + Returns: + missing_config_option: A boolean indicating if any CONFIG_* options + are missing from the main CONFIG_FILE in this commit or if any CONFIG_* + options removed are no longer being used in the repo. + """ + missing_config_option = False + print_banner = True + deprecated_options = set() + # Determine longest CONFIG_* length to be used for formatting. + max_option_length = max(len(option) for option in config_options) + config_option_re = re.compile(r"\b(CONFIG_[a-zA-Z0-9_]+)") + + # Search for all CONFIG_* options in use in the repo. + options_in_use = obtain_config_options_in_use() + + # Check each hunk's line for a missing config option. + for h in hunks: + for l in h.lines: + # Check for the existence of a CONFIG_* in the line. + match = filter( + lambda opt: opt in ALLOWLIST_CONFIGS, config_option_re.findall(l.string) + ) + if not match: + continue + + # At this point, an option was found in the line. However, we need to + # verify that it is not within a comment. + violations = set() + + for option in match: + if not in_comment(h.filename, l.string, option): + # Since the CONFIG_* option is not within a comment, we've found a + # violation. We now need to determine if this line is a deletion or + # not. For deletions, we will need to verify if this CONFIG_* option + # is no longer being used in the entire repo. + + if l.line_type == "-": + if option not in options_in_use and option in config_options: + deprecated_options.add(option) + else: + violations.add(option) + + # Check to see if the CONFIG_* option is in the config file and print the + # violations. + for option in match: + if option not in config_options and option in violations: + # Print the banner once. + if print_banner: + print( + "The following config options were found to be missing " + "from %s.\n" + "Please add new config options there along with " + "descriptions.\n\n" % CONFIG_FILE + ) + print_banner = False + missing_config_option = True + # Print the misssing config option. + print( + "> %-*s %s:%s" + % (max_option_length, option, h.filename, l.line_num) + ) + + if deprecated_options: + print( + "\n\nThe following config options are being removed and also appear" + " to be the last uses\nof that option. Please remove these " + "options from %s.\n\n" % CONFIG_FILE + ) + for option in deprecated_options: + print("> %s" % option) missing_config_option = True - # Print the misssing config option. - print('> %-*s %s:%s' % (max_option_length, option, - h.filename, - l.line_num)) - if deprecated_options: - print('\n\nThe following config options are being removed and also appear' - ' to be the last uses\nof that option. Please remove these ' - 'options from %s.\n\n' % CONFIG_FILE) - for option in deprecated_options: - print('> %s' % option) - missing_config_option = True + return missing_config_option - return missing_config_option def in_comment(filename, line, substr): - """Checks if given substring appears in a comment. - - Args: - filename: The filename where this line is from. This is used to determine - what kind of comments to look for. - line: String of line to search in. - substr: Substring to search for in the line. - - Returns: - is_in_comment: Boolean indicating if substr was in a comment. - """ - - c_style_ext = ('.c', '.h', '.inc', '.S') - make_style_ext = ('.mk') - is_in_comment = False - - extension = os.path.splitext(filename)[1] - substr_idx = line.find(substr) - - # Different files have different comment syntax; Handle appropriately. - if extension in c_style_ext: - beg_comment_idx = line.find('/*') - end_comment_idx = line.find('*/') - if end_comment_idx == -1: - end_comment_idx = len(line) - - if beg_comment_idx == -1: - # Check to see if this line is from a multi-line comment. - if line.lstrip().startswith('* '): - # It _seems_ like it is. - is_in_comment = True - else: - # Check to see if its actually inside the comment. - if beg_comment_idx < substr_idx < end_comment_idx: - is_in_comment = True - elif extension in make_style_ext or 'Makefile' in filename: - beg_comment_idx = line.find('#') - # Ignore everything to the right of the hash. - if beg_comment_idx < substr_idx and beg_comment_idx != -1: - is_in_comment = True - return is_in_comment + """Checks if given substring appears in a comment. + + Args: + filename: The filename where this line is from. This is used to determine + what kind of comments to look for. + line: String of line to search in. + substr: Substring to search for in the line. + + Returns: + is_in_comment: Boolean indicating if substr was in a comment. + """ + + c_style_ext = (".c", ".h", ".inc", ".S") + make_style_ext = ".mk" + is_in_comment = False + + extension = os.path.splitext(filename)[1] + substr_idx = line.find(substr) + + # Different files have different comment syntax; Handle appropriately. + if extension in c_style_ext: + beg_comment_idx = line.find("/*") + end_comment_idx = line.find("*/") + if end_comment_idx == -1: + end_comment_idx = len(line) + + if beg_comment_idx == -1: + # Check to see if this line is from a multi-line comment. + if line.lstrip().startswith("* "): + # It _seems_ like it is. + is_in_comment = True + else: + # Check to see if its actually inside the comment. + if beg_comment_idx < substr_idx < end_comment_idx: + is_in_comment = True + elif extension in make_style_ext or "Makefile" in filename: + beg_comment_idx = line.find("#") + # Ignore everything to the right of the hash. + if beg_comment_idx < substr_idx and beg_comment_idx != -1: + is_in_comment = True + return is_in_comment + def get_hunks(): - """Gets the hunks of the most recent commit. - - States: - new_file: Searching for a new file in the git diff. - filename_search: Searching for the filename of this hunk. - hunk: Searching for the beginning of a new hunk. - lines: Counting line numbers and searching for changes. - - Returns: - hunks: A list of Hunk objects which represent the hunks in the git diff - output. - """ - - diff = [] - hunks = [] - hunk_lines = [] - line = '' - filename = '' - i = 0 - line_num = 0 - - # Regex patterns - new_file_re = re.compile(r'^diff --git') - filename_re = re.compile(r'^[+]{3} (.*)') - hunk_line_num_re = re.compile(r'^@@ -[0-9]+,[0-9]+ \+([0-9]+),[0-9]+ @@.*') - line_re = re.compile(r'^([+| |-])(.*)') - - # Get the diff output. - proc = subprocess.run(['git', 'diff', '--cached', '-GCONFIG_*', '--no-prefix', - '--no-ext-diff', 'HEAD~1'], - stdout=subprocess.PIPE, - encoding='utf-8', - check=True) - diff = proc.stdout.splitlines() - if not diff: - return [] - line = diff[0] - - state = enum.Enum('state', 'NEW_FILE FILENAME_SEARCH HUNK LINES') - current_state = state.NEW_FILE - - while True: - # Search for the beginning of a new file. - if current_state is state.NEW_FILE: - match = new_file_re.search(line) - if match: - current_state = state.FILENAME_SEARCH - - # Search the diff output for a file name. - elif current_state is state.FILENAME_SEARCH: - # Search for a file name. - match = filename_re.search(line) - if match: - filename = match.groups(1)[0] - if filename in ALLOWLIST or ALLOW_PATTERN.match(filename): - # Skip the file if it's allowlisted. - current_state = state.NEW_FILE - else: - current_state = state.HUNK - - # Search for a hunk. Each hunk starts with a line describing the line - # numbers in the file. - elif current_state is state.HUNK: - hunk_lines = [] - match = hunk_line_num_re.search(line) - if match: - # Extract the line number offset. - line_num = int(match.groups(1)[0]) - current_state = state.LINES - - # Start looking for changes. - elif current_state is state.LINES: - # Check if state needs updating. - new_hunk = hunk_line_num_re.search(line) - new_file = new_file_re.search(line) - if new_hunk: - current_state = state.HUNK - hunks.append(Hunk(filename, hunk_lines)) - continue - elif new_file: - current_state = state.NEW_FILE - hunks.append(Hunk(filename, hunk_lines)) - continue - - match = line_re.search(line) - if match: - line_type = match.groups(1)[0] - # We only care about modifications. - if line_type != ' ': - hunk_lines.append(Line(line_num, match.groups(2)[1], line_type)) - # Deletions don't count towards the line numbers. - if line_type != '-': - line_num += 1 - - # Advance to the next line - try: - i += 1 - line = diff[i] - except IndexError: - # We've reached the end of the diff. Return what we have. - if hunk_lines: - hunks.append(Hunk(filename, hunk_lines)) - return hunks + """Gets the hunks of the most recent commit. + + States: + new_file: Searching for a new file in the git diff. + filename_search: Searching for the filename of this hunk. + hunk: Searching for the beginning of a new hunk. + lines: Counting line numbers and searching for changes. + + Returns: + hunks: A list of Hunk objects which represent the hunks in the git diff + output. + """ + + diff = [] + hunks = [] + hunk_lines = [] + line = "" + filename = "" + i = 0 + line_num = 0 + + # Regex patterns + new_file_re = re.compile(r"^diff --git") + filename_re = re.compile(r"^[+]{3} (.*)") + hunk_line_num_re = re.compile(r"^@@ -[0-9]+,[0-9]+ \+([0-9]+),[0-9]+ @@.*") + line_re = re.compile(r"^([+| |-])(.*)") + + # Get the diff output. + proc = subprocess.run( + [ + "git", + "diff", + "--cached", + "-GCONFIG_*", + "--no-prefix", + "--no-ext-diff", + "HEAD~1", + ], + stdout=subprocess.PIPE, + encoding="utf-8", + check=True, + ) + diff = proc.stdout.splitlines() + if not diff: + return [] + line = diff[0] + + state = enum.Enum("state", "NEW_FILE FILENAME_SEARCH HUNK LINES") + current_state = state.NEW_FILE + + while True: + # Search for the beginning of a new file. + if current_state is state.NEW_FILE: + match = new_file_re.search(line) + if match: + current_state = state.FILENAME_SEARCH + + # Search the diff output for a file name. + elif current_state is state.FILENAME_SEARCH: + # Search for a file name. + match = filename_re.search(line) + if match: + filename = match.groups(1)[0] + if filename in ALLOWLIST or ALLOW_PATTERN.match(filename): + # Skip the file if it's allowlisted. + current_state = state.NEW_FILE + else: + current_state = state.HUNK + + # Search for a hunk. Each hunk starts with a line describing the line + # numbers in the file. + elif current_state is state.HUNK: + hunk_lines = [] + match = hunk_line_num_re.search(line) + if match: + # Extract the line number offset. + line_num = int(match.groups(1)[0]) + current_state = state.LINES + + # Start looking for changes. + elif current_state is state.LINES: + # Check if state needs updating. + new_hunk = hunk_line_num_re.search(line) + new_file = new_file_re.search(line) + if new_hunk: + current_state = state.HUNK + hunks.append(Hunk(filename, hunk_lines)) + continue + elif new_file: + current_state = state.NEW_FILE + hunks.append(Hunk(filename, hunk_lines)) + continue + + match = line_re.search(line) + if match: + line_type = match.groups(1)[0] + # We only care about modifications. + if line_type != " ": + hunk_lines.append(Line(line_num, match.groups(2)[1], line_type)) + # Deletions don't count towards the line numbers. + if line_type != "-": + line_num += 1 + + # Advance to the next line + try: + i += 1 + line = diff[i] + except IndexError: + # We've reached the end of the diff. Return what we have. + if hunk_lines: + hunks.append(Hunk(filename, hunk_lines)) + return hunks + def main(): - """Searches through committed changes for missing config options. - - Checks through committed changes for CONFIG_* options. Then checks to make - sure that all CONFIG_* options used are defined in include/config.h. Finally, - reports any missing config options. - """ - # Obtain the hunks of the commit to search through. - hunks = get_hunks() - # Obtain config options from include/config.h. - config_options = obtain_current_config_options() - # Find any missing config options from the hunks and print them. - missing_opts = print_missing_config_options(hunks, config_options) - - if missing_opts: - print('\nIt may also be possible that you have a typo.') - sys.exit(1) - -if __name__ == '__main__': - main() + """Searches through committed changes for missing config options. + + Checks through committed changes for CONFIG_* options. Then checks to make + sure that all CONFIG_* options used are defined in include/config.h. Finally, + reports any missing config options. + """ + # Obtain the hunks of the commit to search through. + hunks = get_hunks() + # Obtain config options from include/config.h. + config_options = obtain_current_config_options() + # Find any missing config options from the hunks and print them. + missing_opts = print_missing_config_options(hunks, config_options) + + if missing_opts: + print("\nIt may also be possible that you have a typo.") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/util/ec3po/console.py b/util/ec3po/console.py index e71216e3f2..33fa5a6775 100755 --- a/util/ec3po/console.py +++ b/util/ec3po/console.py @@ -17,7 +17,6 @@ from __future__ import print_function import argparse import binascii import ctypes -from datetime import datetime import logging import os import pty @@ -26,26 +25,25 @@ import select import stat import sys import traceback +from datetime import datetime import six +from ec3po import interpreter, threadproc_shim -from ec3po import interpreter -from ec3po import threadproc_shim - - -PROMPT = b'> ' +PROMPT = b"> " CONSOLE_INPUT_LINE_SIZE = 80 # Taken from the CONFIG_* with the same name. CONSOLE_MAX_READ = 100 # Max bytes to read at a time from the user. LOOK_BUFFER_SIZE = 256 # Size of search window when looking for the enhanced EC - # image string. +# image string. # In console_init(), the EC will print a string saying that the EC console is # enabled. Enhanced images will print a slightly different string. These # regular expressions are used to determine at reboot whether the EC image is # enhanced or not. -ENHANCED_IMAGE_RE = re.compile(br'Enhanced Console is enabled ' - br'\(v([0-9]+\.[0-9]+\.[0-9]+)\)') -NON_ENHANCED_IMAGE_RE = re.compile(br'Console is enabled; ') +ENHANCED_IMAGE_RE = re.compile( + rb"Enhanced Console is enabled " rb"\(v([0-9]+\.[0-9]+\.[0-9]+)\)" +) +NON_ENHANCED_IMAGE_RE = re.compile(rb"Console is enabled; ") # The timeouts are really only useful for enhanced EC images, but otherwise just # serve as a delay for non-enhanced EC images. Therefore, we can keep this @@ -54,1118 +52,1173 @@ NON_ENHANCED_IMAGE_RE = re.compile(br'Console is enabled; ') # EC image, we can increase the timeout for stability just in case it takes a # bit longer to receive an ACK for some reason. NON_ENHANCED_EC_INTERROGATION_TIMEOUT = 0.3 # Maximum number of seconds to wait - # for a response to an - # interrogation of a non-enhanced - # EC image. +# for a response to an +# interrogation of a non-enhanced +# EC image. ENHANCED_EC_INTERROGATION_TIMEOUT = 1.0 # Maximum number of seconds to wait for - # a response to an interrogation of an - # enhanced EC image. +# a response to an interrogation of an +# enhanced EC image. # List of modes which control when interrogations are performed with the EC. -INTERROGATION_MODES = [b'never', b'always', b'auto'] +INTERROGATION_MODES = [b"never", b"always", b"auto"] # Format for printing host timestamp -HOST_STRFTIME="%y-%m-%d %H:%M:%S.%f" +HOST_STRFTIME = "%y-%m-%d %H:%M:%S.%f" class EscState(object): - """Class which contains an enumeration for states of ESC sequences.""" - ESC_START = 1 - ESC_BRACKET = 2 - ESC_BRACKET_1 = 3 - ESC_BRACKET_3 = 4 - ESC_BRACKET_8 = 5 - + """Class which contains an enumeration for states of ESC sequences.""" -class ControlKey(object): - """Class which contains codes for various control keys.""" - BACKSPACE = 0x08 - CTRL_A = 0x01 - CTRL_B = 0x02 - CTRL_D = 0x04 - CTRL_E = 0x05 - CTRL_F = 0x06 - CTRL_K = 0x0b - CTRL_N = 0xe - CTRL_P = 0x10 - CARRIAGE_RETURN = 0x0d - ESC = 0x1b + ESC_START = 1 + ESC_BRACKET = 2 + ESC_BRACKET_1 = 3 + ESC_BRACKET_3 = 4 + ESC_BRACKET_8 = 5 -class Console(object): - """Class which provides the console interface between the EC and the user. - - This class essentially represents the console interface between the user and - the EC. It handles all of the console editing behaviour - - Attributes: - logger: A logger for this module. - controller_pty: File descriptor to the controller side of the PTY. Used for - driving output to the user and receiving user input. - user_pty: A string representing the PTY name of the served console. - cmd_pipe: A socket.socket or multiprocessing.Connection object which - represents the console side of the command pipe. This must be a - bidirectional pipe. Console commands and responses utilize this pipe. - dbg_pipe: A socket.socket or multiprocessing.Connection object which - represents the console's read-only side of the debug pipe. This must be a - unidirectional pipe attached to the intepreter. EC debug messages use - this pipe. - oobm_queue: A queue.Queue or multiprocessing.Queue which is used for out of - band management for the interactive console. - input_buffer: A string representing the current input command. - input_buffer_pos: An integer representing the current position in the buffer - to insert a char. - partial_cmd: A string representing the command entered on a line before - pressing the up arrow keys. - esc_state: An integer represeting the current state within an escape - sequence. - line_limit: An integer representing the maximum number of characters on a - line. - history: A list of strings containing the past entered console commands. - history_pos: An integer representing the current history buffer position. - This index is used to show previous commands. - prompt: A string representing the console prompt displayed to the user. - enhanced_ec: A boolean indicating if the EC image that we are currently - communicating with is enhanced or not. Enhanced EC images will support - packed commands and host commands over the UART. This defaults to False - until we perform some handshaking. - interrogation_timeout: A float representing the current maximum seconds to - wait for a response to an interrogation. - receiving_oobm_cmd: A boolean indicating whether or not the console is in - the middle of receiving an out of band command. - pending_oobm_cmd: A string containing the pending OOBM command. - interrogation_mode: A string containing the current mode of whether - interrogations are performed with the EC or not and how often. - raw_debug: Flag to indicate whether per interrupt data should be logged to - debug - output_line_log_buffer: buffer for lines coming from the EC to log to debug - """ - - def __init__(self, controller_pty, user_pty, interface_pty, cmd_pipe, dbg_pipe, - name=None): - """Initalises a Console object with the provided arguments. +class ControlKey(object): + """Class which contains codes for various control keys.""" - Args: - controller_pty: File descriptor to the controller side of the PTY. Used for - driving output to the user and receiving user input. - user_pty: A string representing the PTY name of the served console. - interface_pty: A string representing the PTY name of the served command - interface. - cmd_pipe: A socket.socket or multiprocessing.Connection object which - represents the console side of the command pipe. This must be a - bidirectional pipe. Console commands and responses utilize this pipe. - dbg_pipe: A socket.socket or multiprocessing.Connection object which - represents the console's read-only side of the debug pipe. This must be a - unidirectional pipe attached to the intepreter. EC debug messages use - this pipe. - name: the console source name - """ - # Create a unique logger based on the console name - console_prefix = ('%s - ' % name) if name else '' - logger = logging.getLogger('%sEC3PO.Console' % console_prefix) - self.logger = interpreter.LoggerAdapter(logger, {'pty': user_pty}) - self.controller_pty = controller_pty - self.user_pty = user_pty - self.interface_pty = interface_pty - self.cmd_pipe = cmd_pipe - self.dbg_pipe = dbg_pipe - self.oobm_queue = threadproc_shim.Queue() - self.input_buffer = b'' - self.input_buffer_pos = 0 - self.partial_cmd = b'' - self.esc_state = 0 - self.line_limit = CONSOLE_INPUT_LINE_SIZE - self.history = [] - self.history_pos = 0 - self.prompt = PROMPT - self.enhanced_ec = False - self.interrogation_timeout = NON_ENHANCED_EC_INTERROGATION_TIMEOUT - self.receiving_oobm_cmd = False - self.pending_oobm_cmd = b'' - self.interrogation_mode = b'auto' - self.timestamp_enabled = True - self.look_buffer = b'' - self.raw_debug = False - self.output_line_log_buffer = [] - - def __str__(self): - """Show internal state of Console object as a string.""" - string = [] - string.append('controller_pty: %s' % self.controller_pty) - string.append('user_pty: %s' % self.user_pty) - string.append('interface_pty: %s' % self.interface_pty) - string.append('cmd_pipe: %s' % self.cmd_pipe) - string.append('dbg_pipe: %s' % self.dbg_pipe) - string.append('oobm_queue: %s' % self.oobm_queue) - string.append('input_buffer: %s' % self.input_buffer) - string.append('input_buffer_pos: %d' % self.input_buffer_pos) - string.append('esc_state: %d' % self.esc_state) - string.append('line_limit: %d' % self.line_limit) - string.append('history: %r' % self.history) - string.append('history_pos: %d' % self.history_pos) - string.append('prompt: %r' % self.prompt) - string.append('partial_cmd: %r'% self.partial_cmd) - string.append('interrogation_mode: %r' % self.interrogation_mode) - string.append('look_buffer: %r' % self.look_buffer) - return '\n'.join(string) - - def LogConsoleOutput(self, data): - """Log to debug user MCU output to controller_pty when line is filled. - - The logging also suppresses the Cr50 spinner lines by removing characters - when it sees backspaces. + BACKSPACE = 0x08 + CTRL_A = 0x01 + CTRL_B = 0x02 + CTRL_D = 0x04 + CTRL_E = 0x05 + CTRL_F = 0x06 + CTRL_K = 0x0B + CTRL_N = 0xE + CTRL_P = 0x10 + CARRIAGE_RETURN = 0x0D + ESC = 0x1B - Args: - data: bytes - string received from MCU - """ - data = list(data) - # For compatibility with python2 and python3, standardize on the data - # being a list of integers. This requires one more transformation in py2 - if not isinstance(data[0], int): - data = [ord(c) for c in data] - - # This is a list of already filtered characters (or placeholders). - line = self.output_line_log_buffer - - # TODO(b/177480273): use raw strings here - symbols = { - ord(b'\n'): u'\\n', - ord(b'\r'): u'\\r', - ord(b'\t'): u'\\t' - } - # self.logger.debug(u'%s + %r', u''.join(line), ''.join(data)) - while data: - # Recall, data is a list of integers, namely the byte values sent by - # the MCU. - byte = data.pop(0) - # This means that |byte| is an int. - if byte == ord('\n'): - line.append(symbols[byte]) - if line: - self.logger.debug(u'%s', ''.join(line)) - line = [] - elif byte == ord('\b'): - # Backspace: trim the last character off the buffer - if line: - line.pop(-1) - elif byte in symbols: - line.append(symbols[byte]) - elif byte < ord(' ') or byte > ord('~'): - # Turn any character that isn't printable ASCII into escaped hex. - # ' ' is chr(20), and 0-19 are unprintable control characters. - # '~' is chr(126), and 127 is DELETE. 128-255 are control and Latin-1. - line.append(u'\\x%02x' % byte) - else: - # byte is printable. Thus it is safe to use chr() to get the printable - # character out of it again. - line.append(u'%s' % chr(byte)) - self.output_line_log_buffer = line - - def PrintHistory(self): - """Print the history of entered commands.""" - fd = self.controller_pty - # Make it pretty by figuring out how wide to pad the numbers. - wide = (len(self.history) // 10) + 1 - for i in range(len(self.history)): - line = b' %*d %s\r\n' % (wide, i, self.history[i]) - os.write(fd, line) - - def ShowPreviousCommand(self): - """Shows the previous command from the history list.""" - # There's nothing to do if there's no history at all. - if not self.history: - self.logger.debug('No history to print.') - return - - # Don't do anything if there's no more history to show. - if self.history_pos == 0: - self.logger.debug('No more history to show.') - return - - self.logger.debug('current history position: %d.', self.history_pos) - - # Decrement the history buffer position. - self.history_pos -= 1 - self.logger.debug('new history position.: %d', self.history_pos) - - # Save the text entered on the console if any. - if self.history_pos == len(self.history)-1: - self.logger.debug('saving partial_cmd: %r', self.input_buffer) - self.partial_cmd = self.input_buffer - - # Backspace the line. - for _ in range(self.input_buffer_pos): - self.SendBackspace() - - # Print the last entry in the history buffer. - self.logger.debug('printing previous entry %d - %s', self.history_pos, - self.history[self.history_pos]) - fd = self.controller_pty - prev_cmd = self.history[self.history_pos] - os.write(fd, prev_cmd) - # Update the input buffer. - self.input_buffer = prev_cmd - self.input_buffer_pos = len(prev_cmd) - - def ShowNextCommand(self): - """Shows the next command from the history list.""" - # Don't do anything if there's no history at all. - if not self.history: - self.logger.debug('History buffer is empty.') - return - - fd = self.controller_pty - - self.logger.debug('current history position: %d', self.history_pos) - # Increment the history position. - self.history_pos += 1 - - # Restore the partial cmd. - if self.history_pos == len(self.history): - self.logger.debug('Restoring partial command of %r', self.partial_cmd) - # Backspace the line. - for _ in range(self.input_buffer_pos): - self.SendBackspace() - # Print the partially entered command if any. - os.write(fd, self.partial_cmd) - self.input_buffer = self.partial_cmd - self.input_buffer_pos = len(self.input_buffer) - # Now that we've printed it, clear the partial cmd storage. - self.partial_cmd = b'' - # Reset history position. - self.history_pos = len(self.history) - return - - self.logger.debug('new history position: %d', self.history_pos) - if self.history_pos > len(self.history)-1: - self.logger.debug('No more history to show.') - self.history_pos -= 1 - self.logger.debug('Reset history position to %d', self.history_pos) - return - - # Backspace the line. - for _ in range(self.input_buffer_pos): - self.SendBackspace() - - # Print the newer entry from the history buffer. - self.logger.debug('printing next entry %d - %s', self.history_pos, - self.history[self.history_pos]) - next_cmd = self.history[self.history_pos] - os.write(fd, next_cmd) - # Update the input buffer. - self.input_buffer = next_cmd - self.input_buffer_pos = len(next_cmd) - self.logger.debug('new history position: %d.', self.history_pos) - - def SliceOutChar(self): - """Remove a char from the line and shift everything over 1 column.""" - fd = self.controller_pty - # Remove the character at the input_buffer_pos by slicing it out. - self.input_buffer = self.input_buffer[0:self.input_buffer_pos] + \ - self.input_buffer[self.input_buffer_pos+1:] - # Write the rest of the line - moved_col = os.write(fd, self.input_buffer[self.input_buffer_pos:]) - # Write a space to clear out the last char - moved_col += os.write(fd, b' ') - # Update the input buffer position. - self.input_buffer_pos += moved_col - # Reset the cursor - self.MoveCursor('left', moved_col) - - def HandleEsc(self, byte): - """HandleEsc processes escape sequences. - Args: - byte: An integer representing the current byte in the sequence. +class Console(object): + """Class which provides the console interface between the EC and the user. + + This class essentially represents the console interface between the user and + the EC. It handles all of the console editing behaviour + + Attributes: + logger: A logger for this module. + controller_pty: File descriptor to the controller side of the PTY. Used for + driving output to the user and receiving user input. + user_pty: A string representing the PTY name of the served console. + cmd_pipe: A socket.socket or multiprocessing.Connection object which + represents the console side of the command pipe. This must be a + bidirectional pipe. Console commands and responses utilize this pipe. + dbg_pipe: A socket.socket or multiprocessing.Connection object which + represents the console's read-only side of the debug pipe. This must be a + unidirectional pipe attached to the intepreter. EC debug messages use + this pipe. + oobm_queue: A queue.Queue or multiprocessing.Queue which is used for out of + band management for the interactive console. + input_buffer: A string representing the current input command. + input_buffer_pos: An integer representing the current position in the buffer + to insert a char. + partial_cmd: A string representing the command entered on a line before + pressing the up arrow keys. + esc_state: An integer represeting the current state within an escape + sequence. + line_limit: An integer representing the maximum number of characters on a + line. + history: A list of strings containing the past entered console commands. + history_pos: An integer representing the current history buffer position. + This index is used to show previous commands. + prompt: A string representing the console prompt displayed to the user. + enhanced_ec: A boolean indicating if the EC image that we are currently + communicating with is enhanced or not. Enhanced EC images will support + packed commands and host commands over the UART. This defaults to False + until we perform some handshaking. + interrogation_timeout: A float representing the current maximum seconds to + wait for a response to an interrogation. + receiving_oobm_cmd: A boolean indicating whether or not the console is in + the middle of receiving an out of band command. + pending_oobm_cmd: A string containing the pending OOBM command. + interrogation_mode: A string containing the current mode of whether + interrogations are performed with the EC or not and how often. + raw_debug: Flag to indicate whether per interrupt data should be logged to + debug + output_line_log_buffer: buffer for lines coming from the EC to log to debug """ - # We shouldn't be handling an escape sequence if we haven't seen one. - assert self.esc_state != 0 - - if self.esc_state is EscState.ESC_START: - self.logger.debug('ESC_START') - if byte == ord('['): - self.esc_state = EscState.ESC_BRACKET - return - else: - self.logger.error('Unexpected sequence. %c', byte) - self.esc_state = 0 - - elif self.esc_state is EscState.ESC_BRACKET: - self.logger.debug('ESC_BRACKET') - # Left Arrow key was pressed. - if byte == ord('D'): - self.logger.debug('Left arrow key pressed.') - self.MoveCursor('left', 1) - self.esc_state = 0 # Reset the state. - return - - # Right Arrow key. - elif byte == ord('C'): - self.logger.debug('Right arrow key pressed.') - self.MoveCursor('right', 1) - self.esc_state = 0 # Reset the state. - return - - # Up Arrow key. - elif byte == ord('A'): - self.logger.debug('Up arrow key pressed.') - self.ShowPreviousCommand() - # Reset the state. - self.esc_state = 0 # Reset the state. - return - - # Down Arrow key. - elif byte == ord('B'): - self.logger.debug('Down arrow key pressed.') - self.ShowNextCommand() - # Reset the state. - self.esc_state = 0 # Reset the state. - return - - # For some reason, minicom sends a 1 instead of 7. /shrug - # TODO(aaboagye): Figure out why this happens. - elif byte == ord('1') or byte == ord('7'): - self.esc_state = EscState.ESC_BRACKET_1 - - elif byte == ord('3'): - self.esc_state = EscState.ESC_BRACKET_3 - - elif byte == ord('8'): - self.esc_state = EscState.ESC_BRACKET_8 - - else: - self.logger.error(r'Bad or unhandled escape sequence. got ^[%c\(%d)', - chr(byte), byte) - self.esc_state = 0 - return - - elif self.esc_state is EscState.ESC_BRACKET_1: - self.logger.debug('ESC_BRACKET_1') - # HOME key. - if byte == ord('~'): - self.logger.debug('Home key pressed.') - self.MoveCursor('left', self.input_buffer_pos) - self.esc_state = 0 # Reset the state. - self.logger.debug('ESC sequence complete.') - return - - elif self.esc_state is EscState.ESC_BRACKET_3: - self.logger.debug('ESC_BRACKET_3') - # DEL key. - if byte == ord('~'): - self.logger.debug('Delete key pressed.') - if self.input_buffer_pos != len(self.input_buffer): - self.SliceOutChar() - self.esc_state = 0 # Reset the state. - - elif self.esc_state is EscState.ESC_BRACKET_8: - self.logger.debug('ESC_BRACKET_8') - # END key. - if byte == ord('~'): - self.logger.debug('End key pressed.') - self.MoveCursor('right', - len(self.input_buffer) - self.input_buffer_pos) - self.esc_state = 0 # Reset the state. - self.logger.debug('ESC sequence complete.') - return - - else: - self.logger.error('Unexpected sequence. %c', byte) + def __init__( + self, controller_pty, user_pty, interface_pty, cmd_pipe, dbg_pipe, name=None + ): + """Initalises a Console object with the provided arguments. + + Args: + controller_pty: File descriptor to the controller side of the PTY. Used for + driving output to the user and receiving user input. + user_pty: A string representing the PTY name of the served console. + interface_pty: A string representing the PTY name of the served command + interface. + cmd_pipe: A socket.socket or multiprocessing.Connection object which + represents the console side of the command pipe. This must be a + bidirectional pipe. Console commands and responses utilize this pipe. + dbg_pipe: A socket.socket or multiprocessing.Connection object which + represents the console's read-only side of the debug pipe. This must be a + unidirectional pipe attached to the intepreter. EC debug messages use + this pipe. + name: the console source name + """ + # Create a unique logger based on the console name + console_prefix = ("%s - " % name) if name else "" + logger = logging.getLogger("%sEC3PO.Console" % console_prefix) + self.logger = interpreter.LoggerAdapter(logger, {"pty": user_pty}) + self.controller_pty = controller_pty + self.user_pty = user_pty + self.interface_pty = interface_pty + self.cmd_pipe = cmd_pipe + self.dbg_pipe = dbg_pipe + self.oobm_queue = threadproc_shim.Queue() + self.input_buffer = b"" + self.input_buffer_pos = 0 + self.partial_cmd = b"" self.esc_state = 0 + self.line_limit = CONSOLE_INPUT_LINE_SIZE + self.history = [] + self.history_pos = 0 + self.prompt = PROMPT + self.enhanced_ec = False + self.interrogation_timeout = NON_ENHANCED_EC_INTERROGATION_TIMEOUT + self.receiving_oobm_cmd = False + self.pending_oobm_cmd = b"" + self.interrogation_mode = b"auto" + self.timestamp_enabled = True + self.look_buffer = b"" + self.raw_debug = False + self.output_line_log_buffer = [] + + def __str__(self): + """Show internal state of Console object as a string.""" + string = [] + string.append("controller_pty: %s" % self.controller_pty) + string.append("user_pty: %s" % self.user_pty) + string.append("interface_pty: %s" % self.interface_pty) + string.append("cmd_pipe: %s" % self.cmd_pipe) + string.append("dbg_pipe: %s" % self.dbg_pipe) + string.append("oobm_queue: %s" % self.oobm_queue) + string.append("input_buffer: %s" % self.input_buffer) + string.append("input_buffer_pos: %d" % self.input_buffer_pos) + string.append("esc_state: %d" % self.esc_state) + string.append("line_limit: %d" % self.line_limit) + string.append("history: %r" % self.history) + string.append("history_pos: %d" % self.history_pos) + string.append("prompt: %r" % self.prompt) + string.append("partial_cmd: %r" % self.partial_cmd) + string.append("interrogation_mode: %r" % self.interrogation_mode) + string.append("look_buffer: %r" % self.look_buffer) + return "\n".join(string) + + def LogConsoleOutput(self, data): + """Log to debug user MCU output to controller_pty when line is filled. + + The logging also suppresses the Cr50 spinner lines by removing characters + when it sees backspaces. + + Args: + data: bytes - string received from MCU + """ + data = list(data) + # For compatibility with python2 and python3, standardize on the data + # being a list of integers. This requires one more transformation in py2 + if not isinstance(data[0], int): + data = [ord(c) for c in data] + + # This is a list of already filtered characters (or placeholders). + line = self.output_line_log_buffer + + # TODO(b/177480273): use raw strings here + symbols = {ord(b"\n"): "\\n", ord(b"\r"): "\\r", ord(b"\t"): "\\t"} + # self.logger.debug(u'%s + %r', u''.join(line), ''.join(data)) + while data: + # Recall, data is a list of integers, namely the byte values sent by + # the MCU. + byte = data.pop(0) + # This means that |byte| is an int. + if byte == ord("\n"): + line.append(symbols[byte]) + if line: + self.logger.debug("%s", "".join(line)) + line = [] + elif byte == ord("\b"): + # Backspace: trim the last character off the buffer + if line: + line.pop(-1) + elif byte in symbols: + line.append(symbols[byte]) + elif byte < ord(" ") or byte > ord("~"): + # Turn any character that isn't printable ASCII into escaped hex. + # ' ' is chr(20), and 0-19 are unprintable control characters. + # '~' is chr(126), and 127 is DELETE. 128-255 are control and Latin-1. + line.append("\\x%02x" % byte) + else: + # byte is printable. Thus it is safe to use chr() to get the printable + # character out of it again. + line.append("%s" % chr(byte)) + self.output_line_log_buffer = line + + def PrintHistory(self): + """Print the history of entered commands.""" + fd = self.controller_pty + # Make it pretty by figuring out how wide to pad the numbers. + wide = (len(self.history) // 10) + 1 + for i in range(len(self.history)): + line = b" %*d %s\r\n" % (wide, i, self.history[i]) + os.write(fd, line) + + def ShowPreviousCommand(self): + """Shows the previous command from the history list.""" + # There's nothing to do if there's no history at all. + if not self.history: + self.logger.debug("No history to print.") + return + + # Don't do anything if there's no more history to show. + if self.history_pos == 0: + self.logger.debug("No more history to show.") + return + + self.logger.debug("current history position: %d.", self.history_pos) + + # Decrement the history buffer position. + self.history_pos -= 1 + self.logger.debug("new history position.: %d", self.history_pos) + + # Save the text entered on the console if any. + if self.history_pos == len(self.history) - 1: + self.logger.debug("saving partial_cmd: %r", self.input_buffer) + self.partial_cmd = self.input_buffer + + # Backspace the line. + for _ in range(self.input_buffer_pos): + self.SendBackspace() + + # Print the last entry in the history buffer. + self.logger.debug( + "printing previous entry %d - %s", + self.history_pos, + self.history[self.history_pos], + ) + fd = self.controller_pty + prev_cmd = self.history[self.history_pos] + os.write(fd, prev_cmd) + # Update the input buffer. + self.input_buffer = prev_cmd + self.input_buffer_pos = len(prev_cmd) + + def ShowNextCommand(self): + """Shows the next command from the history list.""" + # Don't do anything if there's no history at all. + if not self.history: + self.logger.debug("History buffer is empty.") + return + + fd = self.controller_pty + + self.logger.debug("current history position: %d", self.history_pos) + # Increment the history position. + self.history_pos += 1 + + # Restore the partial cmd. + if self.history_pos == len(self.history): + self.logger.debug("Restoring partial command of %r", self.partial_cmd) + # Backspace the line. + for _ in range(self.input_buffer_pos): + self.SendBackspace() + # Print the partially entered command if any. + os.write(fd, self.partial_cmd) + self.input_buffer = self.partial_cmd + self.input_buffer_pos = len(self.input_buffer) + # Now that we've printed it, clear the partial cmd storage. + self.partial_cmd = b"" + # Reset history position. + self.history_pos = len(self.history) + return + + self.logger.debug("new history position: %d", self.history_pos) + if self.history_pos > len(self.history) - 1: + self.logger.debug("No more history to show.") + self.history_pos -= 1 + self.logger.debug("Reset history position to %d", self.history_pos) + return + + # Backspace the line. + for _ in range(self.input_buffer_pos): + self.SendBackspace() + + # Print the newer entry from the history buffer. + self.logger.debug( + "printing next entry %d - %s", + self.history_pos, + self.history[self.history_pos], + ) + next_cmd = self.history[self.history_pos] + os.write(fd, next_cmd) + # Update the input buffer. + self.input_buffer = next_cmd + self.input_buffer_pos = len(next_cmd) + self.logger.debug("new history position: %d.", self.history_pos) + + def SliceOutChar(self): + """Remove a char from the line and shift everything over 1 column.""" + fd = self.controller_pty + # Remove the character at the input_buffer_pos by slicing it out. + self.input_buffer = ( + self.input_buffer[0 : self.input_buffer_pos] + + self.input_buffer[self.input_buffer_pos + 1 :] + ) + # Write the rest of the line + moved_col = os.write(fd, self.input_buffer[self.input_buffer_pos :]) + # Write a space to clear out the last char + moved_col += os.write(fd, b" ") + # Update the input buffer position. + self.input_buffer_pos += moved_col + # Reset the cursor + self.MoveCursor("left", moved_col) + + def HandleEsc(self, byte): + """HandleEsc processes escape sequences. + + Args: + byte: An integer representing the current byte in the sequence. + """ + # We shouldn't be handling an escape sequence if we haven't seen one. + assert self.esc_state != 0 + + if self.esc_state is EscState.ESC_START: + self.logger.debug("ESC_START") + if byte == ord("["): + self.esc_state = EscState.ESC_BRACKET + return + + else: + self.logger.error("Unexpected sequence. %c", byte) + self.esc_state = 0 + + elif self.esc_state is EscState.ESC_BRACKET: + self.logger.debug("ESC_BRACKET") + # Left Arrow key was pressed. + if byte == ord("D"): + self.logger.debug("Left arrow key pressed.") + self.MoveCursor("left", 1) + self.esc_state = 0 # Reset the state. + return + + # Right Arrow key. + elif byte == ord("C"): + self.logger.debug("Right arrow key pressed.") + self.MoveCursor("right", 1) + self.esc_state = 0 # Reset the state. + return + + # Up Arrow key. + elif byte == ord("A"): + self.logger.debug("Up arrow key pressed.") + self.ShowPreviousCommand() + # Reset the state. + self.esc_state = 0 # Reset the state. + return + + # Down Arrow key. + elif byte == ord("B"): + self.logger.debug("Down arrow key pressed.") + self.ShowNextCommand() + # Reset the state. + self.esc_state = 0 # Reset the state. + return + + # For some reason, minicom sends a 1 instead of 7. /shrug + # TODO(aaboagye): Figure out why this happens. + elif byte == ord("1") or byte == ord("7"): + self.esc_state = EscState.ESC_BRACKET_1 + + elif byte == ord("3"): + self.esc_state = EscState.ESC_BRACKET_3 + + elif byte == ord("8"): + self.esc_state = EscState.ESC_BRACKET_8 + + else: + self.logger.error( + r"Bad or unhandled escape sequence. got ^[%c\(%d)", chr(byte), byte + ) + self.esc_state = 0 + return + + elif self.esc_state is EscState.ESC_BRACKET_1: + self.logger.debug("ESC_BRACKET_1") + # HOME key. + if byte == ord("~"): + self.logger.debug("Home key pressed.") + self.MoveCursor("left", self.input_buffer_pos) + self.esc_state = 0 # Reset the state. + self.logger.debug("ESC sequence complete.") + return + + elif self.esc_state is EscState.ESC_BRACKET_3: + self.logger.debug("ESC_BRACKET_3") + # DEL key. + if byte == ord("~"): + self.logger.debug("Delete key pressed.") + if self.input_buffer_pos != len(self.input_buffer): + self.SliceOutChar() + self.esc_state = 0 # Reset the state. + + elif self.esc_state is EscState.ESC_BRACKET_8: + self.logger.debug("ESC_BRACKET_8") + # END key. + if byte == ord("~"): + self.logger.debug("End key pressed.") + self.MoveCursor("right", len(self.input_buffer) - self.input_buffer_pos) + self.esc_state = 0 # Reset the state. + self.logger.debug("ESC sequence complete.") + return + + else: + self.logger.error("Unexpected sequence. %c", byte) + self.esc_state = 0 + + else: + self.logger.error("Unexpected sequence. %c", byte) + self.esc_state = 0 + + def ProcessInput(self): + """Captures the input determines what actions to take.""" + # There's nothing to do if the input buffer is empty. + if len(self.input_buffer) == 0: + return + + # Don't store 2 consecutive identical commands in the history. + if self.history and self.history[-1] != self.input_buffer or not self.history: + self.history.append(self.input_buffer) + + # Split the command up by spaces. + line = self.input_buffer.split(b" ") + self.logger.debug("cmd: %s", self.input_buffer) + cmd = line[0].lower() + + # The 'history' command is a special case that we handle locally. + if cmd == "history": + self.PrintHistory() + return + + # Send the command to the interpreter. + self.logger.debug("Sending command to interpreter.") + self.cmd_pipe.send(self.input_buffer) + + def CheckForEnhancedECImage(self): + """Performs an interrogation of the EC image. + + Send a SYN and expect an ACK. If no ACK or the response is incorrect, then + assume that the current EC image that we are talking to is not enhanced. + + Returns: + is_enhanced: A boolean indicating whether the EC responded to the + interrogation correctly. + + Raises: + EOFError: Allowed to propagate through from self.dbg_pipe.recv(). + """ + # Send interrogation byte and wait for the response. + self.logger.debug("Performing interrogation.") + self.cmd_pipe.send(interpreter.EC_SYN) + + response = "" + if self.dbg_pipe.poll(self.interrogation_timeout): + response = self.dbg_pipe.recv() + self.logger.debug("response: %r", binascii.hexlify(response)) + else: + self.logger.debug("Timed out waiting for EC_ACK") + + # Verify the acknowledgment. + is_enhanced = response == interpreter.EC_ACK + + if is_enhanced: + # Increase the interrogation timeout for stability purposes. + self.interrogation_timeout = ENHANCED_EC_INTERROGATION_TIMEOUT + self.logger.debug( + "Increasing interrogation timeout to %rs.", self.interrogation_timeout + ) + else: + # Reduce the timeout in order to reduce the perceivable delay. + self.interrogation_timeout = NON_ENHANCED_EC_INTERROGATION_TIMEOUT + self.logger.debug( + "Reducing interrogation timeout to %rs.", self.interrogation_timeout + ) + + return is_enhanced + + def HandleChar(self, byte): + """HandleChar does a certain action when it receives a character. + + Args: + byte: An integer representing the character received from the user. + + Raises: + EOFError: Allowed to propagate through from self.CheckForEnhancedECImage() + i.e. from self.dbg_pipe.recv(). + """ + fd = self.controller_pty + + # Enter the OOBM prompt mode if the user presses '%'. + if byte == ord("%"): + self.logger.debug("Begin OOBM command.") + self.receiving_oobm_cmd = True + # Print a "prompt". + os.write(self.controller_pty, b"\r\n% ") + return + + # Add chars to the pending OOBM command if we're currently receiving one. + if self.receiving_oobm_cmd and byte != ControlKey.CARRIAGE_RETURN: + tmp_bytes = six.int2byte(byte) + self.pending_oobm_cmd += tmp_bytes + self.logger.debug("%s", tmp_bytes) + os.write(self.controller_pty, tmp_bytes) + return + + if byte == ControlKey.CARRIAGE_RETURN: + if self.receiving_oobm_cmd: + # Terminate the command and place it in the OOBM queue. + self.logger.debug("End OOBM command.") + if self.pending_oobm_cmd: + self.oobm_queue.put(self.pending_oobm_cmd) + self.logger.debug( + "Placed %r into OOBM command queue.", self.pending_oobm_cmd + ) + + # Reset the state. + os.write(self.controller_pty, b"\r\n" + self.prompt) + self.input_buffer = b"" + self.input_buffer_pos = 0 + self.receiving_oobm_cmd = False + self.pending_oobm_cmd = b"" + return + + if self.interrogation_mode == b"never": + self.logger.debug( + "Skipping interrogation because interrogation mode" + " is set to never." + ) + elif self.interrogation_mode == b"always": + # Only interrogate the EC if the interrogation mode is set to 'always'. + self.enhanced_ec = self.CheckForEnhancedECImage() + self.logger.debug("Enhanced EC image? %r", self.enhanced_ec) + + if not self.enhanced_ec: + # Send everything straight to the EC to handle. + self.cmd_pipe.send(six.int2byte(byte)) + # Reset the input buffer. + self.input_buffer = b"" + self.input_buffer_pos = 0 + self.logger.log(1, "Reset input buffer.") + return + + # Keep handling the ESC sequence if we're in the middle of it. + if self.esc_state != 0: + self.HandleEsc(byte) + return + + # When we're at the end of the line, we should only allow going backwards, + # backspace, carriage return, up, or down. The arrow keys are escape + # sequences, so we let the escape...escape. + if self.input_buffer_pos >= self.line_limit and byte not in [ + ControlKey.CTRL_B, + ControlKey.ESC, + ControlKey.BACKSPACE, + ControlKey.CTRL_A, + ControlKey.CARRIAGE_RETURN, + ControlKey.CTRL_P, + ControlKey.CTRL_N, + ]: + return + + # If the input buffer is full we can't accept new chars. + buffer_full = len(self.input_buffer) >= self.line_limit + + # Carriage_Return/Enter + if byte == ControlKey.CARRIAGE_RETURN: + self.logger.debug("Enter key pressed.") + # Put a carriage return/newline and the print the prompt. + os.write(fd, b"\r\n") + + # TODO(aaboagye): When we control the printing of all output, print the + # prompt AFTER printing all the output. We can't do it yet because we + # don't know how much is coming from the EC. + + # Print the prompt. + os.write(fd, self.prompt) + # Process the input. + self.ProcessInput() + # Now, clear the buffer. + self.input_buffer = b"" + self.input_buffer_pos = 0 + # Reset history buffer pos. + self.history_pos = len(self.history) + # Clear partial command. + self.partial_cmd = b"" + + # Backspace + elif byte == ControlKey.BACKSPACE: + self.logger.debug("Backspace pressed.") + if self.input_buffer_pos > 0: + # Move left 1 column. + self.MoveCursor("left", 1) + # Remove the character at the input_buffer_pos by slicing it out. + self.SliceOutChar() + + self.logger.debug("input_buffer_pos: %d", self.input_buffer_pos) + + # Ctrl+A. Move cursor to beginning of the line + elif byte == ControlKey.CTRL_A: + self.logger.debug("Control+A pressed.") + self.MoveCursor("left", self.input_buffer_pos) + + # Ctrl+B. Move cursor left 1 column. + elif byte == ControlKey.CTRL_B: + self.logger.debug("Control+B pressed.") + self.MoveCursor("left", 1) + + # Ctrl+D. Delete a character. + elif byte == ControlKey.CTRL_D: + self.logger.debug("Control+D pressed.") + if self.input_buffer_pos != len(self.input_buffer): + # Remove the character by slicing it out. + self.SliceOutChar() + + # Ctrl+E. Move cursor to end of the line. + elif byte == ControlKey.CTRL_E: + self.logger.debug("Control+E pressed.") + self.MoveCursor("right", len(self.input_buffer) - self.input_buffer_pos) + + # Ctrl+F. Move cursor right 1 column. + elif byte == ControlKey.CTRL_F: + self.logger.debug("Control+F pressed.") + self.MoveCursor("right", 1) + + # Ctrl+K. Kill line. + elif byte == ControlKey.CTRL_K: + self.logger.debug("Control+K pressed.") + self.KillLine() + + # Ctrl+N. Next line. + elif byte == ControlKey.CTRL_N: + self.logger.debug("Control+N pressed.") + self.ShowNextCommand() + + # Ctrl+P. Previous line. + elif byte == ControlKey.CTRL_P: + self.logger.debug("Control+P pressed.") + self.ShowPreviousCommand() + + # ESC sequence + elif byte == ControlKey.ESC: + # Starting an ESC sequence + self.esc_state = EscState.ESC_START + + # Only print printable chars. + elif IsPrintable(byte): + # Drop the character if we're full. + if buffer_full: + self.logger.debug("Dropped char: %c(%d)", byte, byte) + return + # Print the character. + os.write(fd, six.int2byte(byte)) + # Print the rest of the line (if any). + extra_bytes_written = os.write( + fd, self.input_buffer[self.input_buffer_pos :] + ) + + # Recreate the input buffer. + self.input_buffer = ( + self.input_buffer[0 : self.input_buffer_pos] + + six.int2byte(byte) + + self.input_buffer[self.input_buffer_pos :] + ) + # Update the input buffer position. + self.input_buffer_pos += 1 + extra_bytes_written + + # Reset the cursor if we wrote any extra bytes. + if extra_bytes_written: + self.MoveCursor("left", extra_bytes_written) + + self.logger.debug("input_buffer_pos: %d", self.input_buffer_pos) + + def MoveCursor(self, direction, count): + """MoveCursor moves the cursor left or right by count columns. + + Args: + direction: A string that should be either 'left' or 'right' representing + the direction to move the cursor on the console. + count: An integer representing how many columns the cursor should be + moved. + + Raises: + AssertionError: If the direction is not equal to 'left' or 'right'. + """ + # If there's nothing to move, we're done. + if not count: + return + fd = self.controller_pty + seq = b"\033[" + str(count).encode("ascii") + if direction == "left": + # Bind the movement. + if count > self.input_buffer_pos: + count = self.input_buffer_pos + seq += b"D" + self.logger.debug("move cursor left %d", count) + self.input_buffer_pos -= count + + elif direction == "right": + # Bind the movement. + if (count + self.input_buffer_pos) > len(self.input_buffer): + count = 0 + seq += b"C" + self.logger.debug("move cursor right %d", count) + self.input_buffer_pos += count + + else: + raise AssertionError( + ("The only valid directions are 'left' and " "'right'") + ) + + self.logger.debug("input_buffer_pos: %d", self.input_buffer_pos) + # Move the cursor. + if count != 0: + os.write(fd, seq) + + def KillLine(self): + """Kill the rest of the line based on the input buffer position.""" + # Killing the line is killing all the text to the right. + diff = len(self.input_buffer) - self.input_buffer_pos + self.logger.debug("diff: %d", diff) + # Diff shouldn't be negative, but if it is for some reason, let's try to + # correct the cursor. + if diff < 0: + self.logger.warning( + "Resetting input buffer position to %d...", len(self.input_buffer) + ) + self.MoveCursor("left", -diff) + return + if diff: + self.MoveCursor("right", diff) + for _ in range(diff): + self.SendBackspace() + self.input_buffer_pos -= diff + self.input_buffer = self.input_buffer[0 : self.input_buffer_pos] + + def SendBackspace(self): + """Backspace a character on the console.""" + os.write(self.controller_pty, b"\033[1D \033[1D") + + def ProcessOOBMQueue(self): + """Retrieve an item from the OOBM queue and process it.""" + item = self.oobm_queue.get() + self.logger.debug("OOBM cmd: %r", item) + cmd = item.split(b" ") + + if cmd[0] == b"loglevel": + # An integer is required in order to set the log level. + if len(cmd) < 2: + self.logger.debug("Insufficient args") + self.PrintOOBMHelp() + return + try: + self.logger.debug("Log level change request.") + new_log_level = int(cmd[1]) + self.logger.logger.setLevel(new_log_level) + self.logger.info("Log level changed to %d.", new_log_level) + + # Forward the request to the interpreter as well. + self.cmd_pipe.send(item) + except ValueError: + # Ignoring the request if an integer was not provided. + self.PrintOOBMHelp() + + elif cmd[0] == b"timestamp": + mode = cmd[1].lower() + self.timestamp_enabled = mode == b"on" + self.logger.info( + "%sabling uart timestamps.", "En" if self.timestamp_enabled else "Dis" + ) + + elif cmd[0] == b"rawdebug": + mode = cmd[1].lower() + self.raw_debug = mode == b"on" + self.logger.info( + "%sabling per interrupt debug logs.", "En" if self.raw_debug else "Dis" + ) + + elif cmd[0] == b"interrogate" and len(cmd) >= 2: + enhanced = False + mode = cmd[1] + if len(cmd) >= 3 and cmd[2] == b"enhanced": + enhanced = True + + # Set the mode if correct. + if mode in INTERROGATION_MODES: + self.interrogation_mode = mode + self.logger.debug("Updated interrogation mode to %s.", mode) + + # Update the assumptions of the EC image. + self.enhanced_ec = enhanced + self.logger.debug("Enhanced EC image is now %r", self.enhanced_ec) + + # Send command to interpreter as well. + self.cmd_pipe.send(b"enhanced " + str(self.enhanced_ec).encode("ascii")) + else: + self.PrintOOBMHelp() + + else: + self.PrintOOBMHelp() + + def PrintOOBMHelp(self): + """Prints out the OOBM help.""" + # Print help syntax. + os.write(self.controller_pty, b"\r\n" + b"Known OOBM commands:\r\n") + os.write( + self.controller_pty, + b" interrogate <never | always | auto> " b"[enhanced]\r\n", + ) + os.write(self.controller_pty, b" loglevel <int>\r\n") + + def CheckBufferForEnhancedImage(self, data): + """Adds data to a look buffer and checks to see for enhanced EC image. + + The EC's console task prints a string upon initialization which says that + "Console is enabled; type HELP for help.". The enhanced EC images print a + different string as a part of their init. This function searches through a + "look" buffer, scanning for the presence of either of those strings and + updating the enhanced_ec state accordingly. + + Args: + data: A string containing the data sent from the interpreter. + """ + self.look_buffer += data + + # Search the buffer for any of the EC image strings. + enhanced_match = re.search(ENHANCED_IMAGE_RE, self.look_buffer) + non_enhanced_match = re.search(NON_ENHANCED_IMAGE_RE, self.look_buffer) + + # Update the state if any matches were found. + if enhanced_match or non_enhanced_match: + if enhanced_match: + self.enhanced_ec = True + elif non_enhanced_match: + self.enhanced_ec = False + + # Inform the interpreter of the result. + self.cmd_pipe.send(b"enhanced " + str(self.enhanced_ec).encode("ascii")) + self.logger.debug("Enhanced EC image? %r", self.enhanced_ec) + + # Clear look buffer since a match was found. + self.look_buffer = b"" + + # Move the sliding window. + self.look_buffer = self.look_buffer[-LOOK_BUFFER_SIZE:] - else: - self.logger.error('Unexpected sequence. %c', byte) - self.esc_state = 0 - - def ProcessInput(self): - """Captures the input determines what actions to take.""" - # There's nothing to do if the input buffer is empty. - if len(self.input_buffer) == 0: - return - - # Don't store 2 consecutive identical commands in the history. - if (self.history and self.history[-1] != self.input_buffer - or not self.history): - self.history.append(self.input_buffer) - - # Split the command up by spaces. - line = self.input_buffer.split(b' ') - self.logger.debug('cmd: %s', self.input_buffer) - cmd = line[0].lower() - - # The 'history' command is a special case that we handle locally. - if cmd == 'history': - self.PrintHistory() - return - - # Send the command to the interpreter. - self.logger.debug('Sending command to interpreter.') - self.cmd_pipe.send(self.input_buffer) - def CheckForEnhancedECImage(self): - """Performs an interrogation of the EC image. +def CanonicalizeTimeString(timestr): + """Canonicalize the timestamp string. - Send a SYN and expect an ACK. If no ACK or the response is incorrect, then - assume that the current EC image that we are talking to is not enhanced. + Args: + timestr: A timestamp string ended with 6 digits msec. Returns: - is_enhanced: A boolean indicating whether the EC responded to the - interrogation correctly. - - Raises: - EOFError: Allowed to propagate through from self.dbg_pipe.recv(). + A string with 3 digits msec and an extra space. """ - # Send interrogation byte and wait for the response. - self.logger.debug('Performing interrogation.') - self.cmd_pipe.send(interpreter.EC_SYN) - - response = '' - if self.dbg_pipe.poll(self.interrogation_timeout): - response = self.dbg_pipe.recv() - self.logger.debug('response: %r', binascii.hexlify(response)) - else: - self.logger.debug('Timed out waiting for EC_ACK') + return timestr[:-3].encode("ascii") + b" " - # Verify the acknowledgment. - is_enhanced = response == interpreter.EC_ACK - - if is_enhanced: - # Increase the interrogation timeout for stability purposes. - self.interrogation_timeout = ENHANCED_EC_INTERROGATION_TIMEOUT - self.logger.debug('Increasing interrogation timeout to %rs.', - self.interrogation_timeout) - else: - # Reduce the timeout in order to reduce the perceivable delay. - self.interrogation_timeout = NON_ENHANCED_EC_INTERROGATION_TIMEOUT - self.logger.debug('Reducing interrogation timeout to %rs.', - self.interrogation_timeout) - - return is_enhanced - - def HandleChar(self, byte): - """HandleChar does a certain action when it receives a character. - - Args: - byte: An integer representing the character received from the user. - Raises: - EOFError: Allowed to propagate through from self.CheckForEnhancedECImage() - i.e. from self.dbg_pipe.recv(). - """ - fd = self.controller_pty - - # Enter the OOBM prompt mode if the user presses '%'. - if byte == ord('%'): - self.logger.debug('Begin OOBM command.') - self.receiving_oobm_cmd = True - # Print a "prompt". - os.write(self.controller_pty, b'\r\n% ') - return - - # Add chars to the pending OOBM command if we're currently receiving one. - if self.receiving_oobm_cmd and byte != ControlKey.CARRIAGE_RETURN: - tmp_bytes = six.int2byte(byte) - self.pending_oobm_cmd += tmp_bytes - self.logger.debug('%s', tmp_bytes) - os.write(self.controller_pty, tmp_bytes) - return - - if byte == ControlKey.CARRIAGE_RETURN: - if self.receiving_oobm_cmd: - # Terminate the command and place it in the OOBM queue. - self.logger.debug('End OOBM command.') - if self.pending_oobm_cmd: - self.oobm_queue.put(self.pending_oobm_cmd) - self.logger.debug('Placed %r into OOBM command queue.', - self.pending_oobm_cmd) - - # Reset the state. - os.write(self.controller_pty, b'\r\n' + self.prompt) - self.input_buffer = b'' - self.input_buffer_pos = 0 - self.receiving_oobm_cmd = False - self.pending_oobm_cmd = b'' - return - - if self.interrogation_mode == b'never': - self.logger.debug('Skipping interrogation because interrogation mode' - ' is set to never.') - elif self.interrogation_mode == b'always': - # Only interrogate the EC if the interrogation mode is set to 'always'. - self.enhanced_ec = self.CheckForEnhancedECImage() - self.logger.debug('Enhanced EC image? %r', self.enhanced_ec) - - if not self.enhanced_ec: - # Send everything straight to the EC to handle. - self.cmd_pipe.send(six.int2byte(byte)) - # Reset the input buffer. - self.input_buffer = b'' - self.input_buffer_pos = 0 - self.logger.log(1, 'Reset input buffer.') - return - - # Keep handling the ESC sequence if we're in the middle of it. - if self.esc_state != 0: - self.HandleEsc(byte) - return - - # When we're at the end of the line, we should only allow going backwards, - # backspace, carriage return, up, or down. The arrow keys are escape - # sequences, so we let the escape...escape. - if (self.input_buffer_pos >= self.line_limit and - byte not in [ControlKey.CTRL_B, ControlKey.ESC, ControlKey.BACKSPACE, - ControlKey.CTRL_A, ControlKey.CARRIAGE_RETURN, - ControlKey.CTRL_P, ControlKey.CTRL_N]): - return - - # If the input buffer is full we can't accept new chars. - buffer_full = len(self.input_buffer) >= self.line_limit - - - # Carriage_Return/Enter - if byte == ControlKey.CARRIAGE_RETURN: - self.logger.debug('Enter key pressed.') - # Put a carriage return/newline and the print the prompt. - os.write(fd, b'\r\n') - - # TODO(aaboagye): When we control the printing of all output, print the - # prompt AFTER printing all the output. We can't do it yet because we - # don't know how much is coming from the EC. - - # Print the prompt. - os.write(fd, self.prompt) - # Process the input. - self.ProcessInput() - # Now, clear the buffer. - self.input_buffer = b'' - self.input_buffer_pos = 0 - # Reset history buffer pos. - self.history_pos = len(self.history) - # Clear partial command. - self.partial_cmd = b'' - - # Backspace - elif byte == ControlKey.BACKSPACE: - self.logger.debug('Backspace pressed.') - if self.input_buffer_pos > 0: - # Move left 1 column. - self.MoveCursor('left', 1) - # Remove the character at the input_buffer_pos by slicing it out. - self.SliceOutChar() - - self.logger.debug('input_buffer_pos: %d', self.input_buffer_pos) - - # Ctrl+A. Move cursor to beginning of the line - elif byte == ControlKey.CTRL_A: - self.logger.debug('Control+A pressed.') - self.MoveCursor('left', self.input_buffer_pos) - - # Ctrl+B. Move cursor left 1 column. - elif byte == ControlKey.CTRL_B: - self.logger.debug('Control+B pressed.') - self.MoveCursor('left', 1) - - # Ctrl+D. Delete a character. - elif byte == ControlKey.CTRL_D: - self.logger.debug('Control+D pressed.') - if self.input_buffer_pos != len(self.input_buffer): - # Remove the character by slicing it out. - self.SliceOutChar() - - # Ctrl+E. Move cursor to end of the line. - elif byte == ControlKey.CTRL_E: - self.logger.debug('Control+E pressed.') - self.MoveCursor('right', - len(self.input_buffer) - self.input_buffer_pos) - - # Ctrl+F. Move cursor right 1 column. - elif byte == ControlKey.CTRL_F: - self.logger.debug('Control+F pressed.') - self.MoveCursor('right', 1) - - # Ctrl+K. Kill line. - elif byte == ControlKey.CTRL_K: - self.logger.debug('Control+K pressed.') - self.KillLine() - - # Ctrl+N. Next line. - elif byte == ControlKey.CTRL_N: - self.logger.debug('Control+N pressed.') - self.ShowNextCommand() - - # Ctrl+P. Previous line. - elif byte == ControlKey.CTRL_P: - self.logger.debug('Control+P pressed.') - self.ShowPreviousCommand() - - # ESC sequence - elif byte == ControlKey.ESC: - # Starting an ESC sequence - self.esc_state = EscState.ESC_START - - # Only print printable chars. - elif IsPrintable(byte): - # Drop the character if we're full. - if buffer_full: - self.logger.debug('Dropped char: %c(%d)', byte, byte) - return - # Print the character. - os.write(fd, six.int2byte(byte)) - # Print the rest of the line (if any). - extra_bytes_written = os.write(fd, - self.input_buffer[self.input_buffer_pos:]) - - # Recreate the input buffer. - self.input_buffer = (self.input_buffer[0:self.input_buffer_pos] + - six.int2byte(byte) + - self.input_buffer[self.input_buffer_pos:]) - # Update the input buffer position. - self.input_buffer_pos += 1 + extra_bytes_written - - # Reset the cursor if we wrote any extra bytes. - if extra_bytes_written: - self.MoveCursor('left', extra_bytes_written) - - self.logger.debug('input_buffer_pos: %d', self.input_buffer_pos) - - def MoveCursor(self, direction, count): - """MoveCursor moves the cursor left or right by count columns. +def IsPrintable(byte): + """Determines if a byte is printable. Args: - direction: A string that should be either 'left' or 'right' representing - the direction to move the cursor on the console. - count: An integer representing how many columns the cursor should be - moved. + byte: An integer potentially representing a printable character. - Raises: - AssertionError: If the direction is not equal to 'left' or 'right'. + Returns: + A boolean indicating whether the byte is a printable character. """ - # If there's nothing to move, we're done. - if not count: - return - fd = self.controller_pty - seq = b'\033[' + str(count).encode('ascii') - if direction == 'left': - # Bind the movement. - if count > self.input_buffer_pos: - count = self.input_buffer_pos - seq += b'D' - self.logger.debug('move cursor left %d', count) - self.input_buffer_pos -= count - - elif direction == 'right': - # Bind the movement. - if (count + self.input_buffer_pos) > len(self.input_buffer): - count = 0 - seq += b'C' - self.logger.debug('move cursor right %d', count) - self.input_buffer_pos += count - - else: - raise AssertionError(('The only valid directions are \'left\' and ' - '\'right\'')) - - self.logger.debug('input_buffer_pos: %d', self.input_buffer_pos) - # Move the cursor. - if count != 0: - os.write(fd, seq) - - def KillLine(self): - """Kill the rest of the line based on the input buffer position.""" - # Killing the line is killing all the text to the right. - diff = len(self.input_buffer) - self.input_buffer_pos - self.logger.debug('diff: %d', diff) - # Diff shouldn't be negative, but if it is for some reason, let's try to - # correct the cursor. - if diff < 0: - self.logger.warning('Resetting input buffer position to %d...', - len(self.input_buffer)) - self.MoveCursor('left', -diff) - return - if diff: - self.MoveCursor('right', diff) - for _ in range(diff): - self.SendBackspace() - self.input_buffer_pos -= diff - self.input_buffer = self.input_buffer[0:self.input_buffer_pos] - - def SendBackspace(self): - """Backspace a character on the console.""" - os.write(self.controller_pty, b'\033[1D \033[1D') - - def ProcessOOBMQueue(self): - """Retrieve an item from the OOBM queue and process it.""" - item = self.oobm_queue.get() - self.logger.debug('OOBM cmd: %r', item) - cmd = item.split(b' ') - - if cmd[0] == b'loglevel': - # An integer is required in order to set the log level. - if len(cmd) < 2: - self.logger.debug('Insufficient args') - self.PrintOOBMHelp() - return - try: - self.logger.debug('Log level change request.') - new_log_level = int(cmd[1]) - self.logger.logger.setLevel(new_log_level) - self.logger.info('Log level changed to %d.', new_log_level) - - # Forward the request to the interpreter as well. - self.cmd_pipe.send(item) - except ValueError: - # Ignoring the request if an integer was not provided. - self.PrintOOBMHelp() - - elif cmd[0] == b'timestamp': - mode = cmd[1].lower() - self.timestamp_enabled = (mode == b'on') - self.logger.info('%sabling uart timestamps.', - 'En' if self.timestamp_enabled else 'Dis') - - elif cmd[0] == b'rawdebug': - mode = cmd[1].lower() - self.raw_debug = (mode == b'on') - self.logger.info('%sabling per interrupt debug logs.', - 'En' if self.raw_debug else 'Dis') - - elif cmd[0] == b'interrogate' and len(cmd) >= 2: - enhanced = False - mode = cmd[1] - if len(cmd) >= 3 and cmd[2] == b'enhanced': - enhanced = True - - # Set the mode if correct. - if mode in INTERROGATION_MODES: - self.interrogation_mode = mode - self.logger.debug('Updated interrogation mode to %s.', mode) - - # Update the assumptions of the EC image. - self.enhanced_ec = enhanced - self.logger.debug('Enhanced EC image is now %r', self.enhanced_ec) - - # Send command to interpreter as well. - self.cmd_pipe.send(b'enhanced ' + str(self.enhanced_ec).encode('ascii')) - else: - self.PrintOOBMHelp() - - else: - self.PrintOOBMHelp() + return byte >= ord(" ") and byte <= ord("~") - def PrintOOBMHelp(self): - """Prints out the OOBM help.""" - # Print help syntax. - os.write(self.controller_pty, b'\r\n' + b'Known OOBM commands:\r\n') - os.write(self.controller_pty, b' interrogate <never | always | auto> ' - b'[enhanced]\r\n') - os.write(self.controller_pty, b' loglevel <int>\r\n') - def CheckBufferForEnhancedImage(self, data): - """Adds data to a look buffer and checks to see for enhanced EC image. - - The EC's console task prints a string upon initialization which says that - "Console is enabled; type HELP for help.". The enhanced EC images print a - different string as a part of their init. This function searches through a - "look" buffer, scanning for the presence of either of those strings and - updating the enhanced_ec state accordingly. +def StartLoop(console, command_active, shutdown_pipe=None): + """Starts the infinite loop of console processing. Args: - data: A string containing the data sent from the interpreter. + console: A Console object that has been properly initialzed. + command_active: ctypes data object or multiprocessing.Value indicating if + servod owns the console, or user owns the console. This prevents input + collisions. + shutdown_pipe: A file object for a pipe or equivalent that becomes readable + (not blocked) to indicate that the loop should exit. Can be None to never + exit the loop. """ - self.look_buffer += data - - # Search the buffer for any of the EC image strings. - enhanced_match = re.search(ENHANCED_IMAGE_RE, self.look_buffer) - non_enhanced_match = re.search(NON_ENHANCED_IMAGE_RE, self.look_buffer) - - # Update the state if any matches were found. - if enhanced_match or non_enhanced_match: - if enhanced_match: - self.enhanced_ec = True - elif non_enhanced_match: - self.enhanced_ec = False - - # Inform the interpreter of the result. - self.cmd_pipe.send(b'enhanced ' + str(self.enhanced_ec).encode('ascii')) - self.logger.debug('Enhanced EC image? %r', self.enhanced_ec) - - # Clear look buffer since a match was found. - self.look_buffer = b'' - - # Move the sliding window. - self.look_buffer = self.look_buffer[-LOOK_BUFFER_SIZE:] - - -def CanonicalizeTimeString(timestr): - """Canonicalize the timestamp string. - - Args: - timestr: A timestamp string ended with 6 digits msec. - - Returns: - A string with 3 digits msec and an extra space. - """ - return timestr[:-3].encode('ascii') + b' ' - - -def IsPrintable(byte): - """Determines if a byte is printable. - - Args: - byte: An integer potentially representing a printable character. - - Returns: - A boolean indicating whether the byte is a printable character. - """ - return byte >= ord(' ') and byte <= ord('~') - - -def StartLoop(console, command_active, shutdown_pipe=None): - """Starts the infinite loop of console processing. - - Args: - console: A Console object that has been properly initialzed. - command_active: ctypes data object or multiprocessing.Value indicating if - servod owns the console, or user owns the console. This prevents input - collisions. - shutdown_pipe: A file object for a pipe or equivalent that becomes readable - (not blocked) to indicate that the loop should exit. Can be None to never - exit the loop. - """ - try: - console.logger.debug('Console is being served on %s.', console.user_pty) - console.logger.debug('Console controller is on %s.', console.controller_pty) - console.logger.debug('Command interface is being served on %s.', - console.interface_pty) - console.logger.debug(console) - - # This checks for HUP to indicate if the user has connected to the pty. - ep = select.epoll() - ep.register(console.controller_pty, select.EPOLLHUP) - - # This is used instead of "break" to avoid exiting the loop in the middle of - # an iteration. - continue_looping = True - - # Used for determining when to print host timestamps - tm_req = True - - while continue_looping: - # Check to see if pts is connected to anything - events = ep.poll(0) - controller_connected = not events - - # Check to see if pipes or the console are ready for reading. - read_list = [console.interface_pty, - console.cmd_pipe, console.dbg_pipe] - if controller_connected: - read_list.append(console.controller_pty) - if shutdown_pipe is not None: - read_list.append(shutdown_pipe) - - # Check if any input is ready, or wait for .1 sec and re-poll if - # a user has connected to the pts. - select_output = select.select(read_list, [], [], .1) - if not select_output: - continue - ready_for_reading = select_output[0] - - for obj in ready_for_reading: - if obj is console.controller_pty: - if not command_active.value: - # Convert to bytes so we can look for non-printable chars such as - # Ctrl+A, Ctrl+E, etc. - try: - line = bytearray(os.read(console.controller_pty, CONSOLE_MAX_READ)) - console.logger.debug('Input from user: %s, locked:%s', - str(line).strip(), command_active.value) - for i in line: - try: - # Handle each character as it arrives. - console.HandleChar(i) - except EOFError: - console.logger.debug( - 'ec3po console received EOF from dbg_pipe in HandleChar()' - ' while reading console.controller_pty') - continue_looping = False - break - except OSError: - console.logger.debug('Ptm read failed, probably user disconnect.') - - elif obj is console.interface_pty: - if command_active.value: - # Convert to bytes so we can look for non-printable chars such as - # Ctrl+A, Ctrl+E, etc. - line = bytearray(os.read(console.interface_pty, CONSOLE_MAX_READ)) - console.logger.debug('Input from interface: %s, locked:%s', - str(line).strip(), command_active.value) - for i in line: - try: - # Handle each character as it arrives. - console.HandleChar(i) - except EOFError: - console.logger.debug( - 'ec3po console received EOF from dbg_pipe in HandleChar()' - ' while reading console.interface_pty') - continue_looping = False - break - - elif obj is console.cmd_pipe: - try: - data = console.cmd_pipe.recv() - except EOFError: - console.logger.debug('ec3po console received EOF from cmd_pipe') - continue_looping = False - else: - # Write it to the user console. - if console.raw_debug: - console.logger.debug('|CMD|-%s->%r', - ('u' if controller_connected else '') + - ('i' if command_active.value else ''), - data.strip()) + try: + console.logger.debug("Console is being served on %s.", console.user_pty) + console.logger.debug("Console controller is on %s.", console.controller_pty) + console.logger.debug( + "Command interface is being served on %s.", console.interface_pty + ) + console.logger.debug(console) + + # This checks for HUP to indicate if the user has connected to the pty. + ep = select.epoll() + ep.register(console.controller_pty, select.EPOLLHUP) + + # This is used instead of "break" to avoid exiting the loop in the middle of + # an iteration. + continue_looping = True + + # Used for determining when to print host timestamps + tm_req = True + + while continue_looping: + # Check to see if pts is connected to anything + events = ep.poll(0) + controller_connected = not events + + # Check to see if pipes or the console are ready for reading. + read_list = [console.interface_pty, console.cmd_pipe, console.dbg_pipe] if controller_connected: - os.write(console.controller_pty, data) - if command_active.value: - os.write(console.interface_pty, data) - - elif obj is console.dbg_pipe: - try: - data = console.dbg_pipe.recv() - except EOFError: - console.logger.debug('ec3po console received EOF from dbg_pipe') - continue_looping = False - else: - if console.interrogation_mode == b'auto': - # Search look buffer for enhanced EC image string. - console.CheckBufferForEnhancedImage(data) - # Write it to the user console. - if len(data) > 1 and console.raw_debug: - console.logger.debug('|DBG|-%s->%r', - ('u' if controller_connected else '') + - ('i' if command_active.value else ''), - data.strip()) - console.LogConsoleOutput(data) - if controller_connected: - end = len(data) - 1 - if console.timestamp_enabled: - # A timestamp is required at the beginning of this line - if tm_req is True: - now = datetime.now() - tm = CanonicalizeTimeString(now.strftime(HOST_STRFTIME)) - os.write(console.controller_pty, tm) - tm_req = False - - # Insert timestamps into the middle where appropriate - # except if the last character is a newline - nls_found = data.count(b'\n', 0, end) - now = datetime.now() - tm = CanonicalizeTimeString(now.strftime('\n' + HOST_STRFTIME)) - data_tm = data.replace(b'\n', tm, nls_found) - else: - data_tm = data - - # timestamp required on next input - if data[end] == b'\n'[0]: - tm_req = True - os.write(console.controller_pty, data_tm) - if command_active.value: - os.write(console.interface_pty, data) - - elif obj is shutdown_pipe: - console.logger.debug( - 'ec3po console received shutdown pipe unblocked notification') - continue_looping = False - - while not console.oobm_queue.empty(): - console.logger.debug('OOBM queue ready for reading.') - console.ProcessOOBMQueue() - - except KeyboardInterrupt: - pass - - finally: - ep.unregister(console.controller_pty) - console.dbg_pipe.close() - console.cmd_pipe.close() - os.close(console.controller_pty) - os.close(console.interface_pty) - if shutdown_pipe is not None: - shutdown_pipe.close() - console.logger.debug('Exit ec3po console loop for %s', console.user_pty) + read_list.append(console.controller_pty) + if shutdown_pipe is not None: + read_list.append(shutdown_pipe) + + # Check if any input is ready, or wait for .1 sec and re-poll if + # a user has connected to the pts. + select_output = select.select(read_list, [], [], 0.1) + if not select_output: + continue + ready_for_reading = select_output[0] + + for obj in ready_for_reading: + if obj is console.controller_pty: + if not command_active.value: + # Convert to bytes so we can look for non-printable chars such as + # Ctrl+A, Ctrl+E, etc. + try: + line = bytearray( + os.read(console.controller_pty, CONSOLE_MAX_READ) + ) + console.logger.debug( + "Input from user: %s, locked:%s", + str(line).strip(), + command_active.value, + ) + for i in line: + try: + # Handle each character as it arrives. + console.HandleChar(i) + except EOFError: + console.logger.debug( + "ec3po console received EOF from dbg_pipe in HandleChar()" + " while reading console.controller_pty" + ) + continue_looping = False + break + except OSError: + console.logger.debug( + "Ptm read failed, probably user disconnect." + ) + + elif obj is console.interface_pty: + if command_active.value: + # Convert to bytes so we can look for non-printable chars such as + # Ctrl+A, Ctrl+E, etc. + line = bytearray( + os.read(console.interface_pty, CONSOLE_MAX_READ) + ) + console.logger.debug( + "Input from interface: %s, locked:%s", + str(line).strip(), + command_active.value, + ) + for i in line: + try: + # Handle each character as it arrives. + console.HandleChar(i) + except EOFError: + console.logger.debug( + "ec3po console received EOF from dbg_pipe in HandleChar()" + " while reading console.interface_pty" + ) + continue_looping = False + break + + elif obj is console.cmd_pipe: + try: + data = console.cmd_pipe.recv() + except EOFError: + console.logger.debug("ec3po console received EOF from cmd_pipe") + continue_looping = False + else: + # Write it to the user console. + if console.raw_debug: + console.logger.debug( + "|CMD|-%s->%r", + ("u" if controller_connected else "") + + ("i" if command_active.value else ""), + data.strip(), + ) + if controller_connected: + os.write(console.controller_pty, data) + if command_active.value: + os.write(console.interface_pty, data) + + elif obj is console.dbg_pipe: + try: + data = console.dbg_pipe.recv() + except EOFError: + console.logger.debug("ec3po console received EOF from dbg_pipe") + continue_looping = False + else: + if console.interrogation_mode == b"auto": + # Search look buffer for enhanced EC image string. + console.CheckBufferForEnhancedImage(data) + # Write it to the user console. + if len(data) > 1 and console.raw_debug: + console.logger.debug( + "|DBG|-%s->%r", + ("u" if controller_connected else "") + + ("i" if command_active.value else ""), + data.strip(), + ) + console.LogConsoleOutput(data) + if controller_connected: + end = len(data) - 1 + if console.timestamp_enabled: + # A timestamp is required at the beginning of this line + if tm_req is True: + now = datetime.now() + tm = CanonicalizeTimeString( + now.strftime(HOST_STRFTIME) + ) + os.write(console.controller_pty, tm) + tm_req = False + + # Insert timestamps into the middle where appropriate + # except if the last character is a newline + nls_found = data.count(b"\n", 0, end) + now = datetime.now() + tm = CanonicalizeTimeString( + now.strftime("\n" + HOST_STRFTIME) + ) + data_tm = data.replace(b"\n", tm, nls_found) + else: + data_tm = data + + # timestamp required on next input + if data[end] == b"\n"[0]: + tm_req = True + os.write(console.controller_pty, data_tm) + if command_active.value: + os.write(console.interface_pty, data) + + elif obj is shutdown_pipe: + console.logger.debug( + "ec3po console received shutdown pipe unblocked notification" + ) + continue_looping = False + + while not console.oobm_queue.empty(): + console.logger.debug("OOBM queue ready for reading.") + console.ProcessOOBMQueue() + + except KeyboardInterrupt: + pass + + finally: + ep.unregister(console.controller_pty) + console.dbg_pipe.close() + console.cmd_pipe.close() + os.close(console.controller_pty) + os.close(console.interface_pty) + if shutdown_pipe is not None: + shutdown_pipe.close() + console.logger.debug("Exit ec3po console loop for %s", console.user_pty) def main(argv): - """Kicks off the EC-3PO interactive console interface and interpreter. - - We create some pipes to communicate with an interpreter, instantiate an - interpreter, create a PTY pair, and begin serving the console interface. - - Args: - argv: A list of strings containing the arguments this module was called - with. - """ - # Set up argument parser. - parser = argparse.ArgumentParser(description=('Start interactive EC console ' - 'and interpreter.')) - parser.add_argument('ec_uart_pty', - help=('The full PTY name that the EC UART' - ' is present on. eg: /dev/pts/12')) - parser.add_argument('--log-level', - default='info', - help='info, debug, warning, error, or critical') - - # Parse arguments. - opts = parser.parse_args(argv) - - # Set logging level. - opts.log_level = opts.log_level.lower() - if opts.log_level == 'info': - log_level = logging.INFO - elif opts.log_level == 'debug': - log_level = logging.DEBUG - elif opts.log_level == 'warning': - log_level = logging.WARNING - elif opts.log_level == 'error': - log_level = logging.ERROR - elif opts.log_level == 'critical': - log_level = logging.CRITICAL - else: - parser.error('Invalid log level. (info, debug, warning, error, critical)') - - # Start logging with a timestamp, module, and log level shown in each log - # entry. - logging.basicConfig(level=log_level, format=('%(asctime)s - %(module)s -' - ' %(levelname)s - %(message)s')) - - # Create some pipes to communicate between the interpreter and the console. - # The command pipe is bidirectional. - cmd_pipe_interactive, cmd_pipe_interp = threadproc_shim.Pipe() - # The debug pipe is unidirectional from interpreter to console only. - dbg_pipe_interactive, dbg_pipe_interp = threadproc_shim.Pipe(duplex=False) - - # Create an interpreter instance. - itpr = interpreter.Interpreter(opts.ec_uart_pty, cmd_pipe_interp, - dbg_pipe_interp, log_level) - - # Spawn an interpreter process. - itpr_process = threadproc_shim.ThreadOrProcess( - target=interpreter.StartLoop, args=(itpr,)) - # Make sure to kill the interpreter when we terminate. - itpr_process.daemon = True - # Start the interpreter. - itpr_process.start() - - # Open a new pseudo-terminal pair - (controller_pty, user_pty) = pty.openpty() - # Set the permissions to 660. - os.chmod(os.ttyname(user_pty), (stat.S_IRGRP | stat.S_IWGRP | - stat.S_IRUSR | stat.S_IWUSR)) - # Create a console. - console = Console(controller_pty, os.ttyname(user_pty), cmd_pipe_interactive, - dbg_pipe_interactive) - # Start serving the console. - v = threadproc_shim.Value(ctypes.c_bool, False) - StartLoop(console, v) - - -if __name__ == '__main__': - main(sys.argv[1:]) + """Kicks off the EC-3PO interactive console interface and interpreter. + + We create some pipes to communicate with an interpreter, instantiate an + interpreter, create a PTY pair, and begin serving the console interface. + + Args: + argv: A list of strings containing the arguments this module was called + with. + """ + # Set up argument parser. + parser = argparse.ArgumentParser( + description=("Start interactive EC console " "and interpreter.") + ) + parser.add_argument( + "ec_uart_pty", + help=("The full PTY name that the EC UART" " is present on. eg: /dev/pts/12"), + ) + parser.add_argument( + "--log-level", default="info", help="info, debug, warning, error, or critical" + ) + + # Parse arguments. + opts = parser.parse_args(argv) + + # Set logging level. + opts.log_level = opts.log_level.lower() + if opts.log_level == "info": + log_level = logging.INFO + elif opts.log_level == "debug": + log_level = logging.DEBUG + elif opts.log_level == "warning": + log_level = logging.WARNING + elif opts.log_level == "error": + log_level = logging.ERROR + elif opts.log_level == "critical": + log_level = logging.CRITICAL + else: + parser.error("Invalid log level. (info, debug, warning, error, critical)") + + # Start logging with a timestamp, module, and log level shown in each log + # entry. + logging.basicConfig( + level=log_level, + format=("%(asctime)s - %(module)s -" " %(levelname)s - %(message)s"), + ) + + # Create some pipes to communicate between the interpreter and the console. + # The command pipe is bidirectional. + cmd_pipe_interactive, cmd_pipe_interp = threadproc_shim.Pipe() + # The debug pipe is unidirectional from interpreter to console only. + dbg_pipe_interactive, dbg_pipe_interp = threadproc_shim.Pipe(duplex=False) + + # Create an interpreter instance. + itpr = interpreter.Interpreter( + opts.ec_uart_pty, cmd_pipe_interp, dbg_pipe_interp, log_level + ) + + # Spawn an interpreter process. + itpr_process = threadproc_shim.ThreadOrProcess( + target=interpreter.StartLoop, args=(itpr,) + ) + # Make sure to kill the interpreter when we terminate. + itpr_process.daemon = True + # Start the interpreter. + itpr_process.start() + + # Open a new pseudo-terminal pair + (controller_pty, user_pty) = pty.openpty() + # Set the permissions to 660. + os.chmod( + os.ttyname(user_pty), + (stat.S_IRGRP | stat.S_IWGRP | stat.S_IRUSR | stat.S_IWUSR), + ) + # Create a console. + console = Console( + controller_pty, os.ttyname(user_pty), cmd_pipe_interactive, dbg_pipe_interactive + ) + # Start serving the console. + v = threadproc_shim.Value(ctypes.c_bool, False) + StartLoop(console, v) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/util/ec3po/console_unittest.py b/util/ec3po/console_unittest.py index 7e341e7e8d..41ae324ef4 100755 --- a/util/ec3po/console_unittest.py +++ b/util/ec3po/console_unittest.py @@ -11,1262 +11,1310 @@ from __future__ import print_function import binascii import logging -import mock import tempfile import unittest +import mock import six - -from ec3po import console -from ec3po import interpreter -from ec3po import threadproc_shim +from ec3po import console, interpreter, threadproc_shim ESC_STRING = six.int2byte(console.ControlKey.ESC) + class Keys(object): - """A class that contains the escape sequences for special keys.""" - LEFT_ARROW = [console.ControlKey.ESC, ord('['), ord('D')] - RIGHT_ARROW = [console.ControlKey.ESC, ord('['), ord('C')] - UP_ARROW = [console.ControlKey.ESC, ord('['), ord('A')] - DOWN_ARROW = [console.ControlKey.ESC, ord('['), ord('B')] - HOME = [console.ControlKey.ESC, ord('['), ord('1'), ord('~')] - END = [console.ControlKey.ESC, ord('['), ord('8'), ord('~')] - DEL = [console.ControlKey.ESC, ord('['), ord('3'), ord('~')] + """A class that contains the escape sequences for special keys.""" + + LEFT_ARROW = [console.ControlKey.ESC, ord("["), ord("D")] + RIGHT_ARROW = [console.ControlKey.ESC, ord("["), ord("C")] + UP_ARROW = [console.ControlKey.ESC, ord("["), ord("A")] + DOWN_ARROW = [console.ControlKey.ESC, ord("["), ord("B")] + HOME = [console.ControlKey.ESC, ord("["), ord("1"), ord("~")] + END = [console.ControlKey.ESC, ord("["), ord("8"), ord("~")] + DEL = [console.ControlKey.ESC, ord("["), ord("3"), ord("~")] + class OutputStream(object): - """A class that has methods which return common console output.""" + """A class that has methods which return common console output.""" - @staticmethod - def MoveCursorLeft(count): - """Produces what would be printed to the console if the cursor moved left. + @staticmethod + def MoveCursorLeft(count): + """Produces what would be printed to the console if the cursor moved left. - Args: - count: An integer representing how many columns to move left. + Args: + count: An integer representing how many columns to move left. - Returns: - string: A string which contains what would be printed to the console if - the cursor moved left. - """ - string = ESC_STRING - string += b'[' + str(count).encode('ascii') + b'D' - return string + Returns: + string: A string which contains what would be printed to the console if + the cursor moved left. + """ + string = ESC_STRING + string += b"[" + str(count).encode("ascii") + b"D" + return string - @staticmethod - def MoveCursorRight(count): - """Produces what would be printed to the console if the cursor moved right. + @staticmethod + def MoveCursorRight(count): + """Produces what would be printed to the console if the cursor moved right. - Args: - count: An integer representing how many columns to move right. + Args: + count: An integer representing how many columns to move right. - Returns: - string: A string which contains what would be printed to the console if - the cursor moved right. - """ - string = ESC_STRING - string += b'[' + str(count).encode('ascii') + b'C' - return string + Returns: + string: A string which contains what would be printed to the console if + the cursor moved right. + """ + string = ESC_STRING + string += b"[" + str(count).encode("ascii") + b"C" + return string -BACKSPACE_STRING = b'' + +BACKSPACE_STRING = b"" # Move cursor left 1 column. BACKSPACE_STRING += OutputStream.MoveCursorLeft(1) # Write a space. -BACKSPACE_STRING += b' ' +BACKSPACE_STRING += b" " # Move cursor left 1 column. BACKSPACE_STRING += OutputStream.MoveCursorLeft(1) + def BytesToByteList(string): - """Converts a bytes string to list of bytes. + """Converts a bytes string to list of bytes. + + Args: + string: A literal bytes to turn into a list of bytes. - Args: - string: A literal bytes to turn into a list of bytes. + Returns: + A list of integers representing the byte value of each character in the + string. + """ + if six.PY3: + return [c for c in string] + return [ord(c) for c in string] - Returns: - A list of integers representing the byte value of each character in the - string. - """ - if six.PY3: - return [c for c in string] - return [ord(c) for c in string] def CheckConsoleOutput(test_case, exp_console_out): - """Verify what was sent out the console matches what we expect. + """Verify what was sent out the console matches what we expect. - Args: - test_case: A unittest.TestCase object representing the current unit test. - exp_console_out: A string representing the console output stream. - """ - # Read what was sent out the console. - test_case.tempfile.seek(0) - console_out = test_case.tempfile.read() + Args: + test_case: A unittest.TestCase object representing the current unit test. + exp_console_out: A string representing the console output stream. + """ + # Read what was sent out the console. + test_case.tempfile.seek(0) + console_out = test_case.tempfile.read() - test_case.assertEqual(exp_console_out, console_out) + test_case.assertEqual(exp_console_out, console_out) -def CheckInputBuffer(test_case, exp_input_buffer): - """Verify that the input buffer contains what we expect. - - Args: - test_case: A unittest.TestCase object representing the current unit test. - exp_input_buffer: A string containing the contents of the current input - buffer. - """ - test_case.assertEqual(exp_input_buffer, test_case.console.input_buffer, - (b'input buffer does not match expected.\n' - b'expected: |' + exp_input_buffer + b'|\n' - b'got: |' + test_case.console.input_buffer + - b'|\n' + str(test_case.console).encode('ascii'))) -def CheckInputBufferPosition(test_case, exp_pos): - """Verify the input buffer position. +def CheckInputBuffer(test_case, exp_input_buffer): + """Verify that the input buffer contains what we expect. - Args: - test_case: A unittest.TestCase object representing the current unit test. - exp_pos: An integer representing the expected input buffer position. - """ - test_case.assertEqual(exp_pos, test_case.console.input_buffer_pos, - 'input buffer position is incorrect.\ngot: ' + - str(test_case.console.input_buffer_pos) + '\nexp: ' + - str(exp_pos) + '\n' + str(test_case.console)) + Args: + test_case: A unittest.TestCase object representing the current unit test. + exp_input_buffer: A string containing the contents of the current input + buffer. + """ + test_case.assertEqual( + exp_input_buffer, + test_case.console.input_buffer, + ( + b"input buffer does not match expected.\n" + b"expected: |" + exp_input_buffer + b"|\n" + b"got: |" + + test_case.console.input_buffer + + b"|\n" + + str(test_case.console).encode("ascii") + ), + ) -def CheckHistoryBuffer(test_case, exp_history): - """Verify that the items in the history buffer are what we expect. - - Args: - test_case: A unittest.TestCase object representing the current unit test. - exp_history: A list of strings representing the expected contents of the - history buffer. - """ - # First, check to see if the length is what we expect. - test_case.assertEqual(len(exp_history), len(test_case.console.history), - ('The number of items in the history is unexpected.\n' - 'exp: ' + str(len(exp_history)) + '\n' - 'got: ' + str(len(test_case.console.history)) + '\n' - 'internal state:\n' + str(test_case.console))) - - # Next, check the actual contents of the history buffer. - for i in range(len(exp_history)): - test_case.assertEqual(exp_history[i], test_case.console.history[i], - (b'history buffer contents are incorrect.\n' - b'exp: ' + exp_history[i] + b'\n' - b'got: ' + test_case.console.history[i] + b'\n' - b'internal state:\n' + - str(test_case.console).encode('ascii'))) +def CheckInputBufferPosition(test_case, exp_pos): + """Verify the input buffer position. -class TestConsoleEditingMethods(unittest.TestCase): - """Test case to verify all console editing methods.""" - - def setUp(self): - """Setup the test harness.""" - # Setup logging with a timestamp, the module, and the log level. - logging.basicConfig(level=logging.DEBUG, - format=('%(asctime)s - %(module)s -' - ' %(levelname)s - %(message)s')) - - # Create a temp file and set both the controller and peripheral PTYs to the - # file to create a loopback. - self.tempfile = tempfile.TemporaryFile() - - # Create some mock pipes. These won't be used since we'll mock out sends - # to the interpreter. - mock_pipe_end_0, mock_pipe_end_1 = threadproc_shim.Pipe() - self.console = console.Console(self.tempfile.fileno(), self.tempfile, - tempfile.TemporaryFile(), - mock_pipe_end_0, mock_pipe_end_1, "EC") - - # Console editing methods are only valid for enhanced EC images, therefore - # we have to assume that the "EC" we're talking to is enhanced. By default, - # the console believes that the EC it's communicating with is NOT enhanced - # which is why we have to override it here. - self.console.enhanced_ec = True - self.console.CheckForEnhancedECImage = mock.MagicMock(return_value=True) - - def test_EnteringChars(self): - """Verify that characters are echoed onto the console.""" - test_str = b'abc' - input_stream = BytesToByteList(test_str) - - # Send the characters in. - for byte in input_stream: - self.console.HandleChar(byte) - - # Check the input position. - exp_pos = len(test_str) - CheckInputBufferPosition(self, exp_pos) - - # Verify that the input buffer is correct. - expected_buffer = test_str - CheckInputBuffer(self, expected_buffer) - - # Check console output - exp_console_out = test_str - CheckConsoleOutput(self, exp_console_out) - - def test_EnteringDeletingMoreCharsThanEntered(self): - """Verify that we can press backspace more than we have entered chars.""" - test_str = b'spamspam' - input_stream = BytesToByteList(test_str) - - # Send the characters in. - for byte in input_stream: - self.console.HandleChar(byte) - - # Now backspace 1 more than what we sent. - input_stream = [] - for _ in range(len(test_str) + 1): - input_stream.append(console.ControlKey.BACKSPACE) - - # Send that sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # First, verify that input buffer position is 0. - CheckInputBufferPosition(self, 0) - - # Next, examine the output stream for the correct sequence. - exp_console_out = test_str - for _ in range(len(test_str)): - exp_console_out += BACKSPACE_STRING - - # Now, verify that we got what we expected. - CheckConsoleOutput(self, exp_console_out) - - def test_EnteringMoreThanCharLimit(self): - """Verify that we drop characters when the line is too long.""" - test_str = self.console.line_limit * b'o' # All allowed. - test_str += 5 * b'x' # All should be dropped. - input_stream = BytesToByteList(test_str) - - # Send the characters in. - for byte in input_stream: - self.console.HandleChar(byte) - - # First, we expect that input buffer position should be equal to the line - # limit. - exp_pos = self.console.line_limit - CheckInputBufferPosition(self, exp_pos) - - # The input buffer should only hold until the line limit. - exp_buffer = test_str[0:self.console.line_limit] - CheckInputBuffer(self, exp_buffer) - - # Lastly, check that the extra characters are not printed. - exp_console_out = exp_buffer - CheckConsoleOutput(self, exp_console_out) - - def test_ValidKeysOnLongLine(self): - """Verify that we can still press valid keys if the line is too long.""" - # Fill the line. - test_str = self.console.line_limit * b'o' - exp_console_out = test_str - # Try to fill it even more; these should all be dropped. - test_str += 5 * b'x' - input_stream = BytesToByteList(test_str) - - # We should be able to press the following keys: - # - Backspace - # - Arrow Keys/CTRL+B/CTRL+F/CTRL+P/CTRL+N - # - Delete - # - Home/CTRL+A - # - End/CTRL+E - # - Carriage Return - - # Backspace 1 character - input_stream.append(console.ControlKey.BACKSPACE) - exp_console_out += BACKSPACE_STRING - # Refill the line. - input_stream.extend(BytesToByteList(b'o')) - exp_console_out += b'o' - - # Left arrow key. - input_stream.extend(Keys.LEFT_ARROW) - exp_console_out += OutputStream.MoveCursorLeft(1) - - # Right arrow key. - input_stream.extend(Keys.RIGHT_ARROW) - exp_console_out += OutputStream.MoveCursorRight(1) - - # CTRL+B - input_stream.append(console.ControlKey.CTRL_B) - exp_console_out += OutputStream.MoveCursorLeft(1) - - # CTRL+F - input_stream.append(console.ControlKey.CTRL_F) - exp_console_out += OutputStream.MoveCursorRight(1) - - # Let's press enter now so we can test up and down. - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - exp_console_out += b'\r\n' + self.console.prompt - - # Up arrow key. - input_stream.extend(Keys.UP_ARROW) - exp_console_out += test_str[:self.console.line_limit] - - # Down arrow key. - input_stream.extend(Keys.DOWN_ARROW) - # Since the line was blank, we have to backspace the entire line. - exp_console_out += self.console.line_limit * BACKSPACE_STRING - - # CTRL+P - input_stream.append(console.ControlKey.CTRL_P) - exp_console_out += test_str[:self.console.line_limit] - - # CTRL+N - input_stream.append(console.ControlKey.CTRL_N) - # Since the line was blank, we have to backspace the entire line. - exp_console_out += self.console.line_limit * BACKSPACE_STRING - - # Press the Up arrow key to reprint the long line. - input_stream.extend(Keys.UP_ARROW) - exp_console_out += test_str[:self.console.line_limit] - - # Press the Home key to jump to the beginning of the line. - input_stream.extend(Keys.HOME) - exp_console_out += OutputStream.MoveCursorLeft(self.console.line_limit) - - # Press the End key to jump to the end of the line. - input_stream.extend(Keys.END) - exp_console_out += OutputStream.MoveCursorRight(self.console.line_limit) - - # Press CTRL+A to jump to the beginning of the line. - input_stream.append(console.ControlKey.CTRL_A) - exp_console_out += OutputStream.MoveCursorLeft(self.console.line_limit) - - # Press CTRL+E to jump to the end of the line. - input_stream.extend(Keys.END) - exp_console_out += OutputStream.MoveCursorRight(self.console.line_limit) - - # Move left one column so we can delete a character. - input_stream.extend(Keys.LEFT_ARROW) - exp_console_out += OutputStream.MoveCursorLeft(1) - - # Press the delete key. - input_stream.extend(Keys.DEL) - # This should look like a space, and then move cursor left 1 column since - # we're at the end of line. - exp_console_out += b' ' + OutputStream.MoveCursorLeft(1) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # Verify everything happened correctly. - CheckConsoleOutput(self, exp_console_out) - - def test_BackspaceOnEmptyLine(self): - """Verify that we can backspace on an empty line with no bad effects.""" - # Send a single backspace. - test_str = [console.ControlKey.BACKSPACE] - - # Send the characters in. - for byte in test_str: - self.console.HandleChar(byte) - - # Check the input position. - exp_pos = 0 - CheckInputBufferPosition(self, exp_pos) - - # Check that buffer is empty. - exp_input_buffer = b'' - CheckInputBuffer(self, exp_input_buffer) - - # Check that the console output is empty. - exp_console_out = b'' - CheckConsoleOutput(self, exp_console_out) - - def test_BackspaceWithinLine(self): - """Verify that we shift the chars over when backspacing within a line.""" - # Misspell 'help' - test_str = b'heelp' - input_stream = BytesToByteList(test_str) - # Use the arrow key to go back to fix it. - # Move cursor left 1 column. - input_stream.extend(2*Keys.LEFT_ARROW) - # Backspace once to remove the extra 'e'. - input_stream.append(console.ControlKey.BACKSPACE) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # Verify the input buffer - exp_input_buffer = b'help' - CheckInputBuffer(self, exp_input_buffer) - - # Verify the input buffer position. It should be at 2 (cursor over the 'l') - CheckInputBufferPosition(self, 2) - - # We expect the console output to be the test string, with two moves to the - # left, another move left, and then the rest of the line followed by a - # space. - exp_console_out = test_str - exp_console_out += 2 * OutputStream.MoveCursorLeft(1) - - # Move cursor left 1 column. - exp_console_out += OutputStream.MoveCursorLeft(1) - # Rest of the line and a space. (test_str in this case) - exp_console_out += b'lp ' - # Reset the cursor 2 + 1 to the left. - exp_console_out += OutputStream.MoveCursorLeft(3) - - # Verify console output. - CheckConsoleOutput(self, exp_console_out) - - def test_JumpToBeginningOfLineViaCtrlA(self): - """Verify that we can jump to the beginning of a line with Ctrl+A.""" - # Enter some chars and press CTRL+A - test_str = b'abc' - input_stream = BytesToByteList(test_str) + [console.ControlKey.CTRL_A] - - # Send the characters in. - for byte in input_stream: - self.console.HandleChar(byte) - - # We expect to see our test string followed by a move cursor left. - exp_console_out = test_str - exp_console_out += OutputStream.MoveCursorLeft(len(test_str)) - - # Check to see what whas printed on the console. - CheckConsoleOutput(self, exp_console_out) - - # Check that the input buffer position is now 0. - CheckInputBufferPosition(self, 0) - - # Check input buffer still contains our test string. - CheckInputBuffer(self, test_str) - - def test_JumpToBeginningOfLineViaHomeKey(self): - """Jump to beginning of line via HOME key.""" - test_str = b'version' - input_stream = BytesToByteList(test_str) - input_stream.extend(Keys.HOME) - - # Send out the stream. - for byte in input_stream: - self.console.HandleChar(byte) - - # First, verify that input buffer position is now 0. - CheckInputBufferPosition(self, 0) - - # Next, verify that the input buffer did not change. - CheckInputBuffer(self, test_str) - - # Lastly, check that the cursor moved correctly. - exp_console_out = test_str - exp_console_out += OutputStream.MoveCursorLeft(len(test_str)) - CheckConsoleOutput(self, exp_console_out) - - def test_JumpToEndOfLineViaEndKey(self): - """Jump to the end of the line using the END key.""" - test_str = b'version' - input_stream = BytesToByteList(test_str) - input_stream += [console.ControlKey.CTRL_A] - # Now, jump to the end of the line. - input_stream.extend(Keys.END) - - # Send out the stream. - for byte in input_stream: - self.console.HandleChar(byte) - - # Verify that the input buffer position is correct. This should be at the - # end of the test string. - CheckInputBufferPosition(self, len(test_str)) - - # The expected output should be the test string, followed by a jump to the - # beginning of the line, and lastly a jump to the end of the line. - exp_console_out = test_str - exp_console_out += OutputStream.MoveCursorLeft(len(test_str)) - # Now the jump back to the end of the line. - exp_console_out += OutputStream.MoveCursorRight(len(test_str)) - - # Verify console output stream. - CheckConsoleOutput(self, exp_console_out) - - def test_JumpToEndOfLineViaCtrlE(self): - """Enter some chars and then try to jump to the end. (Should be a no-op)""" - test_str = b'sysinfo' - input_stream = BytesToByteList(test_str) - input_stream.append(console.ControlKey.CTRL_E) - - # Send out the stream - for byte in input_stream: - self.console.HandleChar(byte) - - # Verify that the input buffer position isn't any further than we expect. - # At this point, the position should be at the end of the test string. - CheckInputBufferPosition(self, len(test_str)) - - # Now, let's try to jump to the beginning and then jump back to the end. - input_stream = [console.ControlKey.CTRL_A, console.ControlKey.CTRL_E] - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # Perform the same verification. - CheckInputBufferPosition(self, len(test_str)) - - # Lastly try to jump again, beyond the end. - input_stream = [console.ControlKey.CTRL_E] - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # Perform the same verification. - CheckInputBufferPosition(self, len(test_str)) - - # We expect to see the test string, a jump to the beginning of the line, and - # one jump to the end of the line. - exp_console_out = test_str - # Jump to beginning. - exp_console_out += OutputStream.MoveCursorLeft(len(test_str)) - # Jump back to end. - exp_console_out += OutputStream.MoveCursorRight(len(test_str)) - - # Verify the console output. - CheckConsoleOutput(self, exp_console_out) - - def test_MoveLeftWithArrowKey(self): - """Move cursor left one column with arrow key.""" - test_str = b'tastyspam' - input_stream = BytesToByteList(test_str) - input_stream.extend(Keys.LEFT_ARROW) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # Verify that the input buffer position is 1 less than the length. - CheckInputBufferPosition(self, len(test_str) - 1) - - # Also, verify that the input buffer is not modified. - CheckInputBuffer(self, test_str) - - # We expect the test string, followed by a one column move left. - exp_console_out = test_str + OutputStream.MoveCursorLeft(1) - - # Verify console output. - CheckConsoleOutput(self, exp_console_out) - - def test_MoveLeftWithCtrlB(self): - """Move cursor back one column with Ctrl+B.""" - test_str = b'tastyspam' - input_stream = BytesToByteList(test_str) - input_stream.append(console.ControlKey.CTRL_B) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # Verify that the input buffer position is 1 less than the length. - CheckInputBufferPosition(self, len(test_str) - 1) + Args: + test_case: A unittest.TestCase object representing the current unit test. + exp_pos: An integer representing the expected input buffer position. + """ + test_case.assertEqual( + exp_pos, + test_case.console.input_buffer_pos, + "input buffer position is incorrect.\ngot: " + + str(test_case.console.input_buffer_pos) + + "\nexp: " + + str(exp_pos) + + "\n" + + str(test_case.console), + ) - # Also, verify that the input buffer is not modified. - CheckInputBuffer(self, test_str) - # We expect the test string, followed by a one column move left. - exp_console_out = test_str + OutputStream.MoveCursorLeft(1) +def CheckHistoryBuffer(test_case, exp_history): + """Verify that the items in the history buffer are what we expect. - # Verify console output. - CheckConsoleOutput(self, exp_console_out) + Args: + test_case: A unittest.TestCase object representing the current unit test. + exp_history: A list of strings representing the expected contents of the + history buffer. + """ + # First, check to see if the length is what we expect. + test_case.assertEqual( + len(exp_history), + len(test_case.console.history), + ( + "The number of items in the history is unexpected.\n" + "exp: " + str(len(exp_history)) + "\n" + "got: " + str(len(test_case.console.history)) + "\n" + "internal state:\n" + str(test_case.console) + ), + ) + + # Next, check the actual contents of the history buffer. + for i in range(len(exp_history)): + test_case.assertEqual( + exp_history[i], + test_case.console.history[i], + ( + b"history buffer contents are incorrect.\n" + b"exp: " + exp_history[i] + b"\n" + b"got: " + test_case.console.history[i] + b"\n" + b"internal state:\n" + str(test_case.console).encode("ascii") + ), + ) - def test_MoveRightWithArrowKey(self): - """Move cursor one column to the right with the arrow key.""" - test_str = b'version' - input_stream = BytesToByteList(test_str) - # Jump to beginning of line. - input_stream.append(console.ControlKey.CTRL_A) - # Press right arrow key. - input_stream.extend(Keys.RIGHT_ARROW) - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) +class TestConsoleEditingMethods(unittest.TestCase): + """Test case to verify all console editing methods.""" + + def setUp(self): + """Setup the test harness.""" + # Setup logging with a timestamp, the module, and the log level. + logging.basicConfig( + level=logging.DEBUG, + format=("%(asctime)s - %(module)s -" " %(levelname)s - %(message)s"), + ) + + # Create a temp file and set both the controller and peripheral PTYs to the + # file to create a loopback. + self.tempfile = tempfile.TemporaryFile() + + # Create some mock pipes. These won't be used since we'll mock out sends + # to the interpreter. + mock_pipe_end_0, mock_pipe_end_1 = threadproc_shim.Pipe() + self.console = console.Console( + self.tempfile.fileno(), + self.tempfile, + tempfile.TemporaryFile(), + mock_pipe_end_0, + mock_pipe_end_1, + "EC", + ) + + # Console editing methods are only valid for enhanced EC images, therefore + # we have to assume that the "EC" we're talking to is enhanced. By default, + # the console believes that the EC it's communicating with is NOT enhanced + # which is why we have to override it here. + self.console.enhanced_ec = True + self.console.CheckForEnhancedECImage = mock.MagicMock(return_value=True) + + def test_EnteringChars(self): + """Verify that characters are echoed onto the console.""" + test_str = b"abc" + input_stream = BytesToByteList(test_str) + + # Send the characters in. + for byte in input_stream: + self.console.HandleChar(byte) + + # Check the input position. + exp_pos = len(test_str) + CheckInputBufferPosition(self, exp_pos) + + # Verify that the input buffer is correct. + expected_buffer = test_str + CheckInputBuffer(self, expected_buffer) + + # Check console output + exp_console_out = test_str + CheckConsoleOutput(self, exp_console_out) + + def test_EnteringDeletingMoreCharsThanEntered(self): + """Verify that we can press backspace more than we have entered chars.""" + test_str = b"spamspam" + input_stream = BytesToByteList(test_str) + + # Send the characters in. + for byte in input_stream: + self.console.HandleChar(byte) + + # Now backspace 1 more than what we sent. + input_stream = [] + for _ in range(len(test_str) + 1): + input_stream.append(console.ControlKey.BACKSPACE) + + # Send that sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # First, verify that input buffer position is 0. + CheckInputBufferPosition(self, 0) + + # Next, examine the output stream for the correct sequence. + exp_console_out = test_str + for _ in range(len(test_str)): + exp_console_out += BACKSPACE_STRING + + # Now, verify that we got what we expected. + CheckConsoleOutput(self, exp_console_out) + + def test_EnteringMoreThanCharLimit(self): + """Verify that we drop characters when the line is too long.""" + test_str = self.console.line_limit * b"o" # All allowed. + test_str += 5 * b"x" # All should be dropped. + input_stream = BytesToByteList(test_str) + + # Send the characters in. + for byte in input_stream: + self.console.HandleChar(byte) + + # First, we expect that input buffer position should be equal to the line + # limit. + exp_pos = self.console.line_limit + CheckInputBufferPosition(self, exp_pos) + + # The input buffer should only hold until the line limit. + exp_buffer = test_str[0 : self.console.line_limit] + CheckInputBuffer(self, exp_buffer) + + # Lastly, check that the extra characters are not printed. + exp_console_out = exp_buffer + CheckConsoleOutput(self, exp_console_out) + + def test_ValidKeysOnLongLine(self): + """Verify that we can still press valid keys if the line is too long.""" + # Fill the line. + test_str = self.console.line_limit * b"o" + exp_console_out = test_str + # Try to fill it even more; these should all be dropped. + test_str += 5 * b"x" + input_stream = BytesToByteList(test_str) + + # We should be able to press the following keys: + # - Backspace + # - Arrow Keys/CTRL+B/CTRL+F/CTRL+P/CTRL+N + # - Delete + # - Home/CTRL+A + # - End/CTRL+E + # - Carriage Return + + # Backspace 1 character + input_stream.append(console.ControlKey.BACKSPACE) + exp_console_out += BACKSPACE_STRING + # Refill the line. + input_stream.extend(BytesToByteList(b"o")) + exp_console_out += b"o" + + # Left arrow key. + input_stream.extend(Keys.LEFT_ARROW) + exp_console_out += OutputStream.MoveCursorLeft(1) + + # Right arrow key. + input_stream.extend(Keys.RIGHT_ARROW) + exp_console_out += OutputStream.MoveCursorRight(1) + + # CTRL+B + input_stream.append(console.ControlKey.CTRL_B) + exp_console_out += OutputStream.MoveCursorLeft(1) + + # CTRL+F + input_stream.append(console.ControlKey.CTRL_F) + exp_console_out += OutputStream.MoveCursorRight(1) + + # Let's press enter now so we can test up and down. + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + exp_console_out += b"\r\n" + self.console.prompt + + # Up arrow key. + input_stream.extend(Keys.UP_ARROW) + exp_console_out += test_str[: self.console.line_limit] + + # Down arrow key. + input_stream.extend(Keys.DOWN_ARROW) + # Since the line was blank, we have to backspace the entire line. + exp_console_out += self.console.line_limit * BACKSPACE_STRING + + # CTRL+P + input_stream.append(console.ControlKey.CTRL_P) + exp_console_out += test_str[: self.console.line_limit] + + # CTRL+N + input_stream.append(console.ControlKey.CTRL_N) + # Since the line was blank, we have to backspace the entire line. + exp_console_out += self.console.line_limit * BACKSPACE_STRING + + # Press the Up arrow key to reprint the long line. + input_stream.extend(Keys.UP_ARROW) + exp_console_out += test_str[: self.console.line_limit] + + # Press the Home key to jump to the beginning of the line. + input_stream.extend(Keys.HOME) + exp_console_out += OutputStream.MoveCursorLeft(self.console.line_limit) + + # Press the End key to jump to the end of the line. + input_stream.extend(Keys.END) + exp_console_out += OutputStream.MoveCursorRight(self.console.line_limit) + + # Press CTRL+A to jump to the beginning of the line. + input_stream.append(console.ControlKey.CTRL_A) + exp_console_out += OutputStream.MoveCursorLeft(self.console.line_limit) + + # Press CTRL+E to jump to the end of the line. + input_stream.extend(Keys.END) + exp_console_out += OutputStream.MoveCursorRight(self.console.line_limit) + + # Move left one column so we can delete a character. + input_stream.extend(Keys.LEFT_ARROW) + exp_console_out += OutputStream.MoveCursorLeft(1) + + # Press the delete key. + input_stream.extend(Keys.DEL) + # This should look like a space, and then move cursor left 1 column since + # we're at the end of line. + exp_console_out += b" " + OutputStream.MoveCursorLeft(1) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # Verify everything happened correctly. + CheckConsoleOutput(self, exp_console_out) + + def test_BackspaceOnEmptyLine(self): + """Verify that we can backspace on an empty line with no bad effects.""" + # Send a single backspace. + test_str = [console.ControlKey.BACKSPACE] + + # Send the characters in. + for byte in test_str: + self.console.HandleChar(byte) + + # Check the input position. + exp_pos = 0 + CheckInputBufferPosition(self, exp_pos) + + # Check that buffer is empty. + exp_input_buffer = b"" + CheckInputBuffer(self, exp_input_buffer) + + # Check that the console output is empty. + exp_console_out = b"" + CheckConsoleOutput(self, exp_console_out) + + def test_BackspaceWithinLine(self): + """Verify that we shift the chars over when backspacing within a line.""" + # Misspell 'help' + test_str = b"heelp" + input_stream = BytesToByteList(test_str) + # Use the arrow key to go back to fix it. + # Move cursor left 1 column. + input_stream.extend(2 * Keys.LEFT_ARROW) + # Backspace once to remove the extra 'e'. + input_stream.append(console.ControlKey.BACKSPACE) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # Verify the input buffer + exp_input_buffer = b"help" + CheckInputBuffer(self, exp_input_buffer) + + # Verify the input buffer position. It should be at 2 (cursor over the 'l') + CheckInputBufferPosition(self, 2) + + # We expect the console output to be the test string, with two moves to the + # left, another move left, and then the rest of the line followed by a + # space. + exp_console_out = test_str + exp_console_out += 2 * OutputStream.MoveCursorLeft(1) + + # Move cursor left 1 column. + exp_console_out += OutputStream.MoveCursorLeft(1) + # Rest of the line and a space. (test_str in this case) + exp_console_out += b"lp " + # Reset the cursor 2 + 1 to the left. + exp_console_out += OutputStream.MoveCursorLeft(3) + + # Verify console output. + CheckConsoleOutput(self, exp_console_out) + + def test_JumpToBeginningOfLineViaCtrlA(self): + """Verify that we can jump to the beginning of a line with Ctrl+A.""" + # Enter some chars and press CTRL+A + test_str = b"abc" + input_stream = BytesToByteList(test_str) + [console.ControlKey.CTRL_A] + + # Send the characters in. + for byte in input_stream: + self.console.HandleChar(byte) + + # We expect to see our test string followed by a move cursor left. + exp_console_out = test_str + exp_console_out += OutputStream.MoveCursorLeft(len(test_str)) + + # Check to see what whas printed on the console. + CheckConsoleOutput(self, exp_console_out) + + # Check that the input buffer position is now 0. + CheckInputBufferPosition(self, 0) + + # Check input buffer still contains our test string. + CheckInputBuffer(self, test_str) + + def test_JumpToBeginningOfLineViaHomeKey(self): + """Jump to beginning of line via HOME key.""" + test_str = b"version" + input_stream = BytesToByteList(test_str) + input_stream.extend(Keys.HOME) + + # Send out the stream. + for byte in input_stream: + self.console.HandleChar(byte) + + # First, verify that input buffer position is now 0. + CheckInputBufferPosition(self, 0) + + # Next, verify that the input buffer did not change. + CheckInputBuffer(self, test_str) + + # Lastly, check that the cursor moved correctly. + exp_console_out = test_str + exp_console_out += OutputStream.MoveCursorLeft(len(test_str)) + CheckConsoleOutput(self, exp_console_out) + + def test_JumpToEndOfLineViaEndKey(self): + """Jump to the end of the line using the END key.""" + test_str = b"version" + input_stream = BytesToByteList(test_str) + input_stream += [console.ControlKey.CTRL_A] + # Now, jump to the end of the line. + input_stream.extend(Keys.END) + + # Send out the stream. + for byte in input_stream: + self.console.HandleChar(byte) + + # Verify that the input buffer position is correct. This should be at the + # end of the test string. + CheckInputBufferPosition(self, len(test_str)) + + # The expected output should be the test string, followed by a jump to the + # beginning of the line, and lastly a jump to the end of the line. + exp_console_out = test_str + exp_console_out += OutputStream.MoveCursorLeft(len(test_str)) + # Now the jump back to the end of the line. + exp_console_out += OutputStream.MoveCursorRight(len(test_str)) + + # Verify console output stream. + CheckConsoleOutput(self, exp_console_out) + + def test_JumpToEndOfLineViaCtrlE(self): + """Enter some chars and then try to jump to the end. (Should be a no-op)""" + test_str = b"sysinfo" + input_stream = BytesToByteList(test_str) + input_stream.append(console.ControlKey.CTRL_E) + + # Send out the stream + for byte in input_stream: + self.console.HandleChar(byte) + + # Verify that the input buffer position isn't any further than we expect. + # At this point, the position should be at the end of the test string. + CheckInputBufferPosition(self, len(test_str)) + + # Now, let's try to jump to the beginning and then jump back to the end. + input_stream = [console.ControlKey.CTRL_A, console.ControlKey.CTRL_E] + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # Perform the same verification. + CheckInputBufferPosition(self, len(test_str)) + + # Lastly try to jump again, beyond the end. + input_stream = [console.ControlKey.CTRL_E] + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # Perform the same verification. + CheckInputBufferPosition(self, len(test_str)) + + # We expect to see the test string, a jump to the beginning of the line, and + # one jump to the end of the line. + exp_console_out = test_str + # Jump to beginning. + exp_console_out += OutputStream.MoveCursorLeft(len(test_str)) + # Jump back to end. + exp_console_out += OutputStream.MoveCursorRight(len(test_str)) + + # Verify the console output. + CheckConsoleOutput(self, exp_console_out) + + def test_MoveLeftWithArrowKey(self): + """Move cursor left one column with arrow key.""" + test_str = b"tastyspam" + input_stream = BytesToByteList(test_str) + input_stream.extend(Keys.LEFT_ARROW) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # Verify that the input buffer position is 1 less than the length. + CheckInputBufferPosition(self, len(test_str) - 1) + + # Also, verify that the input buffer is not modified. + CheckInputBuffer(self, test_str) + + # We expect the test string, followed by a one column move left. + exp_console_out = test_str + OutputStream.MoveCursorLeft(1) + + # Verify console output. + CheckConsoleOutput(self, exp_console_out) + + def test_MoveLeftWithCtrlB(self): + """Move cursor back one column with Ctrl+B.""" + test_str = b"tastyspam" + input_stream = BytesToByteList(test_str) + input_stream.append(console.ControlKey.CTRL_B) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # Verify that the input buffer position is 1 less than the length. + CheckInputBufferPosition(self, len(test_str) - 1) - # Verify that the input buffer position is 1. - CheckInputBufferPosition(self, 1) + # Also, verify that the input buffer is not modified. + CheckInputBuffer(self, test_str) - # Also, verify that the input buffer is not modified. - CheckInputBuffer(self, test_str) + # We expect the test string, followed by a one column move left. + exp_console_out = test_str + OutputStream.MoveCursorLeft(1) - # We expect the test string, followed by a jump to the beginning of the - # line, and finally a move right 1. - exp_console_out = test_str + OutputStream.MoveCursorLeft(len((test_str))) - - # A move right 1 column. - exp_console_out += OutputStream.MoveCursorRight(1) - - # Verify console output. - CheckConsoleOutput(self, exp_console_out) - - def test_MoveRightWithCtrlF(self): - """Move cursor forward one column with Ctrl+F.""" - test_str = b'panicinfo' - input_stream = BytesToByteList(test_str) - input_stream.append(console.ControlKey.CTRL_A) - # Now, move right one column. - input_stream.append(console.ControlKey.CTRL_F) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # Verify that the input buffer position is 1. - CheckInputBufferPosition(self, 1) - - # Also, verify that the input buffer is not modified. - CheckInputBuffer(self, test_str) - - # We expect the test string, followed by a jump to the beginning of the - # line, and finally a move right 1. - exp_console_out = test_str + OutputStream.MoveCursorLeft(len((test_str))) - - # A move right 1 column. - exp_console_out += OutputStream.MoveCursorRight(1) - - # Verify console output. - CheckConsoleOutput(self, exp_console_out) - - def test_ImpossibleMoveLeftWithArrowKey(self): - """Verify that we can't move left at the beginning of the line.""" - # We shouldn't be able to move left if we're at the beginning of the line. - input_stream = Keys.LEFT_ARROW - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # Nothing should have been output. - exp_console_output = b'' - CheckConsoleOutput(self, exp_console_output) - - # The input buffer position should still be 0. - CheckInputBufferPosition(self, 0) - - # The input buffer itself should be empty. - CheckInputBuffer(self, b'') - - def test_ImpossibleMoveRightWithArrowKey(self): - """Verify that we can't move right at the end of the line.""" - # We shouldn't be able to move right if we're at the end of the line. - input_stream = Keys.RIGHT_ARROW - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # Nothing should have been output. - exp_console_output = b'' - CheckConsoleOutput(self, exp_console_output) - - # The input buffer position should still be 0. - CheckInputBufferPosition(self, 0) - - # The input buffer itself should be empty. - CheckInputBuffer(self, b'') - - def test_KillEntireLine(self): - """Verify that we can kill an entire line with Ctrl+K.""" - test_str = b'accelinfo on' - input_stream = BytesToByteList(test_str) - # Jump to beginning of line and then kill it with Ctrl+K. - input_stream.extend([console.ControlKey.CTRL_A, console.ControlKey.CTRL_K]) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # First, we expect that the input buffer is empty. - CheckInputBuffer(self, b'') - - # The buffer position should be 0. - CheckInputBufferPosition(self, 0) - - # What we expect to see on the console stream should be the following. The - # test string, a jump to the beginning of the line, then jump back to the - # end of the line and replace the line with spaces. - exp_console_out = test_str - # Jump to beginning of line. - exp_console_out += OutputStream.MoveCursorLeft(len(test_str)) - # Jump to end of line. - exp_console_out += OutputStream.MoveCursorRight(len(test_str)) - # Replace line with spaces, which looks like backspaces. - for _ in range(len(test_str)): - exp_console_out += BACKSPACE_STRING - - # Verify the console output. - CheckConsoleOutput(self, exp_console_out) - - def test_KillPartialLine(self): - """Verify that we can kill a portion of a line.""" - test_str = b'accelread 0 1' - input_stream = BytesToByteList(test_str) - len_to_kill = 5 - for _ in range(len_to_kill): - # Move cursor left - input_stream.extend(Keys.LEFT_ARROW) - # Now kill - input_stream.append(console.ControlKey.CTRL_K) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # First, check that the input buffer was truncated. - exp_input_buffer = test_str[:-len_to_kill] - CheckInputBuffer(self, exp_input_buffer) - - # Verify the input buffer position. - CheckInputBufferPosition(self, len(test_str) - len_to_kill) - - # The console output stream that we expect is the test string followed by a - # move left of len_to_kill, then a jump to the end of the line and backspace - # of len_to_kill. - exp_console_out = test_str - for _ in range(len_to_kill): - # Move left 1 column. - exp_console_out += OutputStream.MoveCursorLeft(1) - # Then jump to the end of the line - exp_console_out += OutputStream.MoveCursorRight(len_to_kill) - # Backspace of len_to_kill - for _ in range(len_to_kill): - exp_console_out += BACKSPACE_STRING - - # Verify console output. - CheckConsoleOutput(self, exp_console_out) - - def test_InsertingCharacters(self): - """Verify that we can insert characters within the line.""" - test_str = b'accel 0 1' # Here we forgot the 'read' part in 'accelread' - input_stream = BytesToByteList(test_str) - # We need to move over to the 'l' and add read. - insertion_point = test_str.find(b'l') + 1 - for i in range(len(test_str) - insertion_point): - # Move cursor left. - input_stream.extend(Keys.LEFT_ARROW) - # Now, add in 'read' - added_str = b'read' - input_stream.extend(BytesToByteList(added_str)) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # First, verify that the input buffer is correct. - exp_input_buffer = test_str[:insertion_point] + added_str - exp_input_buffer += test_str[insertion_point:] - CheckInputBuffer(self, exp_input_buffer) - - # Verify that the input buffer position is correct. - exp_input_buffer_pos = insertion_point + len(added_str) - CheckInputBufferPosition(self, exp_input_buffer_pos) - - # The console output stream that we expect is the test string, followed by - # move cursor left until the 'l' was found, the added test string while - # shifting characters around. - exp_console_out = test_str - for i in range(len(test_str) - insertion_point): - # Move cursor left. - exp_console_out += OutputStream.MoveCursorLeft(1) - - # Now for each character, write the rest of the line will be shifted to the - # right one column. - for i in range(len(added_str)): - # Printed character. - exp_console_out += added_str[i:i+1] - # The rest of the line - exp_console_out += test_str[insertion_point:] - # Reset the cursor back left - reset_dist = len(test_str[insertion_point:]) - exp_console_out += OutputStream.MoveCursorLeft(reset_dist) - - # Verify the console output. - CheckConsoleOutput(self, exp_console_out) - - def test_StoreCommandHistory(self): - """Verify that entered commands are stored in the history.""" - test_commands = [] - test_commands.append(b'help') - test_commands.append(b'version') - test_commands.append(b'accelread 0 1') - input_stream = [] - for c in test_commands: - input_stream.extend(BytesToByteList(c)) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # We expect to have the test commands in the history buffer. - exp_history_buf = test_commands - CheckHistoryBuffer(self, exp_history_buf) - - def test_CycleUpThruCommandHistory(self): - """Verify that the UP arrow key will print itmes in the history buffer.""" - # Enter some commands. - test_commands = [b'version', b'accelrange 0', b'battery', b'gettime'] - input_stream = [] - for command in test_commands: - input_stream.extend(BytesToByteList(command)) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Now, hit the UP arrow key to print the previous entries. - for i in range(len(test_commands)): - input_stream.extend(Keys.UP_ARROW) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # The expected output should be test commands with prompts printed in - # between, followed by line kills with the previous test commands printed. - exp_console_out = b'' - for i in range(len(test_commands)): - exp_console_out += test_commands[i] + b'\r\n' + self.console.prompt - - # When we press up, the line should be cleared and print the previous buffer - # entry. - for i in range(len(test_commands)-1, 0, -1): - exp_console_out += test_commands[i] - # Backspace to the beginning. - for i in range(len(test_commands[i])): - exp_console_out += BACKSPACE_STRING + # Verify console output. + CheckConsoleOutput(self, exp_console_out) - # The last command should just be printed out with no backspacing. - exp_console_out += test_commands[0] - - # Now, verify. - CheckConsoleOutput(self, exp_console_out) - - def test_UpArrowOnEmptyHistory(self): - """Ensure nothing happens if the history is empty.""" - # Press the up arrow key twice. - input_stream = 2 * Keys.UP_ARROW - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # We expect nothing to have happened. - exp_console_out = b'' - exp_input_buffer = b'' - exp_input_buffer_pos = 0 - exp_history_buf = [] - - # Verify. - CheckConsoleOutput(self, exp_console_out) - CheckInputBufferPosition(self, exp_input_buffer_pos) - CheckInputBuffer(self, exp_input_buffer) - CheckHistoryBuffer(self, exp_history_buf) - - def test_UpArrowDoesNotGoOutOfBounds(self): - """Verify that pressing the up arrow many times won't go out of bounds.""" - # Enter one command. - test_str = b'help version' - input_stream = BytesToByteList(test_str) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - # Then press the up arrow key twice. - input_stream.extend(2 * Keys.UP_ARROW) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # Verify that the history buffer is correct. - exp_history_buf = [test_str] - CheckHistoryBuffer(self, exp_history_buf) - - # We expect that the console output should only contain our entered command, - # a new prompt, and then our command aggain. - exp_console_out = test_str + b'\r\n' + self.console.prompt - # Pressing up should reprint the command we entered. - exp_console_out += test_str - - # Verify. - CheckConsoleOutput(self, exp_console_out) - - def test_CycleDownThruCommandHistory(self): - """Verify that we can select entries by hitting the down arrow.""" - # Enter at least 4 commands. - test_commands = [b'version', b'accelrange 0', b'battery', b'gettime'] - input_stream = [] - for command in test_commands: - input_stream.extend(BytesToByteList(command)) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Now, hit the UP arrow key twice to print the previous two entries. - for i in range(2): - input_stream.extend(Keys.UP_ARROW) - - # Now, hit the DOWN arrow key twice to print the newer entries. - input_stream.extend(2*Keys.DOWN_ARROW) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # The expected output should be commands that we entered, followed by - # prompts, then followed by our last two commands in reverse. Then, we - # should see the last entry in the list, followed by the saved partial cmd - # of a blank line. - exp_console_out = b'' - for i in range(len(test_commands)): - exp_console_out += test_commands[i] + b'\r\n' + self.console.prompt - - # When we press up, the line should be cleared and print the previous buffer - # entry. - for i in range(len(test_commands)-1, 1, -1): - exp_console_out += test_commands[i] - # Backspace to the beginning. - for i in range(len(test_commands[i])): - exp_console_out += BACKSPACE_STRING + def test_MoveRightWithArrowKey(self): + """Move cursor one column to the right with the arrow key.""" + test_str = b"version" + input_stream = BytesToByteList(test_str) + # Jump to beginning of line. + input_stream.append(console.ControlKey.CTRL_A) + # Press right arrow key. + input_stream.extend(Keys.RIGHT_ARROW) - # When we press down, it should have cleared the last command (which we - # covered with the previous for loop), and then prints the next command. - exp_console_out += test_commands[3] - for i in range(len(test_commands[3])): - exp_console_out += BACKSPACE_STRING - - # Verify console output. - CheckConsoleOutput(self, exp_console_out) - - # Verify input buffer. - exp_input_buffer = b'' # Empty because our partial command was empty. - exp_input_buffer_pos = len(exp_input_buffer) - CheckInputBuffer(self, exp_input_buffer) - CheckInputBufferPosition(self, exp_input_buffer_pos) - - def test_SavingPartialCommandWhenNavigatingHistory(self): - """Verify that partial commands are saved when navigating history.""" - # Enter a command. - test_str = b'accelinfo' - input_stream = BytesToByteList(test_str) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Enter a partial command. - partial_cmd = b'ver' - input_stream.extend(BytesToByteList(partial_cmd)) - - # Hit the UP arrow key. - input_stream.extend(Keys.UP_ARROW) - # Then, the DOWN arrow key. - input_stream.extend(Keys.DOWN_ARROW) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # The expected output should be the command we entered, a prompt, the - # partial command, clearing of the partial command, the command entered, - # clearing of the command entered, and then the partial command. - exp_console_out = test_str + b'\r\n' + self.console.prompt - exp_console_out += partial_cmd - for _ in range(len(partial_cmd)): - exp_console_out += BACKSPACE_STRING - exp_console_out += test_str - for _ in range(len(test_str)): - exp_console_out += BACKSPACE_STRING - exp_console_out += partial_cmd - - # Verify console output. - CheckConsoleOutput(self, exp_console_out) - - # Verify input buffer. - exp_input_buffer = partial_cmd - exp_input_buffer_pos = len(exp_input_buffer) - CheckInputBuffer(self, exp_input_buffer) - CheckInputBufferPosition(self, exp_input_buffer_pos) - - def test_DownArrowOnEmptyHistory(self): - """Ensure nothing happens if the history is empty.""" - # Then press the up down arrow twice. - input_stream = 2 * Keys.DOWN_ARROW - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # We expect nothing to have happened. - exp_console_out = b'' - exp_input_buffer = b'' - exp_input_buffer_pos = 0 - exp_history_buf = [] - - # Verify. - CheckConsoleOutput(self, exp_console_out) - CheckInputBufferPosition(self, exp_input_buffer_pos) - CheckInputBuffer(self, exp_input_buffer) - CheckHistoryBuffer(self, exp_history_buf) - - def test_DeleteCharsUsingDELKey(self): - """Verify that we can delete characters using the DEL key.""" - test_str = b'version' - input_stream = BytesToByteList(test_str) - - # Hit the left arrow key 2 times. - input_stream.extend(2 * Keys.LEFT_ARROW) - - # Press the DEL key. - input_stream.extend(Keys.DEL) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # The expected output should be the command we entered, 2 individual cursor - # moves to the left, and then removing a char and shifting everything to the - # left one column. - exp_console_out = test_str - exp_console_out += 2 * OutputStream.MoveCursorLeft(1) - - # Remove the char by shifting everything to the left one, slicing out the - # remove char. - exp_console_out += test_str[-1:] + b' ' - - # Reset the cursor by moving back 2 columns because of the 'n' and space. - exp_console_out += OutputStream.MoveCursorLeft(2) - - # Verify console output. - CheckConsoleOutput(self, exp_console_out) - - # Verify input buffer. The input buffer should have the char sliced out and - # be positioned where the char was removed. - exp_input_buffer = test_str[:-2] + test_str[-1:] - exp_input_buffer_pos = len(exp_input_buffer) - 1 - CheckInputBuffer(self, exp_input_buffer) - CheckInputBufferPosition(self, exp_input_buffer_pos) - - def test_RepeatedCommandInHistory(self): - """Verify that we don't store 2 consecutive identical commands in history""" - # Enter a few commands. - test_commands = [b'version', b'accelrange 0', b'battery', b'gettime'] - # Repeat the last command. - test_commands.append(test_commands[len(test_commands)-1]) - - input_stream = [] - for command in test_commands: - input_stream.extend(BytesToByteList(command)) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # Verify that the history buffer is correct. The last command, since - # it was repeated, should not have been added to the history. - exp_history_buf = test_commands[0:len(test_commands)-1] - CheckHistoryBuffer(self, exp_history_buf) + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + # Verify that the input buffer position is 1. + CheckInputBufferPosition(self, 1) -class TestConsoleCompatibility(unittest.TestCase): - """Verify that console can speak to enhanced and non-enhanced EC images.""" - def setUp(self): - """Setup the test harness.""" - # Setup logging with a timestamp, the module, and the log level. - logging.basicConfig(level=logging.DEBUG, - format=('%(asctime)s - %(module)s -' - ' %(levelname)s - %(message)s')) - # Create a temp file and set both the controller and peripheral PTYs to the - # file to create a loopback. - self.tempfile = tempfile.TemporaryFile() - - # Mock out the pipes. - mock_pipe_end_0, mock_pipe_end_1 = mock.MagicMock(), mock.MagicMock() - self.console = console.Console(self.tempfile.fileno(), self.tempfile, - tempfile.TemporaryFile(), - mock_pipe_end_0, mock_pipe_end_1, "EC") - - @mock.patch('ec3po.console.Console.CheckForEnhancedECImage') - def test_ActAsPassThruInNonEnhancedMode(self, mock_check): - """Verify we simply pass everything thru to non-enhanced ECs. + # Also, verify that the input buffer is not modified. + CheckInputBuffer(self, test_str) - Args: - mock_check: A MagicMock object replacing the CheckForEnhancedECImage() - method. - """ - # Set the interrogation mode to always so that we actually interrogate. - self.console.interrogation_mode = b'always' - - # Assume EC interrogations indicate that the image is non-enhanced. - mock_check.return_value = False - - # Press enter, followed by the command, and another enter. - input_stream = [] - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - test_command = b'version' - input_stream.extend(BytesToByteList(test_command)) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # Expected calls to send down the pipe would be each character of the test - # command. - expected_calls = [] - expected_calls.append(mock.call( - six.int2byte(console.ControlKey.CARRIAGE_RETURN))) - for char in test_command: - if six.PY3: - expected_calls.append(mock.call(bytes([char]))) - else: - expected_calls.append(mock.call(char)) - expected_calls.append(mock.call( - six.int2byte(console.ControlKey.CARRIAGE_RETURN))) - - # Verify that the calls happened. - self.console.cmd_pipe.send.assert_has_calls(expected_calls) - - # Since we're acting as a pass-thru, the input buffer should be empty and - # input_buffer_pos is 0. - CheckInputBuffer(self, b'') - CheckInputBufferPosition(self, 0) - - @mock.patch('ec3po.console.Console.CheckForEnhancedECImage') - def test_TransitionFromNonEnhancedToEnhanced(self, mock_check): - """Verify that we transition correctly to enhanced mode. + # We expect the test string, followed by a jump to the beginning of the + # line, and finally a move right 1. + exp_console_out = test_str + OutputStream.MoveCursorLeft(len((test_str))) + + # A move right 1 column. + exp_console_out += OutputStream.MoveCursorRight(1) + + # Verify console output. + CheckConsoleOutput(self, exp_console_out) + + def test_MoveRightWithCtrlF(self): + """Move cursor forward one column with Ctrl+F.""" + test_str = b"panicinfo" + input_stream = BytesToByteList(test_str) + input_stream.append(console.ControlKey.CTRL_A) + # Now, move right one column. + input_stream.append(console.ControlKey.CTRL_F) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # Verify that the input buffer position is 1. + CheckInputBufferPosition(self, 1) + + # Also, verify that the input buffer is not modified. + CheckInputBuffer(self, test_str) + + # We expect the test string, followed by a jump to the beginning of the + # line, and finally a move right 1. + exp_console_out = test_str + OutputStream.MoveCursorLeft(len((test_str))) + + # A move right 1 column. + exp_console_out += OutputStream.MoveCursorRight(1) + + # Verify console output. + CheckConsoleOutput(self, exp_console_out) + + def test_ImpossibleMoveLeftWithArrowKey(self): + """Verify that we can't move left at the beginning of the line.""" + # We shouldn't be able to move left if we're at the beginning of the line. + input_stream = Keys.LEFT_ARROW + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # Nothing should have been output. + exp_console_output = b"" + CheckConsoleOutput(self, exp_console_output) + + # The input buffer position should still be 0. + CheckInputBufferPosition(self, 0) + + # The input buffer itself should be empty. + CheckInputBuffer(self, b"") + + def test_ImpossibleMoveRightWithArrowKey(self): + """Verify that we can't move right at the end of the line.""" + # We shouldn't be able to move right if we're at the end of the line. + input_stream = Keys.RIGHT_ARROW + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # Nothing should have been output. + exp_console_output = b"" + CheckConsoleOutput(self, exp_console_output) + + # The input buffer position should still be 0. + CheckInputBufferPosition(self, 0) + + # The input buffer itself should be empty. + CheckInputBuffer(self, b"") + + def test_KillEntireLine(self): + """Verify that we can kill an entire line with Ctrl+K.""" + test_str = b"accelinfo on" + input_stream = BytesToByteList(test_str) + # Jump to beginning of line and then kill it with Ctrl+K. + input_stream.extend([console.ControlKey.CTRL_A, console.ControlKey.CTRL_K]) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # First, we expect that the input buffer is empty. + CheckInputBuffer(self, b"") + + # The buffer position should be 0. + CheckInputBufferPosition(self, 0) + + # What we expect to see on the console stream should be the following. The + # test string, a jump to the beginning of the line, then jump back to the + # end of the line and replace the line with spaces. + exp_console_out = test_str + # Jump to beginning of line. + exp_console_out += OutputStream.MoveCursorLeft(len(test_str)) + # Jump to end of line. + exp_console_out += OutputStream.MoveCursorRight(len(test_str)) + # Replace line with spaces, which looks like backspaces. + for _ in range(len(test_str)): + exp_console_out += BACKSPACE_STRING + + # Verify the console output. + CheckConsoleOutput(self, exp_console_out) + + def test_KillPartialLine(self): + """Verify that we can kill a portion of a line.""" + test_str = b"accelread 0 1" + input_stream = BytesToByteList(test_str) + len_to_kill = 5 + for _ in range(len_to_kill): + # Move cursor left + input_stream.extend(Keys.LEFT_ARROW) + # Now kill + input_stream.append(console.ControlKey.CTRL_K) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # First, check that the input buffer was truncated. + exp_input_buffer = test_str[:-len_to_kill] + CheckInputBuffer(self, exp_input_buffer) + + # Verify the input buffer position. + CheckInputBufferPosition(self, len(test_str) - len_to_kill) + + # The console output stream that we expect is the test string followed by a + # move left of len_to_kill, then a jump to the end of the line and backspace + # of len_to_kill. + exp_console_out = test_str + for _ in range(len_to_kill): + # Move left 1 column. + exp_console_out += OutputStream.MoveCursorLeft(1) + # Then jump to the end of the line + exp_console_out += OutputStream.MoveCursorRight(len_to_kill) + # Backspace of len_to_kill + for _ in range(len_to_kill): + exp_console_out += BACKSPACE_STRING + + # Verify console output. + CheckConsoleOutput(self, exp_console_out) + + def test_InsertingCharacters(self): + """Verify that we can insert characters within the line.""" + test_str = b"accel 0 1" # Here we forgot the 'read' part in 'accelread' + input_stream = BytesToByteList(test_str) + # We need to move over to the 'l' and add read. + insertion_point = test_str.find(b"l") + 1 + for i in range(len(test_str) - insertion_point): + # Move cursor left. + input_stream.extend(Keys.LEFT_ARROW) + # Now, add in 'read' + added_str = b"read" + input_stream.extend(BytesToByteList(added_str)) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # First, verify that the input buffer is correct. + exp_input_buffer = test_str[:insertion_point] + added_str + exp_input_buffer += test_str[insertion_point:] + CheckInputBuffer(self, exp_input_buffer) + + # Verify that the input buffer position is correct. + exp_input_buffer_pos = insertion_point + len(added_str) + CheckInputBufferPosition(self, exp_input_buffer_pos) + + # The console output stream that we expect is the test string, followed by + # move cursor left until the 'l' was found, the added test string while + # shifting characters around. + exp_console_out = test_str + for i in range(len(test_str) - insertion_point): + # Move cursor left. + exp_console_out += OutputStream.MoveCursorLeft(1) + + # Now for each character, write the rest of the line will be shifted to the + # right one column. + for i in range(len(added_str)): + # Printed character. + exp_console_out += added_str[i : i + 1] + # The rest of the line + exp_console_out += test_str[insertion_point:] + # Reset the cursor back left + reset_dist = len(test_str[insertion_point:]) + exp_console_out += OutputStream.MoveCursorLeft(reset_dist) + + # Verify the console output. + CheckConsoleOutput(self, exp_console_out) + + def test_StoreCommandHistory(self): + """Verify that entered commands are stored in the history.""" + test_commands = [] + test_commands.append(b"help") + test_commands.append(b"version") + test_commands.append(b"accelread 0 1") + input_stream = [] + for c in test_commands: + input_stream.extend(BytesToByteList(c)) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # We expect to have the test commands in the history buffer. + exp_history_buf = test_commands + CheckHistoryBuffer(self, exp_history_buf) + + def test_CycleUpThruCommandHistory(self): + """Verify that the UP arrow key will print itmes in the history buffer.""" + # Enter some commands. + test_commands = [b"version", b"accelrange 0", b"battery", b"gettime"] + input_stream = [] + for command in test_commands: + input_stream.extend(BytesToByteList(command)) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Now, hit the UP arrow key to print the previous entries. + for i in range(len(test_commands)): + input_stream.extend(Keys.UP_ARROW) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # The expected output should be test commands with prompts printed in + # between, followed by line kills with the previous test commands printed. + exp_console_out = b"" + for i in range(len(test_commands)): + exp_console_out += test_commands[i] + b"\r\n" + self.console.prompt + + # When we press up, the line should be cleared and print the previous buffer + # entry. + for i in range(len(test_commands) - 1, 0, -1): + exp_console_out += test_commands[i] + # Backspace to the beginning. + for i in range(len(test_commands[i])): + exp_console_out += BACKSPACE_STRING + + # The last command should just be printed out with no backspacing. + exp_console_out += test_commands[0] + + # Now, verify. + CheckConsoleOutput(self, exp_console_out) + + def test_UpArrowOnEmptyHistory(self): + """Ensure nothing happens if the history is empty.""" + # Press the up arrow key twice. + input_stream = 2 * Keys.UP_ARROW + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # We expect nothing to have happened. + exp_console_out = b"" + exp_input_buffer = b"" + exp_input_buffer_pos = 0 + exp_history_buf = [] + + # Verify. + CheckConsoleOutput(self, exp_console_out) + CheckInputBufferPosition(self, exp_input_buffer_pos) + CheckInputBuffer(self, exp_input_buffer) + CheckHistoryBuffer(self, exp_history_buf) + + def test_UpArrowDoesNotGoOutOfBounds(self): + """Verify that pressing the up arrow many times won't go out of bounds.""" + # Enter one command. + test_str = b"help version" + input_stream = BytesToByteList(test_str) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + # Then press the up arrow key twice. + input_stream.extend(2 * Keys.UP_ARROW) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # Verify that the history buffer is correct. + exp_history_buf = [test_str] + CheckHistoryBuffer(self, exp_history_buf) + + # We expect that the console output should only contain our entered command, + # a new prompt, and then our command aggain. + exp_console_out = test_str + b"\r\n" + self.console.prompt + # Pressing up should reprint the command we entered. + exp_console_out += test_str + + # Verify. + CheckConsoleOutput(self, exp_console_out) + + def test_CycleDownThruCommandHistory(self): + """Verify that we can select entries by hitting the down arrow.""" + # Enter at least 4 commands. + test_commands = [b"version", b"accelrange 0", b"battery", b"gettime"] + input_stream = [] + for command in test_commands: + input_stream.extend(BytesToByteList(command)) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Now, hit the UP arrow key twice to print the previous two entries. + for i in range(2): + input_stream.extend(Keys.UP_ARROW) + + # Now, hit the DOWN arrow key twice to print the newer entries. + input_stream.extend(2 * Keys.DOWN_ARROW) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # The expected output should be commands that we entered, followed by + # prompts, then followed by our last two commands in reverse. Then, we + # should see the last entry in the list, followed by the saved partial cmd + # of a blank line. + exp_console_out = b"" + for i in range(len(test_commands)): + exp_console_out += test_commands[i] + b"\r\n" + self.console.prompt + + # When we press up, the line should be cleared and print the previous buffer + # entry. + for i in range(len(test_commands) - 1, 1, -1): + exp_console_out += test_commands[i] + # Backspace to the beginning. + for i in range(len(test_commands[i])): + exp_console_out += BACKSPACE_STRING + + # When we press down, it should have cleared the last command (which we + # covered with the previous for loop), and then prints the next command. + exp_console_out += test_commands[3] + for i in range(len(test_commands[3])): + exp_console_out += BACKSPACE_STRING + + # Verify console output. + CheckConsoleOutput(self, exp_console_out) + + # Verify input buffer. + exp_input_buffer = b"" # Empty because our partial command was empty. + exp_input_buffer_pos = len(exp_input_buffer) + CheckInputBuffer(self, exp_input_buffer) + CheckInputBufferPosition(self, exp_input_buffer_pos) + + def test_SavingPartialCommandWhenNavigatingHistory(self): + """Verify that partial commands are saved when navigating history.""" + # Enter a command. + test_str = b"accelinfo" + input_stream = BytesToByteList(test_str) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Enter a partial command. + partial_cmd = b"ver" + input_stream.extend(BytesToByteList(partial_cmd)) + + # Hit the UP arrow key. + input_stream.extend(Keys.UP_ARROW) + # Then, the DOWN arrow key. + input_stream.extend(Keys.DOWN_ARROW) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # The expected output should be the command we entered, a prompt, the + # partial command, clearing of the partial command, the command entered, + # clearing of the command entered, and then the partial command. + exp_console_out = test_str + b"\r\n" + self.console.prompt + exp_console_out += partial_cmd + for _ in range(len(partial_cmd)): + exp_console_out += BACKSPACE_STRING + exp_console_out += test_str + for _ in range(len(test_str)): + exp_console_out += BACKSPACE_STRING + exp_console_out += partial_cmd + + # Verify console output. + CheckConsoleOutput(self, exp_console_out) + + # Verify input buffer. + exp_input_buffer = partial_cmd + exp_input_buffer_pos = len(exp_input_buffer) + CheckInputBuffer(self, exp_input_buffer) + CheckInputBufferPosition(self, exp_input_buffer_pos) + + def test_DownArrowOnEmptyHistory(self): + """Ensure nothing happens if the history is empty.""" + # Then press the up down arrow twice. + input_stream = 2 * Keys.DOWN_ARROW + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # We expect nothing to have happened. + exp_console_out = b"" + exp_input_buffer = b"" + exp_input_buffer_pos = 0 + exp_history_buf = [] + + # Verify. + CheckConsoleOutput(self, exp_console_out) + CheckInputBufferPosition(self, exp_input_buffer_pos) + CheckInputBuffer(self, exp_input_buffer) + CheckHistoryBuffer(self, exp_history_buf) + + def test_DeleteCharsUsingDELKey(self): + """Verify that we can delete characters using the DEL key.""" + test_str = b"version" + input_stream = BytesToByteList(test_str) + + # Hit the left arrow key 2 times. + input_stream.extend(2 * Keys.LEFT_ARROW) + + # Press the DEL key. + input_stream.extend(Keys.DEL) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # The expected output should be the command we entered, 2 individual cursor + # moves to the left, and then removing a char and shifting everything to the + # left one column. + exp_console_out = test_str + exp_console_out += 2 * OutputStream.MoveCursorLeft(1) + + # Remove the char by shifting everything to the left one, slicing out the + # remove char. + exp_console_out += test_str[-1:] + b" " + + # Reset the cursor by moving back 2 columns because of the 'n' and space. + exp_console_out += OutputStream.MoveCursorLeft(2) + + # Verify console output. + CheckConsoleOutput(self, exp_console_out) + + # Verify input buffer. The input buffer should have the char sliced out and + # be positioned where the char was removed. + exp_input_buffer = test_str[:-2] + test_str[-1:] + exp_input_buffer_pos = len(exp_input_buffer) - 1 + CheckInputBuffer(self, exp_input_buffer) + CheckInputBufferPosition(self, exp_input_buffer_pos) + + def test_RepeatedCommandInHistory(self): + """Verify that we don't store 2 consecutive identical commands in history""" + # Enter a few commands. + test_commands = [b"version", b"accelrange 0", b"battery", b"gettime"] + # Repeat the last command. + test_commands.append(test_commands[len(test_commands) - 1]) + + input_stream = [] + for command in test_commands: + input_stream.extend(BytesToByteList(command)) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # Verify that the history buffer is correct. The last command, since + # it was repeated, should not have been added to the history. + exp_history_buf = test_commands[0 : len(test_commands) - 1] + CheckHistoryBuffer(self, exp_history_buf) - Args: - mock_check: A MagicMock object replacing the CheckForEnhancedECImage() - method. - """ - # Set the interrogation mode to always so that we actually interrogate. - self.console.interrogation_mode = b'always' - - # First, assume that the EC interrogations indicate an enhanced EC image. - mock_check.return_value = True - # But our current knowledge of the EC image (which was actually the - # 'previous' EC) was a non-enhanced image. - self.console.enhanced_ec = False - - test_command = b'sysinfo' - input_stream = [] - input_stream.extend(BytesToByteList(test_command)) - - expected_calls = [] - # All keystrokes to the console should be directed straight through to the - # EC until we press the enter key. - for char in test_command: - if six.PY3: - expected_calls.append(mock.call(bytes([char]))) - else: - expected_calls.append(mock.call(char)) - - # Press the enter key. - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - # The enter key should not be sent to the pipe since we should negotiate - # to an enhanced EC image. - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # At this point, we should have negotiated to enhanced. - self.assertTrue(self.console.enhanced_ec, msg=('Did not negotiate to ' - 'enhanced EC image.')) - - # The command would have been dropped however, so verify this... - CheckInputBuffer(self, b'') - CheckInputBufferPosition(self, 0) - # ...and repeat the command. - input_stream = BytesToByteList(test_command) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # Since we're enhanced now, we should have sent the entire command as one - # string with no trailing carriage return - expected_calls.append(mock.call(test_command)) - - # Verify all of the calls. - self.console.cmd_pipe.send.assert_has_calls(expected_calls) - - @mock.patch('ec3po.console.Console.CheckForEnhancedECImage') - def test_TransitionFromEnhancedToNonEnhanced(self, mock_check): - """Verify that we transition correctly to non-enhanced mode. - Args: - mock_check: A MagicMock object replacing the CheckForEnhancedECImage() - method. - """ - # Set the interrogation mode to always so that we actually interrogate. - self.console.interrogation_mode = b'always' - - # First, assume that the EC interrogations indicate an non-enhanced EC - # image. - mock_check.return_value = False - # But our current knowledge of the EC image (which was actually the - # 'previous' EC) was an enhanced image. - self.console.enhanced_ec = True - - test_command = b'sysinfo' - input_stream = [] - input_stream.extend(BytesToByteList(test_command)) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # But, we will negotiate to non-enhanced however, dropping this command. - # Verify this. - self.assertFalse(self.console.enhanced_ec, msg=('Did not negotiate to' - 'non-enhanced EC image.')) - CheckInputBuffer(self, b'') - CheckInputBufferPosition(self, 0) - - # The carriage return should have passed through though. - expected_calls = [] - expected_calls.append(mock.call( - six.int2byte(console.ControlKey.CARRIAGE_RETURN))) - - # Since the command was dropped, repeat the command. - input_stream = BytesToByteList(test_command) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # Since we're not enhanced now, we should have sent each character in the - # entire command separately and a carriage return. - for char in test_command: - if six.PY3: - expected_calls.append(mock.call(bytes([char]))) - else: - expected_calls.append(mock.call(char)) - expected_calls.append(mock.call( - six.int2byte(console.ControlKey.CARRIAGE_RETURN))) - - # Verify all of the calls. - self.console.cmd_pipe.send.assert_has_calls(expected_calls) - - def test_EnhancedCheckIfTimedOut(self): - """Verify that the check returns false if it times out.""" - # Make the debug pipe "time out". - self.console.dbg_pipe.poll.return_value = False - self.assertFalse(self.console.CheckForEnhancedECImage()) - - def test_EnhancedCheckIfACKReceived(self): - """Verify that the check returns true if the ACK is received.""" - # Make the debug pipe return EC_ACK. - self.console.dbg_pipe.poll.return_value = True - self.console.dbg_pipe.recv.return_value = interpreter.EC_ACK - self.assertTrue(self.console.CheckForEnhancedECImage()) - - def test_EnhancedCheckIfWrong(self): - """Verify that the check returns false if byte received is wrong.""" - # Make the debug pipe return the wrong byte. - self.console.dbg_pipe.poll.return_value = True - self.console.dbg_pipe.recv.return_value = b'\xff' - self.assertFalse(self.console.CheckForEnhancedECImage()) - - def test_EnhancedCheckUsingBuffer(self): - """Verify that given reboot output, enhanced EC images are detected.""" - enhanced_output_stream = b""" +class TestConsoleCompatibility(unittest.TestCase): + """Verify that console can speak to enhanced and non-enhanced EC images.""" + + def setUp(self): + """Setup the test harness.""" + # Setup logging with a timestamp, the module, and the log level. + logging.basicConfig( + level=logging.DEBUG, + format=("%(asctime)s - %(module)s -" " %(levelname)s - %(message)s"), + ) + # Create a temp file and set both the controller and peripheral PTYs to the + # file to create a loopback. + self.tempfile = tempfile.TemporaryFile() + + # Mock out the pipes. + mock_pipe_end_0, mock_pipe_end_1 = mock.MagicMock(), mock.MagicMock() + self.console = console.Console( + self.tempfile.fileno(), + self.tempfile, + tempfile.TemporaryFile(), + mock_pipe_end_0, + mock_pipe_end_1, + "EC", + ) + + @mock.patch("ec3po.console.Console.CheckForEnhancedECImage") + def test_ActAsPassThruInNonEnhancedMode(self, mock_check): + """Verify we simply pass everything thru to non-enhanced ECs. + + Args: + mock_check: A MagicMock object replacing the CheckForEnhancedECImage() + method. + """ + # Set the interrogation mode to always so that we actually interrogate. + self.console.interrogation_mode = b"always" + + # Assume EC interrogations indicate that the image is non-enhanced. + mock_check.return_value = False + + # Press enter, followed by the command, and another enter. + input_stream = [] + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + test_command = b"version" + input_stream.extend(BytesToByteList(test_command)) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # Expected calls to send down the pipe would be each character of the test + # command. + expected_calls = [] + expected_calls.append( + mock.call(six.int2byte(console.ControlKey.CARRIAGE_RETURN)) + ) + for char in test_command: + if six.PY3: + expected_calls.append(mock.call(bytes([char]))) + else: + expected_calls.append(mock.call(char)) + expected_calls.append( + mock.call(six.int2byte(console.ControlKey.CARRIAGE_RETURN)) + ) + + # Verify that the calls happened. + self.console.cmd_pipe.send.assert_has_calls(expected_calls) + + # Since we're acting as a pass-thru, the input buffer should be empty and + # input_buffer_pos is 0. + CheckInputBuffer(self, b"") + CheckInputBufferPosition(self, 0) + + @mock.patch("ec3po.console.Console.CheckForEnhancedECImage") + def test_TransitionFromNonEnhancedToEnhanced(self, mock_check): + """Verify that we transition correctly to enhanced mode. + + Args: + mock_check: A MagicMock object replacing the CheckForEnhancedECImage() + method. + """ + # Set the interrogation mode to always so that we actually interrogate. + self.console.interrogation_mode = b"always" + + # First, assume that the EC interrogations indicate an enhanced EC image. + mock_check.return_value = True + # But our current knowledge of the EC image (which was actually the + # 'previous' EC) was a non-enhanced image. + self.console.enhanced_ec = False + + test_command = b"sysinfo" + input_stream = [] + input_stream.extend(BytesToByteList(test_command)) + + expected_calls = [] + # All keystrokes to the console should be directed straight through to the + # EC until we press the enter key. + for char in test_command: + if six.PY3: + expected_calls.append(mock.call(bytes([char]))) + else: + expected_calls.append(mock.call(char)) + + # Press the enter key. + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + # The enter key should not be sent to the pipe since we should negotiate + # to an enhanced EC image. + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # At this point, we should have negotiated to enhanced. + self.assertTrue( + self.console.enhanced_ec, msg=("Did not negotiate to " "enhanced EC image.") + ) + + # The command would have been dropped however, so verify this... + CheckInputBuffer(self, b"") + CheckInputBufferPosition(self, 0) + # ...and repeat the command. + input_stream = BytesToByteList(test_command) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # Since we're enhanced now, we should have sent the entire command as one + # string with no trailing carriage return + expected_calls.append(mock.call(test_command)) + + # Verify all of the calls. + self.console.cmd_pipe.send.assert_has_calls(expected_calls) + + @mock.patch("ec3po.console.Console.CheckForEnhancedECImage") + def test_TransitionFromEnhancedToNonEnhanced(self, mock_check): + """Verify that we transition correctly to non-enhanced mode. + + Args: + mock_check: A MagicMock object replacing the CheckForEnhancedECImage() + method. + """ + # Set the interrogation mode to always so that we actually interrogate. + self.console.interrogation_mode = b"always" + + # First, assume that the EC interrogations indicate an non-enhanced EC + # image. + mock_check.return_value = False + # But our current knowledge of the EC image (which was actually the + # 'previous' EC) was an enhanced image. + self.console.enhanced_ec = True + + test_command = b"sysinfo" + input_stream = [] + input_stream.extend(BytesToByteList(test_command)) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # But, we will negotiate to non-enhanced however, dropping this command. + # Verify this. + self.assertFalse( + self.console.enhanced_ec, + msg=("Did not negotiate to" "non-enhanced EC image."), + ) + CheckInputBuffer(self, b"") + CheckInputBufferPosition(self, 0) + + # The carriage return should have passed through though. + expected_calls = [] + expected_calls.append( + mock.call(six.int2byte(console.ControlKey.CARRIAGE_RETURN)) + ) + + # Since the command was dropped, repeat the command. + input_stream = BytesToByteList(test_command) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # Since we're not enhanced now, we should have sent each character in the + # entire command separately and a carriage return. + for char in test_command: + if six.PY3: + expected_calls.append(mock.call(bytes([char]))) + else: + expected_calls.append(mock.call(char)) + expected_calls.append( + mock.call(six.int2byte(console.ControlKey.CARRIAGE_RETURN)) + ) + + # Verify all of the calls. + self.console.cmd_pipe.send.assert_has_calls(expected_calls) + + def test_EnhancedCheckIfTimedOut(self): + """Verify that the check returns false if it times out.""" + # Make the debug pipe "time out". + self.console.dbg_pipe.poll.return_value = False + self.assertFalse(self.console.CheckForEnhancedECImage()) + + def test_EnhancedCheckIfACKReceived(self): + """Verify that the check returns true if the ACK is received.""" + # Make the debug pipe return EC_ACK. + self.console.dbg_pipe.poll.return_value = True + self.console.dbg_pipe.recv.return_value = interpreter.EC_ACK + self.assertTrue(self.console.CheckForEnhancedECImage()) + + def test_EnhancedCheckIfWrong(self): + """Verify that the check returns false if byte received is wrong.""" + # Make the debug pipe return the wrong byte. + self.console.dbg_pipe.poll.return_value = True + self.console.dbg_pipe.recv.return_value = b"\xff" + self.assertFalse(self.console.CheckForEnhancedECImage()) + + def test_EnhancedCheckUsingBuffer(self): + """Verify that given reboot output, enhanced EC images are detected.""" + enhanced_output_stream = b""" --- UART initialized after reboot --- [Reset cause: reset-pin soft] [Image: RO, jerry_v1.1.4363-2af8572-dirty 2016-02-23 13:26:20 aaboagye@lithium.mtv.corp.google.com] @@ -1295,19 +1343,19 @@ Enhanced Console is enabled (v1.0.0); type HELP for help. [0.224060 hash done 41dac382e3a6e3d2ea5b4d789c1bc46525cae7cc5ff6758f0de8d8369b506f57] [0.375150 POWER_GOOD seen] """ - for line in enhanced_output_stream.split(b'\n'): - self.console.CheckBufferForEnhancedImage(line) + for line in enhanced_output_stream.split(b"\n"): + self.console.CheckBufferForEnhancedImage(line) - # Since the enhanced console string was present in the output, the console - # should have caught it. - self.assertTrue(self.console.enhanced_ec) + # Since the enhanced console string was present in the output, the console + # should have caught it. + self.assertTrue(self.console.enhanced_ec) - # Also should check that the command was sent to the interpreter. - self.console.cmd_pipe.send.assert_called_once_with(b'enhanced True') + # Also should check that the command was sent to the interpreter. + self.console.cmd_pipe.send.assert_called_once_with(b"enhanced True") - # Now test the non-enhanced EC image. - self.console.cmd_pipe.reset_mock() - non_enhanced_output_stream = b""" + # Now test the non-enhanced EC image. + self.console.cmd_pipe.reset_mock() + non_enhanced_output_stream = b""" --- UART initialized after reboot --- [Reset cause: reset-pin soft] [Image: RO, jerry_v1.1.4363-2af8572-dirty 2016-02-23 13:03:15 aaboagye@lithium.mtv.corp.google.com] @@ -1331,239 +1379,253 @@ Console is enabled; type HELP for help. [0.010285 power on 2] [0.010385 power state 5 = S5->S3, in 0x0000] """ - for line in non_enhanced_output_stream.split(b'\n'): - self.console.CheckBufferForEnhancedImage(line) + for line in non_enhanced_output_stream.split(b"\n"): + self.console.CheckBufferForEnhancedImage(line) - # Since the default console string is present in the output, it should be - # determined to be non enhanced now. - self.assertFalse(self.console.enhanced_ec) + # Since the default console string is present in the output, it should be + # determined to be non enhanced now. + self.assertFalse(self.console.enhanced_ec) - # Check that command was also sent to the interpreter. - self.console.cmd_pipe.send.assert_called_once_with(b'enhanced False') + # Check that command was also sent to the interpreter. + self.console.cmd_pipe.send.assert_called_once_with(b"enhanced False") class TestOOBMConsoleCommands(unittest.TestCase): - """Verify that OOBM console commands work correctly.""" - def setUp(self): - """Setup the test harness.""" - # Setup logging with a timestamp, the module, and the log level. - logging.basicConfig(level=logging.DEBUG, - format=('%(asctime)s - %(module)s -' - ' %(levelname)s - %(message)s')) - # Create a temp file and set both the controller and peripheral PTYs to the - # file to create a loopback. - self.tempfile = tempfile.TemporaryFile() - - # Mock out the pipes. - mock_pipe_end_0, mock_pipe_end_1 = mock.MagicMock(), mock.MagicMock() - self.console = console.Console(self.tempfile.fileno(), self.tempfile, - tempfile.TemporaryFile(), - mock_pipe_end_0, mock_pipe_end_1, "EC") - self.console.oobm_queue = mock.MagicMock() - - @mock.patch('ec3po.console.Console.CheckForEnhancedECImage') - def test_InterrogateCommand(self, mock_check): - """Verify that 'interrogate' command works as expected. - - Args: - mock_check: A MagicMock object replacing the CheckForEnhancedECIMage() - method. - """ - input_stream = [] - expected_calls = [] - mock_check.side_effect = [False] - - # 'interrogate never' should disable the interrogation from happening at - # all. - cmd = b'interrogate never' - # Enter the OOBM prompt. - input_stream.extend(BytesToByteList(b'%')) - # Type the command - input_stream.extend(BytesToByteList(cmd)) - # Press enter. - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - input_stream = [] - - # The OOBM queue should have been called with the command being put. - expected_calls.append(mock.call.put(cmd)) - self.console.oobm_queue.assert_has_calls(expected_calls) - - # Process the OOBM queue. - self.console.oobm_queue.get.side_effect = [cmd] - self.console.ProcessOOBMQueue() - - # Type out a few commands. - input_stream.extend(BytesToByteList(b'version')) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - input_stream.extend(BytesToByteList(b'flashinfo')) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - input_stream.extend(BytesToByteList(b'sysinfo')) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # The Check function should NOT have been called at all. - mock_check.assert_not_called() - - # The EC image should be assumed to be not enhanced. - self.assertFalse(self.console.enhanced_ec, 'The image should be assumed to' - ' be NOT enhanced.') - - # Reset the mocks. - mock_check.reset_mock() - self.console.oobm_queue.reset_mock() - - # 'interrogate auto' should not interrogate at all. It should only be - # scanning the output stream for the 'console is enabled' strings. - cmd = b'interrogate auto' - # Enter the OOBM prompt. - input_stream.extend(BytesToByteList(b'%')) - # Type the command - input_stream.extend(BytesToByteList(cmd)) - # Press enter. - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - input_stream = [] - expected_calls = [] - - # The OOBM queue should have been called with the command being put. - expected_calls.append(mock.call.put(cmd)) - self.console.oobm_queue.assert_has_calls(expected_calls) - - # Process the OOBM queue. - self.console.oobm_queue.get.side_effect = [cmd] - self.console.ProcessOOBMQueue() - - # Type out a few commands. - input_stream.extend(BytesToByteList(b'version')) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - input_stream.extend(BytesToByteList(b'flashinfo')) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - input_stream.extend(BytesToByteList(b'sysinfo')) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # The Check function should NOT have been called at all. - mock_check.assert_not_called() - - # The EC image should be assumed to be not enhanced. - self.assertFalse(self.console.enhanced_ec, 'The image should be assumed to' - ' be NOT enhanced.') - - # Reset the mocks. - mock_check.reset_mock() - self.console.oobm_queue.reset_mock() - - # 'interrogate always' should, like its name implies, interrogate always - # after each press of the enter key. This was the former way of doing - # interrogation. - cmd = b'interrogate always' - # Enter the OOBM prompt. - input_stream.extend(BytesToByteList(b'%')) - # Type the command - input_stream.extend(BytesToByteList(cmd)) - # Press enter. - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - input_stream = [] - expected_calls = [] - - # The OOBM queue should have been called with the command being put. - expected_calls.append(mock.call.put(cmd)) - self.console.oobm_queue.assert_has_calls(expected_calls) - - # Process the OOBM queue. - self.console.oobm_queue.get.side_effect = [cmd] - self.console.ProcessOOBMQueue() - - # The Check method should be called 3 times here. - mock_check.side_effect = [False, False, False] - - # Type out a few commands. - input_stream.extend(BytesToByteList(b'help list')) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - input_stream.extend(BytesToByteList(b'taskinfo')) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - input_stream.extend(BytesToByteList(b'hibdelay')) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # The Check method should have been called 3 times here. - expected_calls = [mock.call(), mock.call(), mock.call()] - mock_check.assert_has_calls(expected_calls) - - # The EC image should be assumed to be not enhanced. - self.assertFalse(self.console.enhanced_ec, 'The image should be assumed to' - ' be NOT enhanced.') - - # Now, let's try to assume that the image is enhanced while still disabling - # interrogation. - mock_check.reset_mock() - self.console.oobm_queue.reset_mock() - input_stream = [] - cmd = b'interrogate never enhanced' - # Enter the OOBM prompt. - input_stream.extend(BytesToByteList(b'%')) - # Type the command - input_stream.extend(BytesToByteList(cmd)) - # Press enter. - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - input_stream = [] - expected_calls = [] - - # The OOBM queue should have been called with the command being put. - expected_calls.append(mock.call.put(cmd)) - self.console.oobm_queue.assert_has_calls(expected_calls) - - # Process the OOBM queue. - self.console.oobm_queue.get.side_effect = [cmd] - self.console.ProcessOOBMQueue() - - # Type out a few commands. - input_stream.extend(BytesToByteList(b'chgstate')) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - input_stream.extend(BytesToByteList(b'hash')) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - input_stream.extend(BytesToByteList(b'sysjump rw')) - input_stream.append(console.ControlKey.CARRIAGE_RETURN) - - # Send the sequence out. - for byte in input_stream: - self.console.HandleChar(byte) - - # The check method should have never been called. - mock_check.assert_not_called() - - # The EC image should be assumed to be enhanced. - self.assertTrue(self.console.enhanced_ec, 'The image should be' - ' assumed to be enhanced.') - - -if __name__ == '__main__': - unittest.main() + """Verify that OOBM console commands work correctly.""" + + def setUp(self): + """Setup the test harness.""" + # Setup logging with a timestamp, the module, and the log level. + logging.basicConfig( + level=logging.DEBUG, + format=("%(asctime)s - %(module)s -" " %(levelname)s - %(message)s"), + ) + # Create a temp file and set both the controller and peripheral PTYs to the + # file to create a loopback. + self.tempfile = tempfile.TemporaryFile() + + # Mock out the pipes. + mock_pipe_end_0, mock_pipe_end_1 = mock.MagicMock(), mock.MagicMock() + self.console = console.Console( + self.tempfile.fileno(), + self.tempfile, + tempfile.TemporaryFile(), + mock_pipe_end_0, + mock_pipe_end_1, + "EC", + ) + self.console.oobm_queue = mock.MagicMock() + + @mock.patch("ec3po.console.Console.CheckForEnhancedECImage") + def test_InterrogateCommand(self, mock_check): + """Verify that 'interrogate' command works as expected. + + Args: + mock_check: A MagicMock object replacing the CheckForEnhancedECIMage() + method. + """ + input_stream = [] + expected_calls = [] + mock_check.side_effect = [False] + + # 'interrogate never' should disable the interrogation from happening at + # all. + cmd = b"interrogate never" + # Enter the OOBM prompt. + input_stream.extend(BytesToByteList(b"%")) + # Type the command + input_stream.extend(BytesToByteList(cmd)) + # Press enter. + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + input_stream = [] + + # The OOBM queue should have been called with the command being put. + expected_calls.append(mock.call.put(cmd)) + self.console.oobm_queue.assert_has_calls(expected_calls) + + # Process the OOBM queue. + self.console.oobm_queue.get.side_effect = [cmd] + self.console.ProcessOOBMQueue() + + # Type out a few commands. + input_stream.extend(BytesToByteList(b"version")) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + input_stream.extend(BytesToByteList(b"flashinfo")) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + input_stream.extend(BytesToByteList(b"sysinfo")) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # The Check function should NOT have been called at all. + mock_check.assert_not_called() + + # The EC image should be assumed to be not enhanced. + self.assertFalse( + self.console.enhanced_ec, + "The image should be assumed to" " be NOT enhanced.", + ) + + # Reset the mocks. + mock_check.reset_mock() + self.console.oobm_queue.reset_mock() + + # 'interrogate auto' should not interrogate at all. It should only be + # scanning the output stream for the 'console is enabled' strings. + cmd = b"interrogate auto" + # Enter the OOBM prompt. + input_stream.extend(BytesToByteList(b"%")) + # Type the command + input_stream.extend(BytesToByteList(cmd)) + # Press enter. + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + input_stream = [] + expected_calls = [] + + # The OOBM queue should have been called with the command being put. + expected_calls.append(mock.call.put(cmd)) + self.console.oobm_queue.assert_has_calls(expected_calls) + + # Process the OOBM queue. + self.console.oobm_queue.get.side_effect = [cmd] + self.console.ProcessOOBMQueue() + + # Type out a few commands. + input_stream.extend(BytesToByteList(b"version")) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + input_stream.extend(BytesToByteList(b"flashinfo")) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + input_stream.extend(BytesToByteList(b"sysinfo")) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # The Check function should NOT have been called at all. + mock_check.assert_not_called() + + # The EC image should be assumed to be not enhanced. + self.assertFalse( + self.console.enhanced_ec, + "The image should be assumed to" " be NOT enhanced.", + ) + + # Reset the mocks. + mock_check.reset_mock() + self.console.oobm_queue.reset_mock() + + # 'interrogate always' should, like its name implies, interrogate always + # after each press of the enter key. This was the former way of doing + # interrogation. + cmd = b"interrogate always" + # Enter the OOBM prompt. + input_stream.extend(BytesToByteList(b"%")) + # Type the command + input_stream.extend(BytesToByteList(cmd)) + # Press enter. + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + input_stream = [] + expected_calls = [] + + # The OOBM queue should have been called with the command being put. + expected_calls.append(mock.call.put(cmd)) + self.console.oobm_queue.assert_has_calls(expected_calls) + + # Process the OOBM queue. + self.console.oobm_queue.get.side_effect = [cmd] + self.console.ProcessOOBMQueue() + + # The Check method should be called 3 times here. + mock_check.side_effect = [False, False, False] + + # Type out a few commands. + input_stream.extend(BytesToByteList(b"help list")) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + input_stream.extend(BytesToByteList(b"taskinfo")) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + input_stream.extend(BytesToByteList(b"hibdelay")) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # The Check method should have been called 3 times here. + expected_calls = [mock.call(), mock.call(), mock.call()] + mock_check.assert_has_calls(expected_calls) + + # The EC image should be assumed to be not enhanced. + self.assertFalse( + self.console.enhanced_ec, + "The image should be assumed to" " be NOT enhanced.", + ) + + # Now, let's try to assume that the image is enhanced while still disabling + # interrogation. + mock_check.reset_mock() + self.console.oobm_queue.reset_mock() + input_stream = [] + cmd = b"interrogate never enhanced" + # Enter the OOBM prompt. + input_stream.extend(BytesToByteList(b"%")) + # Type the command + input_stream.extend(BytesToByteList(cmd)) + # Press enter. + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + input_stream = [] + expected_calls = [] + + # The OOBM queue should have been called with the command being put. + expected_calls.append(mock.call.put(cmd)) + self.console.oobm_queue.assert_has_calls(expected_calls) + + # Process the OOBM queue. + self.console.oobm_queue.get.side_effect = [cmd] + self.console.ProcessOOBMQueue() + + # Type out a few commands. + input_stream.extend(BytesToByteList(b"chgstate")) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + input_stream.extend(BytesToByteList(b"hash")) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + input_stream.extend(BytesToByteList(b"sysjump rw")) + input_stream.append(console.ControlKey.CARRIAGE_RETURN) + + # Send the sequence out. + for byte in input_stream: + self.console.HandleChar(byte) + + # The check method should have never been called. + mock_check.assert_not_called() + + # The EC image should be assumed to be enhanced. + self.assertTrue( + self.console.enhanced_ec, "The image should be" " assumed to be enhanced." + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/util/ec3po/interpreter.py b/util/ec3po/interpreter.py index 4e151083bd..591603e038 100644 --- a/util/ec3po/interpreter.py +++ b/util/ec3po/interpreter.py @@ -25,443 +25,447 @@ import traceback import six - COMMAND_RETRIES = 3 # Number of attempts to retry a command. EC_MAX_READ = 1024 # Max bytes to read at a time from the EC. -EC_SYN = b'\xec' # Byte indicating EC interrogation. -EC_ACK = b'\xc0' # Byte representing correct EC response to interrogation. +EC_SYN = b"\xec" # Byte indicating EC interrogation. +EC_ACK = b"\xc0" # Byte representing correct EC response to interrogation. class LoggerAdapter(logging.LoggerAdapter): - """Class which provides a small adapter for the logger.""" + """Class which provides a small adapter for the logger.""" - def process(self, msg, kwargs): - """Prepends the served PTY to the beginning of the log message.""" - return '%s - %s' % (self.extra['pty'], msg), kwargs + def process(self, msg, kwargs): + """Prepends the served PTY to the beginning of the log message.""" + return "%s - %s" % (self.extra["pty"], msg), kwargs class Interpreter(object): - """Class which provides the interpretation layer between the EC and user. - - This class essentially performs all of the intepretation for the EC and the - user. It handles all of the automatic command retrying as well as the - formation of commands for EC images which support that. - - Attributes: - logger: A logger for this module. - ec_uart_pty: An opened file object to the raw EC UART PTY. - ec_uart_pty_name: A string containing the name of the raw EC UART PTY. - cmd_pipe: A socket.socket or multiprocessing.Connection object which - represents the Interpreter side of the command pipe. This must be a - bidirectional pipe. Commands and responses will utilize this pipe. - dbg_pipe: A socket.socket or multiprocessing.Connection object which - represents the Interpreter side of the debug pipe. This must be a - unidirectional pipe with write capabilities. EC debug output will utilize - this pipe. - cmd_retries: An integer representing the number of attempts the console - should retry commands if it receives an error. - log_level: An integer representing the numeric value of the log level. - inputs: A list of objects that the intpreter selects for reading. - Initially, these are the EC UART and the command pipe. - outputs: A list of objects that the interpreter selects for writing. - ec_cmd_queue: A FIFO queue used for sending commands down to the EC UART. - last_cmd: A string that represents the last command sent to the EC. If an - error is encountered, the interpreter will attempt to retry this command - up to COMMAND_RETRIES. - enhanced_ec: A boolean indicating if the EC image that we are currently - communicating with is enhanced or not. Enhanced EC images will support - packed commands and host commands over the UART. This defaults to False - and is changed depending on the result of an interrogation. - interrogating: A boolean indicating if we are in the middle of interrogating - the EC. - connected: A boolean indicating if the interpreter is actually connected to - the UART and listening. - """ - def __init__(self, ec_uart_pty, cmd_pipe, dbg_pipe, log_level=logging.INFO, - name=None): - """Intializes an Interpreter object with the provided args. + """Class which provides the interpretation layer between the EC and user. - Args: - ec_uart_pty: A string representing the EC UART to connect to. + This class essentially performs all of the intepretation for the EC and the + user. It handles all of the automatic command retrying as well as the + formation of commands for EC images which support that. + + Attributes: + logger: A logger for this module. + ec_uart_pty: An opened file object to the raw EC UART PTY. + ec_uart_pty_name: A string containing the name of the raw EC UART PTY. cmd_pipe: A socket.socket or multiprocessing.Connection object which represents the Interpreter side of the command pipe. This must be a bidirectional pipe. Commands and responses will utilize this pipe. dbg_pipe: A socket.socket or multiprocessing.Connection object which represents the Interpreter side of the debug pipe. This must be a - unidirectional pipe with write capabilities. EC debug output will - utilize this pipe. + unidirectional pipe with write capabilities. EC debug output will utilize + this pipe. cmd_retries: An integer representing the number of attempts the console should retry commands if it receives an error. - log_level: An optional integer representing the numeric value of the log - level. By default, the log level will be logging.INFO (20). - name: the console source name - """ - # Create a unique logger based on the interpreter name - interpreter_prefix = ('%s - ' % name) if name else '' - logger = logging.getLogger('%sEC3PO.Interpreter' % interpreter_prefix) - self.logger = LoggerAdapter(logger, {'pty': ec_uart_pty}) - # TODO(https://crbug.com/1162189): revist the 2 TODOs below - # TODO(https://bugs.python.org/issue27805, python3.7+): revert to ab+ - # TODO(https://bugs.python.org/issue20074): removing buffering=0 if/when - # that gets fixed, or keep two pty: one for reading and one for writing - self.ec_uart_pty = open(ec_uart_pty, 'r+b', buffering=0) - self.ec_uart_pty_name = ec_uart_pty - self.cmd_pipe = cmd_pipe - self.dbg_pipe = dbg_pipe - self.cmd_retries = COMMAND_RETRIES - self.log_level = log_level - self.inputs = [self.ec_uart_pty, self.cmd_pipe] - self.outputs = [] - self.ec_cmd_queue = six.moves.queue.Queue() - self.last_cmd = b'' - self.enhanced_ec = False - self.interrogating = False - self.connected = True - - def __str__(self): - """Show internal state of the Interpreter object. - - Returns: - A string that shows the values of the attributes. - """ - string = [] - string.append('%r' % self) - string.append('ec_uart_pty: %s' % self.ec_uart_pty) - string.append('cmd_pipe: %r' % self.cmd_pipe) - string.append('dbg_pipe: %r' % self.dbg_pipe) - string.append('cmd_retries: %d' % self.cmd_retries) - string.append('log_level: %d' % self.log_level) - string.append('inputs: %r' % self.inputs) - string.append('outputs: %r' % self.outputs) - string.append('ec_cmd_queue: %r' % self.ec_cmd_queue) - string.append('last_cmd: \'%s\'' % self.last_cmd) - string.append('enhanced_ec: %r' % self.enhanced_ec) - string.append('interrogating: %r' % self.interrogating) - return '\n'.join(string) - - def EnqueueCmd(self, command): - """Enqueue a command to be sent to the EC UART. - - Args: - command: A string which contains the command to be sent. + log_level: An integer representing the numeric value of the log level. + inputs: A list of objects that the intpreter selects for reading. + Initially, these are the EC UART and the command pipe. + outputs: A list of objects that the interpreter selects for writing. + ec_cmd_queue: A FIFO queue used for sending commands down to the EC UART. + last_cmd: A string that represents the last command sent to the EC. If an + error is encountered, the interpreter will attempt to retry this command + up to COMMAND_RETRIES. + enhanced_ec: A boolean indicating if the EC image that we are currently + communicating with is enhanced or not. Enhanced EC images will support + packed commands and host commands over the UART. This defaults to False + and is changed depending on the result of an interrogation. + interrogating: A boolean indicating if we are in the middle of interrogating + the EC. + connected: A boolean indicating if the interpreter is actually connected to + the UART and listening. """ - self.ec_cmd_queue.put(command) - self.logger.log(1, 'Commands now in queue: %d', self.ec_cmd_queue.qsize()) - # Add the EC UART as an output to be serviced. - if self.connected and self.ec_uart_pty not in self.outputs: - self.outputs.append(self.ec_uart_pty) - - def PackCommand(self, raw_cmd): - r"""Packs a command for use with error checking. - - For error checking, we pack console commands in a particular format. The - format is as follows: - - &&[x][x][x][x]&{cmd}\n\n - ^ ^ ^^ ^^ ^ ^-- 2 newlines. - | | || || |-- the raw console command. - | | || ||-- 1 ampersand. - | | ||____|--- 2 hex digits representing the CRC8 of cmd. - | |____|-- 2 hex digits reprsenting the length of cmd. - |-- 2 ampersands - - Args: - raw_cmd: A pre-packed string which contains the raw command. - - Returns: - A string which contains the packed command. - """ - # Don't pack a single carriage return. - if raw_cmd != b'\r': - # The command format is as follows. - # &&[x][x][x][x]&{cmd}\n\n - packed_cmd = [] - packed_cmd.append(b'&&') - # The first pair of hex digits are the length of the command. - packed_cmd.append(b'%02x' % len(raw_cmd)) - # Then the CRC8 of cmd. - packed_cmd.append(b'%02x' % Crc8(raw_cmd)) - packed_cmd.append(b'&') - # Now, the raw command followed by 2 newlines. - packed_cmd.append(raw_cmd) - packed_cmd.append(b'\n\n') - return b''.join(packed_cmd) - else: - return raw_cmd - - def ProcessCommand(self, command): - """Captures the input determines what actions to take. - - Args: - command: A string representing the command sent by the user. - """ - if command == b'disconnect': - if self.connected: - self.logger.debug('UART disconnect request.') - # Drop all pending commands if any. - while not self.ec_cmd_queue.empty(): - c = self.ec_cmd_queue.get() - self.logger.debug('dropped: \'%s\'', c) - if self.enhanced_ec: - # Reset retry state. - self.cmd_retries = COMMAND_RETRIES - self.last_cmd = b'' - # Get the UART that the interpreter is attached to. - fileobj = self.ec_uart_pty - self.logger.debug('fileobj: %r', fileobj) - # Remove the descriptor from the inputs and outputs. - self.inputs.remove(fileobj) - if fileobj in self.outputs: - self.outputs.remove(fileobj) - self.logger.debug('Removed fileobj. Remaining inputs: %r', self.inputs) - # Close the file. - fileobj.close() - # Mark the interpreter as disconnected now. - self.connected = False - self.logger.debug('Disconnected from %s.', self.ec_uart_pty_name) - return - - elif command == b'reconnect': - if not self.connected: - self.logger.debug('UART reconnect request.') - # Reopen the PTY. + def __init__( + self, ec_uart_pty, cmd_pipe, dbg_pipe, log_level=logging.INFO, name=None + ): + """Intializes an Interpreter object with the provided args. + + Args: + ec_uart_pty: A string representing the EC UART to connect to. + cmd_pipe: A socket.socket or multiprocessing.Connection object which + represents the Interpreter side of the command pipe. This must be a + bidirectional pipe. Commands and responses will utilize this pipe. + dbg_pipe: A socket.socket or multiprocessing.Connection object which + represents the Interpreter side of the debug pipe. This must be a + unidirectional pipe with write capabilities. EC debug output will + utilize this pipe. + cmd_retries: An integer representing the number of attempts the console + should retry commands if it receives an error. + log_level: An optional integer representing the numeric value of the log + level. By default, the log level will be logging.INFO (20). + name: the console source name + """ + # Create a unique logger based on the interpreter name + interpreter_prefix = ("%s - " % name) if name else "" + logger = logging.getLogger("%sEC3PO.Interpreter" % interpreter_prefix) + self.logger = LoggerAdapter(logger, {"pty": ec_uart_pty}) + # TODO(https://crbug.com/1162189): revist the 2 TODOs below # TODO(https://bugs.python.org/issue27805, python3.7+): revert to ab+ # TODO(https://bugs.python.org/issue20074): removing buffering=0 if/when # that gets fixed, or keep two pty: one for reading and one for writing - fileobj = open(self.ec_uart_pty_name, 'r+b', buffering=0) - self.logger.debug('fileobj: %r', fileobj) - self.ec_uart_pty = fileobj - # Add the descriptor to the inputs. - self.inputs.append(fileobj) - self.logger.debug('fileobj added. curr inputs: %r', self.inputs) - # Mark the interpreter as connected now. - self.connected = True - self.logger.debug('Connected to %s.', self.ec_uart_pty_name) - return - - elif command.startswith(b'enhanced'): - self.enhanced_ec = command.split(b' ')[1] == b'True' - return - - # Ignore any other commands while in the disconnected state. - self.logger.log(1, 'command: \'%s\'', command) - if not self.connected: - self.logger.debug('Ignoring command because currently disconnected.') - return - - # Remove leading and trailing spaces only if this is an enhanced EC image. - # For non-enhanced EC images, commands will be single characters at a time - # and can be spaces. - if self.enhanced_ec: - command = command.strip(b' ') - - # There's nothing to do if the command is empty. - if len(command) == 0: - return - - # Handle log level change requests. - if command.startswith(b'loglevel'): - self.logger.debug('Log level change request.') - new_log_level = int(command.split(b' ')[1]) - self.logger.logger.setLevel(new_log_level) - self.logger.info('Log level changed to %d.', new_log_level) - return - - # Check for interrogation command. - if command == EC_SYN: - # User is requesting interrogation. Send SYN as is. - self.logger.debug('User requesting interrogation.') - self.interrogating = True - # Assume the EC isn't enhanced until we get a response. - self.enhanced_ec = False - elif self.enhanced_ec: - # Enhanced EC images require the plaintext commands to be packed. - command = self.PackCommand(command) - # TODO(aaboagye): Make a dict of commands and keys and eventually, - # handle partial matching based on unique prefixes. - - self.EnqueueCmd(command) - - def HandleCmdRetries(self): - """Attempts to retry commands if possible.""" - if self.cmd_retries > 0: - # The EC encountered an error. We'll have to retry again. - self.logger.warning('Retrying command...') - self.cmd_retries -= 1 - self.logger.warning('Retries remaining: %d', self.cmd_retries) - # Retry the command and add the EC UART to the writers again. - self.EnqueueCmd(self.last_cmd) - self.outputs.append(self.ec_uart_pty) - else: - # We're out of retries, so just give up. - self.logger.error('Command failed. No retries left.') - # Clear the command in progress. - self.last_cmd = b'' - # Reset the retry count. - self.cmd_retries = COMMAND_RETRIES - - def SendCmdToEC(self): - """Sends a command to the EC.""" - # If we're retrying a command, just try to send it again. - if self.cmd_retries < COMMAND_RETRIES: - cmd = self.last_cmd - else: - # If we're not retrying, we should not be writing to the EC if we have no - # items in our command queue. - assert not self.ec_cmd_queue.empty() - # Get the command to send. - cmd = self.ec_cmd_queue.get() - - # Send the command. - self.ec_uart_pty.write(cmd) - self.ec_uart_pty.flush() - self.logger.log(1, 'Sent command to EC.') - - if self.enhanced_ec and cmd != EC_SYN: - # Now, that we've sent the command, store the current command as the last - # command sent. If we encounter an error string, we will attempt to retry - # this command. - if cmd != self.last_cmd: - self.last_cmd = cmd - # Reset the retry count. + self.ec_uart_pty = open(ec_uart_pty, "r+b", buffering=0) + self.ec_uart_pty_name = ec_uart_pty + self.cmd_pipe = cmd_pipe + self.dbg_pipe = dbg_pipe self.cmd_retries = COMMAND_RETRIES + self.log_level = log_level + self.inputs = [self.ec_uart_pty, self.cmd_pipe] + self.outputs = [] + self.ec_cmd_queue = six.moves.queue.Queue() + self.last_cmd = b"" + self.enhanced_ec = False + self.interrogating = False + self.connected = True - # If no command is pending to be sent, then we can remove the EC UART from - # writers. Might need better checking for command retry logic in here. - if self.ec_cmd_queue.empty(): - # Remove the EC UART from the writers while we wait for a response. - self.logger.debug('Removing EC UART from writers.') - self.outputs.remove(self.ec_uart_pty) - - def HandleECData(self): - """Handle any debug prints from the EC.""" - self.logger.log(1, 'EC has data') - # Read what the EC sent us. - data = os.read(self.ec_uart_pty.fileno(), EC_MAX_READ) - self.logger.log(1, 'got: \'%s\'', binascii.hexlify(data)) - if b'&E' in data and self.enhanced_ec: - # We received an error, so we should retry it if possible. - self.logger.warning('Error string found in data.') - self.HandleCmdRetries() - return - - # If we were interrogating, check the response and update our knowledge - # of the current EC image. - if self.interrogating: - self.enhanced_ec = data == EC_ACK - if self.enhanced_ec: - self.logger.debug('The current EC image seems enhanced.') - else: - self.logger.debug('The current EC image does NOT seem enhanced.') - # Done interrogating. - self.interrogating = False - # For now, just forward everything the EC sends us. - self.logger.log(1, 'Forwarding to user...') - self.dbg_pipe.send(data) - - def HandleUserData(self): - """Handle any incoming commands from the user. - - Raises: - EOFError: Allowed to propagate through from self.cmd_pipe.recv(). - """ - self.logger.log(1, 'Command data available. Begin processing.') - data = self.cmd_pipe.recv() - # Process the command. - self.ProcessCommand(data) + def __str__(self): + """Show internal state of the Interpreter object. + + Returns: + A string that shows the values of the attributes. + """ + string = [] + string.append("%r" % self) + string.append("ec_uart_pty: %s" % self.ec_uart_pty) + string.append("cmd_pipe: %r" % self.cmd_pipe) + string.append("dbg_pipe: %r" % self.dbg_pipe) + string.append("cmd_retries: %d" % self.cmd_retries) + string.append("log_level: %d" % self.log_level) + string.append("inputs: %r" % self.inputs) + string.append("outputs: %r" % self.outputs) + string.append("ec_cmd_queue: %r" % self.ec_cmd_queue) + string.append("last_cmd: '%s'" % self.last_cmd) + string.append("enhanced_ec: %r" % self.enhanced_ec) + string.append("interrogating: %r" % self.interrogating) + return "\n".join(string) + + def EnqueueCmd(self, command): + """Enqueue a command to be sent to the EC UART. + + Args: + command: A string which contains the command to be sent. + """ + self.ec_cmd_queue.put(command) + self.logger.log(1, "Commands now in queue: %d", self.ec_cmd_queue.qsize()) + + # Add the EC UART as an output to be serviced. + if self.connected and self.ec_uart_pty not in self.outputs: + self.outputs.append(self.ec_uart_pty) + + def PackCommand(self, raw_cmd): + r"""Packs a command for use with error checking. + + For error checking, we pack console commands in a particular format. The + format is as follows: + + &&[x][x][x][x]&{cmd}\n\n + ^ ^ ^^ ^^ ^ ^-- 2 newlines. + | | || || |-- the raw console command. + | | || ||-- 1 ampersand. + | | ||____|--- 2 hex digits representing the CRC8 of cmd. + | |____|-- 2 hex digits reprsenting the length of cmd. + |-- 2 ampersands + + Args: + raw_cmd: A pre-packed string which contains the raw command. + + Returns: + A string which contains the packed command. + """ + # Don't pack a single carriage return. + if raw_cmd != b"\r": + # The command format is as follows. + # &&[x][x][x][x]&{cmd}\n\n + packed_cmd = [] + packed_cmd.append(b"&&") + # The first pair of hex digits are the length of the command. + packed_cmd.append(b"%02x" % len(raw_cmd)) + # Then the CRC8 of cmd. + packed_cmd.append(b"%02x" % Crc8(raw_cmd)) + packed_cmd.append(b"&") + # Now, the raw command followed by 2 newlines. + packed_cmd.append(raw_cmd) + packed_cmd.append(b"\n\n") + return b"".join(packed_cmd) + else: + return raw_cmd + + def ProcessCommand(self, command): + """Captures the input determines what actions to take. + + Args: + command: A string representing the command sent by the user. + """ + if command == b"disconnect": + if self.connected: + self.logger.debug("UART disconnect request.") + # Drop all pending commands if any. + while not self.ec_cmd_queue.empty(): + c = self.ec_cmd_queue.get() + self.logger.debug("dropped: '%s'", c) + if self.enhanced_ec: + # Reset retry state. + self.cmd_retries = COMMAND_RETRIES + self.last_cmd = b"" + # Get the UART that the interpreter is attached to. + fileobj = self.ec_uart_pty + self.logger.debug("fileobj: %r", fileobj) + # Remove the descriptor from the inputs and outputs. + self.inputs.remove(fileobj) + if fileobj in self.outputs: + self.outputs.remove(fileobj) + self.logger.debug("Removed fileobj. Remaining inputs: %r", self.inputs) + # Close the file. + fileobj.close() + # Mark the interpreter as disconnected now. + self.connected = False + self.logger.debug("Disconnected from %s.", self.ec_uart_pty_name) + return + + elif command == b"reconnect": + if not self.connected: + self.logger.debug("UART reconnect request.") + # Reopen the PTY. + # TODO(https://bugs.python.org/issue27805, python3.7+): revert to ab+ + # TODO(https://bugs.python.org/issue20074): removing buffering=0 if/when + # that gets fixed, or keep two pty: one for reading and one for writing + fileobj = open(self.ec_uart_pty_name, "r+b", buffering=0) + self.logger.debug("fileobj: %r", fileobj) + self.ec_uart_pty = fileobj + # Add the descriptor to the inputs. + self.inputs.append(fileobj) + self.logger.debug("fileobj added. curr inputs: %r", self.inputs) + # Mark the interpreter as connected now. + self.connected = True + self.logger.debug("Connected to %s.", self.ec_uart_pty_name) + return + + elif command.startswith(b"enhanced"): + self.enhanced_ec = command.split(b" ")[1] == b"True" + return + + # Ignore any other commands while in the disconnected state. + self.logger.log(1, "command: '%s'", command) + if not self.connected: + self.logger.debug("Ignoring command because currently disconnected.") + return + + # Remove leading and trailing spaces only if this is an enhanced EC image. + # For non-enhanced EC images, commands will be single characters at a time + # and can be spaces. + if self.enhanced_ec: + command = command.strip(b" ") + + # There's nothing to do if the command is empty. + if len(command) == 0: + return + + # Handle log level change requests. + if command.startswith(b"loglevel"): + self.logger.debug("Log level change request.") + new_log_level = int(command.split(b" ")[1]) + self.logger.logger.setLevel(new_log_level) + self.logger.info("Log level changed to %d.", new_log_level) + return + + # Check for interrogation command. + if command == EC_SYN: + # User is requesting interrogation. Send SYN as is. + self.logger.debug("User requesting interrogation.") + self.interrogating = True + # Assume the EC isn't enhanced until we get a response. + self.enhanced_ec = False + elif self.enhanced_ec: + # Enhanced EC images require the plaintext commands to be packed. + command = self.PackCommand(command) + # TODO(aaboagye): Make a dict of commands and keys and eventually, + # handle partial matching based on unique prefixes. + + self.EnqueueCmd(command) + + def HandleCmdRetries(self): + """Attempts to retry commands if possible.""" + if self.cmd_retries > 0: + # The EC encountered an error. We'll have to retry again. + self.logger.warning("Retrying command...") + self.cmd_retries -= 1 + self.logger.warning("Retries remaining: %d", self.cmd_retries) + # Retry the command and add the EC UART to the writers again. + self.EnqueueCmd(self.last_cmd) + self.outputs.append(self.ec_uart_pty) + else: + # We're out of retries, so just give up. + self.logger.error("Command failed. No retries left.") + # Clear the command in progress. + self.last_cmd = b"" + # Reset the retry count. + self.cmd_retries = COMMAND_RETRIES + + def SendCmdToEC(self): + """Sends a command to the EC.""" + # If we're retrying a command, just try to send it again. + if self.cmd_retries < COMMAND_RETRIES: + cmd = self.last_cmd + else: + # If we're not retrying, we should not be writing to the EC if we have no + # items in our command queue. + assert not self.ec_cmd_queue.empty() + # Get the command to send. + cmd = self.ec_cmd_queue.get() + + # Send the command. + self.ec_uart_pty.write(cmd) + self.ec_uart_pty.flush() + self.logger.log(1, "Sent command to EC.") + + if self.enhanced_ec and cmd != EC_SYN: + # Now, that we've sent the command, store the current command as the last + # command sent. If we encounter an error string, we will attempt to retry + # this command. + if cmd != self.last_cmd: + self.last_cmd = cmd + # Reset the retry count. + self.cmd_retries = COMMAND_RETRIES + + # If no command is pending to be sent, then we can remove the EC UART from + # writers. Might need better checking for command retry logic in here. + if self.ec_cmd_queue.empty(): + # Remove the EC UART from the writers while we wait for a response. + self.logger.debug("Removing EC UART from writers.") + self.outputs.remove(self.ec_uart_pty) + + def HandleECData(self): + """Handle any debug prints from the EC.""" + self.logger.log(1, "EC has data") + # Read what the EC sent us. + data = os.read(self.ec_uart_pty.fileno(), EC_MAX_READ) + self.logger.log(1, "got: '%s'", binascii.hexlify(data)) + if b"&E" in data and self.enhanced_ec: + # We received an error, so we should retry it if possible. + self.logger.warning("Error string found in data.") + self.HandleCmdRetries() + return + + # If we were interrogating, check the response and update our knowledge + # of the current EC image. + if self.interrogating: + self.enhanced_ec = data == EC_ACK + if self.enhanced_ec: + self.logger.debug("The current EC image seems enhanced.") + else: + self.logger.debug("The current EC image does NOT seem enhanced.") + # Done interrogating. + self.interrogating = False + # For now, just forward everything the EC sends us. + self.logger.log(1, "Forwarding to user...") + self.dbg_pipe.send(data) + + def HandleUserData(self): + """Handle any incoming commands from the user. + + Raises: + EOFError: Allowed to propagate through from self.cmd_pipe.recv(). + """ + self.logger.log(1, "Command data available. Begin processing.") + data = self.cmd_pipe.recv() + # Process the command. + self.ProcessCommand(data) def Crc8(data): - """Calculates the CRC8 of data. + """Calculates the CRC8 of data. - The generator polynomial used is: x^8 + x^2 + x + 1. - This is the same implementation that is used in the EC. + The generator polynomial used is: x^8 + x^2 + x + 1. + This is the same implementation that is used in the EC. - Args: - data: A string of data that we wish to calculate the CRC8 on. + Args: + data: A string of data that we wish to calculate the CRC8 on. - Returns: - crc >> 8: An integer representing the CRC8 value. - """ - crc = 0 - for byte in six.iterbytes(data): - crc ^= (byte << 8) - for _ in range(8): - if crc & 0x8000: - crc ^= (0x1070 << 3) - crc <<= 1 - return crc >> 8 + Returns: + crc >> 8: An integer representing the CRC8 value. + """ + crc = 0 + for byte in six.iterbytes(data): + crc ^= byte << 8 + for _ in range(8): + if crc & 0x8000: + crc ^= 0x1070 << 3 + crc <<= 1 + return crc >> 8 def StartLoop(interp, shutdown_pipe=None): - """Starts an infinite loop of servicing the user and the EC. - - StartLoop checks to see if there are any commands to process, processing them - if any, and forwards EC output to the user. - - When sending a command to the EC, we send the command once and check the - response to see if the EC encountered an error when receiving the command. An - error condition is reported to the interpreter by a string with at least one - '&' and 'E'. The full string is actually '&&EE', however it's possible that - the leading ampersand or trailing 'E' could be dropped. If an error is - encountered, the interpreter will retry up to the amount configured. - - Args: - interp: An Interpreter object that has been properly initialised. - shutdown_pipe: A file object for a pipe or equivalent that becomes readable - (not blocked) to indicate that the loop should exit. Can be None to never - exit the loop. - """ - try: - # This is used instead of "break" to avoid exiting the loop in the middle of - # an iteration. - continue_looping = True - - while continue_looping: - # The inputs list is created anew in each loop iteration because the - # Interpreter class sometimes modifies the interp.inputs list. - if shutdown_pipe is None: - inputs = interp.inputs - else: - inputs = list(interp.inputs) - inputs.append(shutdown_pipe) - - readable, writeable, _ = select.select(inputs, interp.outputs, []) - - for obj in readable: - # Handle any debug prints from the EC. - if obj is interp.ec_uart_pty: - interp.HandleECData() - - # Handle any commands from the user. - elif obj is interp.cmd_pipe: - try: - interp.HandleUserData() - except EOFError: - interp.logger.debug( - 'ec3po interpreter received EOF from cmd_pipe in ' - 'HandleUserData()') - continue_looping = False - - elif obj is shutdown_pipe: - interp.logger.debug( - 'ec3po interpreter received shutdown pipe unblocked notification') - continue_looping = False - - for obj in writeable: - # Send a command to the EC. - if obj is interp.ec_uart_pty: - interp.SendCmdToEC() - - except KeyboardInterrupt: - pass - - finally: - interp.cmd_pipe.close() - interp.dbg_pipe.close() - interp.ec_uart_pty.close() - if shutdown_pipe is not None: - shutdown_pipe.close() - interp.logger.debug('Exit ec3po interpreter loop for %s', - interp.ec_uart_pty_name) + """Starts an infinite loop of servicing the user and the EC. + + StartLoop checks to see if there are any commands to process, processing them + if any, and forwards EC output to the user. + + When sending a command to the EC, we send the command once and check the + response to see if the EC encountered an error when receiving the command. An + error condition is reported to the interpreter by a string with at least one + '&' and 'E'. The full string is actually '&&EE', however it's possible that + the leading ampersand or trailing 'E' could be dropped. If an error is + encountered, the interpreter will retry up to the amount configured. + + Args: + interp: An Interpreter object that has been properly initialised. + shutdown_pipe: A file object for a pipe or equivalent that becomes readable + (not blocked) to indicate that the loop should exit. Can be None to never + exit the loop. + """ + try: + # This is used instead of "break" to avoid exiting the loop in the middle of + # an iteration. + continue_looping = True + + while continue_looping: + # The inputs list is created anew in each loop iteration because the + # Interpreter class sometimes modifies the interp.inputs list. + if shutdown_pipe is None: + inputs = interp.inputs + else: + inputs = list(interp.inputs) + inputs.append(shutdown_pipe) + + readable, writeable, _ = select.select(inputs, interp.outputs, []) + + for obj in readable: + # Handle any debug prints from the EC. + if obj is interp.ec_uart_pty: + interp.HandleECData() + + # Handle any commands from the user. + elif obj is interp.cmd_pipe: + try: + interp.HandleUserData() + except EOFError: + interp.logger.debug( + "ec3po interpreter received EOF from cmd_pipe in " + "HandleUserData()" + ) + continue_looping = False + + elif obj is shutdown_pipe: + interp.logger.debug( + "ec3po interpreter received shutdown pipe unblocked notification" + ) + continue_looping = False + + for obj in writeable: + # Send a command to the EC. + if obj is interp.ec_uart_pty: + interp.SendCmdToEC() + + except KeyboardInterrupt: + pass + + finally: + interp.cmd_pipe.close() + interp.dbg_pipe.close() + interp.ec_uart_pty.close() + if shutdown_pipe is not None: + shutdown_pipe.close() + interp.logger.debug( + "Exit ec3po interpreter loop for %s", interp.ec_uart_pty_name + ) diff --git a/util/ec3po/interpreter_unittest.py b/util/ec3po/interpreter_unittest.py index fe4d43c351..509b90f667 100755 --- a/util/ec3po/interpreter_unittest.py +++ b/util/ec3po/interpreter_unittest.py @@ -10,371 +10,389 @@ from __future__ import print_function import logging -import mock import tempfile import unittest +import mock import six - -from ec3po import interpreter -from ec3po import threadproc_shim +from ec3po import interpreter, threadproc_shim def GetBuiltins(func): - if six.PY2: - return '__builtin__.' + func - return 'builtins.' + func + if six.PY2: + return "__builtin__." + func + return "builtins." + func class TestEnhancedECBehaviour(unittest.TestCase): - """Test case to verify all enhanced EC interpretation tasks.""" - def setUp(self): - """Setup the test harness.""" - # Setup logging with a timestamp, the module, and the log level. - logging.basicConfig(level=logging.DEBUG, - format=('%(asctime)s - %(module)s -' - ' %(levelname)s - %(message)s')) - - # Create a tempfile that would represent the EC UART PTY. - self.tempfile = tempfile.NamedTemporaryFile() - - # Create the pipes that the interpreter will use. - self.cmd_pipe_user, self.cmd_pipe_itpr = threadproc_shim.Pipe() - self.dbg_pipe_user, self.dbg_pipe_itpr = threadproc_shim.Pipe(duplex=False) - - # Mock the open() function so we can inspect reads/writes to the EC. - self.ec_uart_pty = mock.mock_open() - - with mock.patch(GetBuiltins('open'), self.ec_uart_pty): - # Create an interpreter. - self.itpr = interpreter.Interpreter(self.tempfile.name, - self.cmd_pipe_itpr, - self.dbg_pipe_itpr, - log_level=logging.DEBUG, - name="EC") - - @mock.patch('ec3po.interpreter.os') - def test_HandlingCommandsThatProduceNoOutput(self, mock_os): - """Verify that the Interpreter correctly handles non-output commands. - - Args: - mock_os: MagicMock object replacing the 'os' module for this test - case. - """ - # The interpreter init should open the EC UART PTY. - expected_ec_calls = [mock.call(self.tempfile.name, 'r+b', buffering=0)] - # Have a command come in the command pipe. The first command will be an - # interrogation to determine if the EC is enhanced or not. - self.cmd_pipe_user.send(interpreter.EC_SYN) - self.itpr.HandleUserData() - # At this point, the command should be queued up waiting to be sent, so - # let's actually send it to the EC. - self.itpr.SendCmdToEC() - expected_ec_calls.extend([mock.call().write(interpreter.EC_SYN), - mock.call().flush()]) - # Now, assume that the EC sends only 1 response back of EC_ACK. - mock_os.read.side_effect = [interpreter.EC_ACK] - # When reading the EC, the interpreter will call file.fileno() to pass to - # os.read(). - expected_ec_calls.append(mock.call().fileno()) - # Simulate the response. - self.itpr.HandleECData() - - # Now that the interrogation was complete, it's time to send down the real - # command. - test_cmd = b'chan save' - # Send the test command down the pipe. - self.cmd_pipe_user.send(test_cmd) - self.itpr.HandleUserData() - self.itpr.SendCmdToEC() - # Since the EC image is enhanced, we should have sent a packed command. - expected_ec_calls.append(mock.call().write(self.itpr.PackCommand(test_cmd))) - expected_ec_calls.append(mock.call().flush()) - - # Now that the first command was sent, we should send another command which - # produces no output. The console would send another interrogation. - self.cmd_pipe_user.send(interpreter.EC_SYN) - self.itpr.HandleUserData() - self.itpr.SendCmdToEC() - expected_ec_calls.extend([mock.call().write(interpreter.EC_SYN), - mock.call().flush()]) - # Again, assume that the EC sends only 1 response back of EC_ACK. - mock_os.read.side_effect = [interpreter.EC_ACK] - # When reading the EC, the interpreter will call file.fileno() to pass to - # os.read(). - expected_ec_calls.append(mock.call().fileno()) - # Simulate the response. - self.itpr.HandleECData() - - # Now send the second test command. - test_cmd = b'chan 0' - self.cmd_pipe_user.send(test_cmd) - self.itpr.HandleUserData() - self.itpr.SendCmdToEC() - # Since the EC image is enhanced, we should have sent a packed command. - expected_ec_calls.append(mock.call().write(self.itpr.PackCommand(test_cmd))) - expected_ec_calls.append(mock.call().flush()) - - # Finally, verify that the appropriate writes were actually sent to the EC. - self.ec_uart_pty.assert_has_calls(expected_ec_calls) - - @mock.patch('ec3po.interpreter.os') - def test_CommandRetryingOnError(self, mock_os): - """Verify that commands are retried if an error is encountered. - - Args: - mock_os: MagicMock object replacing the 'os' module for this test - case. - """ - # The interpreter init should open the EC UART PTY. - expected_ec_calls = [mock.call(self.tempfile.name, 'r+b', buffering=0)] - # Have a command come in the command pipe. The first command will be an - # interrogation to determine if the EC is enhanced or not. - self.cmd_pipe_user.send(interpreter.EC_SYN) - self.itpr.HandleUserData() - # At this point, the command should be queued up waiting to be sent, so - # let's actually send it to the EC. - self.itpr.SendCmdToEC() - expected_ec_calls.extend([mock.call().write(interpreter.EC_SYN), - mock.call().flush()]) - # Now, assume that the EC sends only 1 response back of EC_ACK. - mock_os.read.side_effect = [interpreter.EC_ACK] - # When reading the EC, the interpreter will call file.fileno() to pass to - # os.read(). - expected_ec_calls.append(mock.call().fileno()) - # Simulate the response. - self.itpr.HandleECData() - - # Let's send a command that is received on the EC-side with an error. - test_cmd = b'accelinfo' - self.cmd_pipe_user.send(test_cmd) - self.itpr.HandleUserData() - self.itpr.SendCmdToEC() - packed_cmd = self.itpr.PackCommand(test_cmd) - expected_ec_calls.extend([mock.call().write(packed_cmd), - mock.call().flush()]) - # Have the EC return the error string twice. - mock_os.read.side_effect = [b'&&EE', b'&&EE'] - for i in range(2): - # When reading the EC, the interpreter will call file.fileno() to pass to - # os.read(). - expected_ec_calls.append(mock.call().fileno()) - # Simulate the response. - self.itpr.HandleECData() - - # Since an error was received, the EC should attempt to retry the command. - expected_ec_calls.extend([mock.call().write(packed_cmd), - mock.call().flush()]) - # Verify that the retry count was decremented. - self.assertEqual(interpreter.COMMAND_RETRIES-i-1, self.itpr.cmd_retries, - 'Unexpected cmd_remaining count.') - # Actually retry the command. - self.itpr.SendCmdToEC() - - # Now assume that the last one goes through with no trouble. - expected_ec_calls.extend([mock.call().write(packed_cmd), - mock.call().flush()]) - self.itpr.SendCmdToEC() - - # Verify all the calls. - self.ec_uart_pty.assert_has_calls(expected_ec_calls) - - def test_PackCommandsForEnhancedEC(self): - """Verify that the interpreter packs commands for enhanced EC images.""" - # Assume current EC image is enhanced. - self.itpr.enhanced_ec = True - # Receive a command from the user. - test_cmd = b'gettime' - self.cmd_pipe_user.send(test_cmd) - # Mock out PackCommand to see if it was called. - self.itpr.PackCommand = mock.MagicMock() - # Have the interpreter handle the command. - self.itpr.HandleUserData() - # Verify that PackCommand() was called. - self.itpr.PackCommand.assert_called_once_with(test_cmd) - - def test_DontPackCommandsForNonEnhancedEC(self): - """Verify the interpreter doesn't pack commands for non-enhanced images.""" - # Assume current EC image is not enhanced. - self.itpr.enhanced_ec = False - # Receive a command from the user. - test_cmd = b'gettime' - self.cmd_pipe_user.send(test_cmd) - # Mock out PackCommand to see if it was called. - self.itpr.PackCommand = mock.MagicMock() - # Have the interpreter handle the command. - self.itpr.HandleUserData() - # Verify that PackCommand() was called. - self.itpr.PackCommand.assert_not_called() - - @mock.patch('ec3po.interpreter.os') - def test_KeepingTrackOfInterrogation(self, mock_os): - """Verify that the interpreter can track the state of the interrogation. - - Args: - mock_os: MagicMock object replacing the 'os' module. for this test - case. - """ - # Upon init, the interpreter should assume that the current EC image is not - # enhanced. - self.assertFalse(self.itpr.enhanced_ec, msg=('State of enhanced_ec upon' - ' init is not False.')) - - # Assume an interrogation request comes in from the user. - self.cmd_pipe_user.send(interpreter.EC_SYN) - self.itpr.HandleUserData() - - # Verify the state is now within an interrogation. - self.assertTrue(self.itpr.interrogating, 'interrogating should be True') - # The state of enhanced_ec should not be changed yet because we haven't - # received a valid response yet. - self.assertFalse(self.itpr.enhanced_ec, msg=('State of enhanced_ec is ' - 'not False.')) - - # Assume that the EC responds with an EC_ACK. - mock_os.read.side_effect = [interpreter.EC_ACK] - self.itpr.HandleECData() - - # Now, the interrogation should be complete and we should know that the - # current EC image is enhanced. - self.assertFalse(self.itpr.interrogating, msg=('interrogating should be ' - 'False')) - self.assertTrue(self.itpr.enhanced_ec, msg='enhanced_ec sholud be True') - - # Now let's perform another interrogation, but pretend that the EC ignores - # it. - self.cmd_pipe_user.send(interpreter.EC_SYN) - self.itpr.HandleUserData() - - # Verify interrogating state. - self.assertTrue(self.itpr.interrogating, 'interrogating sholud be True') - # We should assume that the image is not enhanced until we get the valid - # response. - self.assertFalse(self.itpr.enhanced_ec, 'enhanced_ec should be False now.') - - # Let's pretend that we get a random debug print. This should clear the - # interrogating flag. - mock_os.read.side_effect = [b'[1660.593076 HC 0x103]'] - self.itpr.HandleECData() - - # Verify that interrogating flag is cleared and enhanced_ec is still False. - self.assertFalse(self.itpr.interrogating, 'interrogating should be False.') - self.assertFalse(self.itpr.enhanced_ec, - 'enhanced_ec should still be False.') + """Test case to verify all enhanced EC interpretation tasks.""" + + def setUp(self): + """Setup the test harness.""" + # Setup logging with a timestamp, the module, and the log level. + logging.basicConfig( + level=logging.DEBUG, + format=("%(asctime)s - %(module)s -" " %(levelname)s - %(message)s"), + ) + + # Create a tempfile that would represent the EC UART PTY. + self.tempfile = tempfile.NamedTemporaryFile() + + # Create the pipes that the interpreter will use. + self.cmd_pipe_user, self.cmd_pipe_itpr = threadproc_shim.Pipe() + self.dbg_pipe_user, self.dbg_pipe_itpr = threadproc_shim.Pipe(duplex=False) + + # Mock the open() function so we can inspect reads/writes to the EC. + self.ec_uart_pty = mock.mock_open() + + with mock.patch(GetBuiltins("open"), self.ec_uart_pty): + # Create an interpreter. + self.itpr = interpreter.Interpreter( + self.tempfile.name, + self.cmd_pipe_itpr, + self.dbg_pipe_itpr, + log_level=logging.DEBUG, + name="EC", + ) + + @mock.patch("ec3po.interpreter.os") + def test_HandlingCommandsThatProduceNoOutput(self, mock_os): + """Verify that the Interpreter correctly handles non-output commands. + + Args: + mock_os: MagicMock object replacing the 'os' module for this test + case. + """ + # The interpreter init should open the EC UART PTY. + expected_ec_calls = [mock.call(self.tempfile.name, "r+b", buffering=0)] + # Have a command come in the command pipe. The first command will be an + # interrogation to determine if the EC is enhanced or not. + self.cmd_pipe_user.send(interpreter.EC_SYN) + self.itpr.HandleUserData() + # At this point, the command should be queued up waiting to be sent, so + # let's actually send it to the EC. + self.itpr.SendCmdToEC() + expected_ec_calls.extend( + [mock.call().write(interpreter.EC_SYN), mock.call().flush()] + ) + # Now, assume that the EC sends only 1 response back of EC_ACK. + mock_os.read.side_effect = [interpreter.EC_ACK] + # When reading the EC, the interpreter will call file.fileno() to pass to + # os.read(). + expected_ec_calls.append(mock.call().fileno()) + # Simulate the response. + self.itpr.HandleECData() + + # Now that the interrogation was complete, it's time to send down the real + # command. + test_cmd = b"chan save" + # Send the test command down the pipe. + self.cmd_pipe_user.send(test_cmd) + self.itpr.HandleUserData() + self.itpr.SendCmdToEC() + # Since the EC image is enhanced, we should have sent a packed command. + expected_ec_calls.append(mock.call().write(self.itpr.PackCommand(test_cmd))) + expected_ec_calls.append(mock.call().flush()) + + # Now that the first command was sent, we should send another command which + # produces no output. The console would send another interrogation. + self.cmd_pipe_user.send(interpreter.EC_SYN) + self.itpr.HandleUserData() + self.itpr.SendCmdToEC() + expected_ec_calls.extend( + [mock.call().write(interpreter.EC_SYN), mock.call().flush()] + ) + # Again, assume that the EC sends only 1 response back of EC_ACK. + mock_os.read.side_effect = [interpreter.EC_ACK] + # When reading the EC, the interpreter will call file.fileno() to pass to + # os.read(). + expected_ec_calls.append(mock.call().fileno()) + # Simulate the response. + self.itpr.HandleECData() + + # Now send the second test command. + test_cmd = b"chan 0" + self.cmd_pipe_user.send(test_cmd) + self.itpr.HandleUserData() + self.itpr.SendCmdToEC() + # Since the EC image is enhanced, we should have sent a packed command. + expected_ec_calls.append(mock.call().write(self.itpr.PackCommand(test_cmd))) + expected_ec_calls.append(mock.call().flush()) + + # Finally, verify that the appropriate writes were actually sent to the EC. + self.ec_uart_pty.assert_has_calls(expected_ec_calls) + + @mock.patch("ec3po.interpreter.os") + def test_CommandRetryingOnError(self, mock_os): + """Verify that commands are retried if an error is encountered. + + Args: + mock_os: MagicMock object replacing the 'os' module for this test + case. + """ + # The interpreter init should open the EC UART PTY. + expected_ec_calls = [mock.call(self.tempfile.name, "r+b", buffering=0)] + # Have a command come in the command pipe. The first command will be an + # interrogation to determine if the EC is enhanced or not. + self.cmd_pipe_user.send(interpreter.EC_SYN) + self.itpr.HandleUserData() + # At this point, the command should be queued up waiting to be sent, so + # let's actually send it to the EC. + self.itpr.SendCmdToEC() + expected_ec_calls.extend( + [mock.call().write(interpreter.EC_SYN), mock.call().flush()] + ) + # Now, assume that the EC sends only 1 response back of EC_ACK. + mock_os.read.side_effect = [interpreter.EC_ACK] + # When reading the EC, the interpreter will call file.fileno() to pass to + # os.read(). + expected_ec_calls.append(mock.call().fileno()) + # Simulate the response. + self.itpr.HandleECData() + + # Let's send a command that is received on the EC-side with an error. + test_cmd = b"accelinfo" + self.cmd_pipe_user.send(test_cmd) + self.itpr.HandleUserData() + self.itpr.SendCmdToEC() + packed_cmd = self.itpr.PackCommand(test_cmd) + expected_ec_calls.extend([mock.call().write(packed_cmd), mock.call().flush()]) + # Have the EC return the error string twice. + mock_os.read.side_effect = [b"&&EE", b"&&EE"] + for i in range(2): + # When reading the EC, the interpreter will call file.fileno() to pass to + # os.read(). + expected_ec_calls.append(mock.call().fileno()) + # Simulate the response. + self.itpr.HandleECData() + + # Since an error was received, the EC should attempt to retry the command. + expected_ec_calls.extend( + [mock.call().write(packed_cmd), mock.call().flush()] + ) + # Verify that the retry count was decremented. + self.assertEqual( + interpreter.COMMAND_RETRIES - i - 1, + self.itpr.cmd_retries, + "Unexpected cmd_remaining count.", + ) + # Actually retry the command. + self.itpr.SendCmdToEC() + + # Now assume that the last one goes through with no trouble. + expected_ec_calls.extend([mock.call().write(packed_cmd), mock.call().flush()]) + self.itpr.SendCmdToEC() + + # Verify all the calls. + self.ec_uart_pty.assert_has_calls(expected_ec_calls) + + def test_PackCommandsForEnhancedEC(self): + """Verify that the interpreter packs commands for enhanced EC images.""" + # Assume current EC image is enhanced. + self.itpr.enhanced_ec = True + # Receive a command from the user. + test_cmd = b"gettime" + self.cmd_pipe_user.send(test_cmd) + # Mock out PackCommand to see if it was called. + self.itpr.PackCommand = mock.MagicMock() + # Have the interpreter handle the command. + self.itpr.HandleUserData() + # Verify that PackCommand() was called. + self.itpr.PackCommand.assert_called_once_with(test_cmd) + + def test_DontPackCommandsForNonEnhancedEC(self): + """Verify the interpreter doesn't pack commands for non-enhanced images.""" + # Assume current EC image is not enhanced. + self.itpr.enhanced_ec = False + # Receive a command from the user. + test_cmd = b"gettime" + self.cmd_pipe_user.send(test_cmd) + # Mock out PackCommand to see if it was called. + self.itpr.PackCommand = mock.MagicMock() + # Have the interpreter handle the command. + self.itpr.HandleUserData() + # Verify that PackCommand() was called. + self.itpr.PackCommand.assert_not_called() + + @mock.patch("ec3po.interpreter.os") + def test_KeepingTrackOfInterrogation(self, mock_os): + """Verify that the interpreter can track the state of the interrogation. + + Args: + mock_os: MagicMock object replacing the 'os' module. for this test + case. + """ + # Upon init, the interpreter should assume that the current EC image is not + # enhanced. + self.assertFalse( + self.itpr.enhanced_ec, + msg=("State of enhanced_ec upon" " init is not False."), + ) + + # Assume an interrogation request comes in from the user. + self.cmd_pipe_user.send(interpreter.EC_SYN) + self.itpr.HandleUserData() + + # Verify the state is now within an interrogation. + self.assertTrue(self.itpr.interrogating, "interrogating should be True") + # The state of enhanced_ec should not be changed yet because we haven't + # received a valid response yet. + self.assertFalse( + self.itpr.enhanced_ec, msg=("State of enhanced_ec is " "not False.") + ) + + # Assume that the EC responds with an EC_ACK. + mock_os.read.side_effect = [interpreter.EC_ACK] + self.itpr.HandleECData() + + # Now, the interrogation should be complete and we should know that the + # current EC image is enhanced. + self.assertFalse( + self.itpr.interrogating, msg=("interrogating should be " "False") + ) + self.assertTrue(self.itpr.enhanced_ec, msg="enhanced_ec sholud be True") + + # Now let's perform another interrogation, but pretend that the EC ignores + # it. + self.cmd_pipe_user.send(interpreter.EC_SYN) + self.itpr.HandleUserData() + + # Verify interrogating state. + self.assertTrue(self.itpr.interrogating, "interrogating sholud be True") + # We should assume that the image is not enhanced until we get the valid + # response. + self.assertFalse(self.itpr.enhanced_ec, "enhanced_ec should be False now.") + + # Let's pretend that we get a random debug print. This should clear the + # interrogating flag. + mock_os.read.side_effect = [b"[1660.593076 HC 0x103]"] + self.itpr.HandleECData() + + # Verify that interrogating flag is cleared and enhanced_ec is still False. + self.assertFalse(self.itpr.interrogating, "interrogating should be False.") + self.assertFalse(self.itpr.enhanced_ec, "enhanced_ec should still be False.") class TestUARTDisconnection(unittest.TestCase): - """Test case to verify interpreter disconnection/reconnection.""" - def setUp(self): - """Setup the test harness.""" - # Setup logging with a timestamp, the module, and the log level. - logging.basicConfig(level=logging.DEBUG, - format=('%(asctime)s - %(module)s -' - ' %(levelname)s - %(message)s')) - - # Create a tempfile that would represent the EC UART PTY. - self.tempfile = tempfile.NamedTemporaryFile() - - # Create the pipes that the interpreter will use. - self.cmd_pipe_user, self.cmd_pipe_itpr = threadproc_shim.Pipe() - self.dbg_pipe_user, self.dbg_pipe_itpr = threadproc_shim.Pipe(duplex=False) - - # Mock the open() function so we can inspect reads/writes to the EC. - self.ec_uart_pty = mock.mock_open() - - with mock.patch(GetBuiltins('open'), self.ec_uart_pty): - # Create an interpreter. - self.itpr = interpreter.Interpreter(self.tempfile.name, - self.cmd_pipe_itpr, - self.dbg_pipe_itpr, - log_level=logging.DEBUG, - name="EC") - - # First, check that interpreter is initialized to connected. - self.assertTrue(self.itpr.connected, ('The interpreter should be' - ' initialized in a connected state')) - - def test_DisconnectStopsECTraffic(self): - """Verify that when in disconnected state, no debug prints are sent.""" - # Let's send a disconnect command through the command pipe. - self.cmd_pipe_user.send(b'disconnect') - self.itpr.HandleUserData() - - # Verify interpreter is disconnected from EC. - self.assertFalse(self.itpr.connected, ('The interpreter should be' - 'disconnected.')) - # Verify that the EC UART is no longer a member of the inputs. The - # interpreter will never pull data from the EC if it's not a member of the - # inputs list. - self.assertFalse(self.itpr.ec_uart_pty in self.itpr.inputs) - - def test_CommandsDroppedWhenDisconnected(self): - """Verify that when in disconnected state, commands are dropped.""" - # Send a command, followed by 'disconnect'. - self.cmd_pipe_user.send(b'taskinfo') - self.itpr.HandleUserData() - self.cmd_pipe_user.send(b'disconnect') - self.itpr.HandleUserData() - - # Verify interpreter is disconnected from EC. - self.assertFalse(self.itpr.connected, ('The interpreter should be' - 'disconnected.')) - # Verify that the EC UART is no longer a member of the inputs nor outputs. - self.assertFalse(self.itpr.ec_uart_pty in self.itpr.inputs) - self.assertFalse(self.itpr.ec_uart_pty in self.itpr.outputs) - - # Have the user send a few more commands in the disconnected state. - command = 'help\n' - for char in command: - self.cmd_pipe_user.send(char.encode('utf-8')) - self.itpr.HandleUserData() - - # The command queue should be empty. - self.assertEqual(0, self.itpr.ec_cmd_queue.qsize()) - - # Now send the reconnect command. - self.cmd_pipe_user.send(b'reconnect') - - with mock.patch(GetBuiltins('open'), mock.mock_open()): - self.itpr.HandleUserData() - - # Verify interpreter is connected. - self.assertTrue(self.itpr.connected) - # Verify that EC UART is a member of the inputs. - self.assertTrue(self.itpr.ec_uart_pty in self.itpr.inputs) - # Since no command was sent after reconnection, verify that the EC UART is - # not a member of the outputs. - self.assertFalse(self.itpr.ec_uart_pty in self.itpr.outputs) - - def test_ReconnectAllowsECTraffic(self): - """Verify that when connected, EC UART traffic is allowed.""" - # Let's send a disconnect command through the command pipe. - self.cmd_pipe_user.send(b'disconnect') - self.itpr.HandleUserData() - - # Verify interpreter is disconnected. - self.assertFalse(self.itpr.connected, ('The interpreter should be' - 'disconnected.')) - # Verify that the EC UART is no longer a member of the inputs nor outputs. - self.assertFalse(self.itpr.ec_uart_pty in self.itpr.inputs) - self.assertFalse(self.itpr.ec_uart_pty in self.itpr.outputs) - - # Issue reconnect command through the command pipe. - self.cmd_pipe_user.send(b'reconnect') - - with mock.patch(GetBuiltins('open'), mock.mock_open()): - self.itpr.HandleUserData() - - # Verify interpreter is connected. - self.assertTrue(self.itpr.connected, ('The interpreter should be' - 'connected.')) - # Verify that the EC UART is now a member of the inputs. - self.assertTrue(self.itpr.ec_uart_pty in self.itpr.inputs) - # Since we have issued no commands during the disconnected state, no - # commands are pending and therefore the PTY should not be added to the - # outputs. - self.assertFalse(self.itpr.ec_uart_pty in self.itpr.outputs) - - -if __name__ == '__main__': - unittest.main() + """Test case to verify interpreter disconnection/reconnection.""" + + def setUp(self): + """Setup the test harness.""" + # Setup logging with a timestamp, the module, and the log level. + logging.basicConfig( + level=logging.DEBUG, + format=("%(asctime)s - %(module)s -" " %(levelname)s - %(message)s"), + ) + + # Create a tempfile that would represent the EC UART PTY. + self.tempfile = tempfile.NamedTemporaryFile() + + # Create the pipes that the interpreter will use. + self.cmd_pipe_user, self.cmd_pipe_itpr = threadproc_shim.Pipe() + self.dbg_pipe_user, self.dbg_pipe_itpr = threadproc_shim.Pipe(duplex=False) + + # Mock the open() function so we can inspect reads/writes to the EC. + self.ec_uart_pty = mock.mock_open() + + with mock.patch(GetBuiltins("open"), self.ec_uart_pty): + # Create an interpreter. + self.itpr = interpreter.Interpreter( + self.tempfile.name, + self.cmd_pipe_itpr, + self.dbg_pipe_itpr, + log_level=logging.DEBUG, + name="EC", + ) + + # First, check that interpreter is initialized to connected. + self.assertTrue( + self.itpr.connected, + ("The interpreter should be" " initialized in a connected state"), + ) + + def test_DisconnectStopsECTraffic(self): + """Verify that when in disconnected state, no debug prints are sent.""" + # Let's send a disconnect command through the command pipe. + self.cmd_pipe_user.send(b"disconnect") + self.itpr.HandleUserData() + + # Verify interpreter is disconnected from EC. + self.assertFalse( + self.itpr.connected, ("The interpreter should be" "disconnected.") + ) + # Verify that the EC UART is no longer a member of the inputs. The + # interpreter will never pull data from the EC if it's not a member of the + # inputs list. + self.assertFalse(self.itpr.ec_uart_pty in self.itpr.inputs) + + def test_CommandsDroppedWhenDisconnected(self): + """Verify that when in disconnected state, commands are dropped.""" + # Send a command, followed by 'disconnect'. + self.cmd_pipe_user.send(b"taskinfo") + self.itpr.HandleUserData() + self.cmd_pipe_user.send(b"disconnect") + self.itpr.HandleUserData() + + # Verify interpreter is disconnected from EC. + self.assertFalse( + self.itpr.connected, ("The interpreter should be" "disconnected.") + ) + # Verify that the EC UART is no longer a member of the inputs nor outputs. + self.assertFalse(self.itpr.ec_uart_pty in self.itpr.inputs) + self.assertFalse(self.itpr.ec_uart_pty in self.itpr.outputs) + + # Have the user send a few more commands in the disconnected state. + command = "help\n" + for char in command: + self.cmd_pipe_user.send(char.encode("utf-8")) + self.itpr.HandleUserData() + + # The command queue should be empty. + self.assertEqual(0, self.itpr.ec_cmd_queue.qsize()) + + # Now send the reconnect command. + self.cmd_pipe_user.send(b"reconnect") + + with mock.patch(GetBuiltins("open"), mock.mock_open()): + self.itpr.HandleUserData() + + # Verify interpreter is connected. + self.assertTrue(self.itpr.connected) + # Verify that EC UART is a member of the inputs. + self.assertTrue(self.itpr.ec_uart_pty in self.itpr.inputs) + # Since no command was sent after reconnection, verify that the EC UART is + # not a member of the outputs. + self.assertFalse(self.itpr.ec_uart_pty in self.itpr.outputs) + + def test_ReconnectAllowsECTraffic(self): + """Verify that when connected, EC UART traffic is allowed.""" + # Let's send a disconnect command through the command pipe. + self.cmd_pipe_user.send(b"disconnect") + self.itpr.HandleUserData() + + # Verify interpreter is disconnected. + self.assertFalse( + self.itpr.connected, ("The interpreter should be" "disconnected.") + ) + # Verify that the EC UART is no longer a member of the inputs nor outputs. + self.assertFalse(self.itpr.ec_uart_pty in self.itpr.inputs) + self.assertFalse(self.itpr.ec_uart_pty in self.itpr.outputs) + + # Issue reconnect command through the command pipe. + self.cmd_pipe_user.send(b"reconnect") + + with mock.patch(GetBuiltins("open"), mock.mock_open()): + self.itpr.HandleUserData() + + # Verify interpreter is connected. + self.assertTrue(self.itpr.connected, ("The interpreter should be" "connected.")) + # Verify that the EC UART is now a member of the inputs. + self.assertTrue(self.itpr.ec_uart_pty in self.itpr.inputs) + # Since we have issued no commands during the disconnected state, no + # commands are pending and therefore the PTY should not be added to the + # outputs. + self.assertFalse(self.itpr.ec_uart_pty in self.itpr.outputs) + + +if __name__ == "__main__": + unittest.main() diff --git a/util/ec3po/threadproc_shim.py b/util/ec3po/threadproc_shim.py index da5440b1f3..c0b3ce0bf4 100644 --- a/util/ec3po/threadproc_shim.py +++ b/util/ec3po/threadproc_shim.py @@ -34,33 +34,34 @@ wait until after completing the TODO above to stop using multiprocessing.Pipe! # Imports to bring objects into this namespace for users of this module. from multiprocessing import Pipe -from six.moves.queue import Queue from threading import Thread as ThreadOrProcess +from six.moves.queue import Queue + # True if this module has ec3po using subprocesses, False if using threads. USING_SUBPROCS = False def _DoNothing(): - """Do-nothing function for use as a callback with DoIf().""" + """Do-nothing function for use as a callback with DoIf().""" def DoIf(subprocs=_DoNothing, threads=_DoNothing): - """Return a callback or not based on ec3po use of subprocesses or threads. + """Return a callback or not based on ec3po use of subprocesses or threads. - Args: - subprocs: callback that does not require any args - This will be returned - (not called!) if and only if ec3po is using subprocesses. This is - OPTIONAL, the default value is a do-nothing callback that returns None. - threads: callback that does not require any args - This will be returned - (not called!) if and only if ec3po is using threads. This is OPTIONAL, - the default value is a do-nothing callback that returns None. + Args: + subprocs: callback that does not require any args - This will be returned + (not called!) if and only if ec3po is using subprocesses. This is + OPTIONAL, the default value is a do-nothing callback that returns None. + threads: callback that does not require any args - This will be returned + (not called!) if and only if ec3po is using threads. This is OPTIONAL, + the default value is a do-nothing callback that returns None. - Returns: - Either the subprocs or threads argument will be returned. - """ - return subprocs if USING_SUBPROCS else threads + Returns: + Either the subprocs or threads argument will be returned. + """ + return subprocs if USING_SUBPROCS else threads def Value(ctype, *args): - return ctype(*args) + return ctype(*args) diff --git a/util/ec_openocd.py b/util/ec_openocd.py index a84c00643c..11956ffa1c 100755 --- a/util/ec_openocd.py +++ b/util/ec_openocd.py @@ -16,6 +16,7 @@ import time Flashes and debugs the EC through openocd """ + @dataclasses.dataclass class BoardInfo: gdb_variant: str @@ -24,9 +25,7 @@ class BoardInfo: # Debuggers for each board, OpenOCD currently only supports GDB -boards = { - "skyrim": BoardInfo("arm-none-eabi-gdb", 6, 4) -} +boards = {"skyrim": BoardInfo("arm-none-eabi-gdb", 6, 4)} def create_openocd_args(interface, board): @@ -36,9 +35,12 @@ def create_openocd_args(interface, board): board_info = boards[board] args = [ "openocd", - "-f", f"interface/{interface}.cfg", - "-c", "add_script_search_dir openocd", - "-f", f"board/{board}.cfg", + "-f", + f"interface/{interface}.cfg", + "-c", + "add_script_search_dir openocd", + "-f", + f"board/{board}.cfg", ] return args @@ -53,11 +55,13 @@ def create_gdb_args(board, port, executable): board_info.gdb_variant, executable, # GDB can't autodetect these according to OpenOCD - "-ex", f"set remote hardware-breakpoint-limit {board_info.num_breakpoints}", - "-ex", f"set remote hardware-watchpoint-limit {board_info.num_watchpoints}", - + "-ex", + f"set remote hardware-breakpoint-limit {board_info.num_breakpoints}", + "-ex", + f"set remote hardware-watchpoint-limit {board_info.num_watchpoints}", # Connect to OpenOCD - "-ex", f"target extended-remote localhost:{port}", + "-ex", + f"target extended-remote localhost:{port}", ] return args diff --git a/util/flash_jlink.py b/util/flash_jlink.py index 26c3c2e709..50a0bfca20 100755 --- a/util/flash_jlink.py +++ b/util/flash_jlink.py @@ -25,7 +25,6 @@ import sys import tempfile import time - DEFAULT_SEGGER_REMOTE_PORT = 19020 # Commands are documented here: https://wiki.segger.com/J-Link_Commander @@ -41,27 +40,34 @@ exit class BoardConfig: """Board configuration.""" + def __init__(self, interface, device, flash_address): self.interface = interface self.device = device self.flash_address = flash_address -SWD_INTERFACE = 'SWD' -STM32_DEFAULT_FLASH_ADDRESS = '0x8000000' -DRAGONCLAW_CONFIG = BoardConfig(interface=SWD_INTERFACE, device='STM32F412CG', - flash_address=STM32_DEFAULT_FLASH_ADDRESS) -ICETOWER_CONFIG = BoardConfig(interface=SWD_INTERFACE, device='STM32H743ZI', - flash_address=STM32_DEFAULT_FLASH_ADDRESS) +SWD_INTERFACE = "SWD" +STM32_DEFAULT_FLASH_ADDRESS = "0x8000000" +DRAGONCLAW_CONFIG = BoardConfig( + interface=SWD_INTERFACE, + device="STM32F412CG", + flash_address=STM32_DEFAULT_FLASH_ADDRESS, +) +ICETOWER_CONFIG = BoardConfig( + interface=SWD_INTERFACE, + device="STM32H743ZI", + flash_address=STM32_DEFAULT_FLASH_ADDRESS, +) BOARD_CONFIGS = { - 'dragonclaw': DRAGONCLAW_CONFIG, - 'bloonchipper': DRAGONCLAW_CONFIG, - 'nucleo-f412zg': DRAGONCLAW_CONFIG, - 'dartmonkey': ICETOWER_CONFIG, - 'icetower': ICETOWER_CONFIG, - 'nucleo-dartmonkey': ICETOWER_CONFIG, - 'nucleo-h743zi': ICETOWER_CONFIG, + "dragonclaw": DRAGONCLAW_CONFIG, + "bloonchipper": DRAGONCLAW_CONFIG, + "nucleo-f412zg": DRAGONCLAW_CONFIG, + "dartmonkey": ICETOWER_CONFIG, + "icetower": ICETOWER_CONFIG, + "nucleo-dartmonkey": ICETOWER_CONFIG, + "nucleo-h743zi": ICETOWER_CONFIG, } @@ -93,9 +99,11 @@ def is_tcp_port_open(host: str, tcp_port: int) -> bool: def create_jlink_command_file(firmware_file, config): tmp = tempfile.NamedTemporaryFile() - tmp.write(JLINK_COMMANDS.format(FIRMWARE=firmware_file, - FLASH_ADDRESS=config.flash_address).encode( - 'utf-8')) + tmp.write( + JLINK_COMMANDS.format( + FIRMWARE=firmware_file, FLASH_ADDRESS=config.flash_address + ).encode("utf-8") + ) tmp.flush() return tmp @@ -106,8 +114,8 @@ def flash(jlink_exe, remote, device, interface, cmd_file): ] if remote: - logging.debug(f'Connecting to J-Link over TCP/IP {remote}.') - remote_components = remote.split(':') + logging.debug(f"Connecting to J-Link over TCP/IP {remote}.") + remote_components = remote.split(":") if len(remote_components) not in [1, 2]: logging.debug(f'Given remote "{remote}" is malformed.') return 1 @@ -118,7 +126,7 @@ def flash(jlink_exe, remote, device, interface, cmd_file): except socket.gaierror as e: logging.error(f'Failed to resolve host "{host}": {e}.') return 1 - logging.debug(f'Resolved {host} as {ip}.') + logging.debug(f"Resolved {host} as {ip}.") port = DEFAULT_SEGGER_REMOTE_PORT if len(remote_components) == 2: @@ -126,29 +134,36 @@ def flash(jlink_exe, remote, device, interface, cmd_file): port = int(remote_components[1]) except ValueError: logging.error( - f'Given remote port "{remote_components[1]}" is malformed.') + f'Given remote port "{remote_components[1]}" is malformed.' + ) return 1 - remote = f'{ip}:{port}' + remote = f"{ip}:{port}" - logging.debug(f'Checking connection to {remote}.') + logging.debug(f"Checking connection to {remote}.") if not is_tcp_port_open(ip, port): - logging.error( - f"JLink server doesn't seem to be listening on {remote}.") - logging.error('Ensure that JLinkRemoteServerCLExe is running.') + logging.error(f"JLink server doesn't seem to be listening on {remote}.") + logging.error("Ensure that JLinkRemoteServerCLExe is running.") return 1 - cmd.extend(['-ip', remote]) - - cmd.extend([ - '-device', device, - '-if', interface, - '-speed', 'auto', - '-autoconnect', '1', - '-CommandFile', cmd_file, - ]) - logging.debug('Running command: "%s"', ' '.join(cmd)) + cmd.extend(["-ip", remote]) + + cmd.extend( + [ + "-device", + device, + "-if", + interface, + "-speed", + "auto", + "-autoconnect", + "1", + "-CommandFile", + cmd_file, + ] + ) + logging.debug('Running command: "%s"', " ".join(cmd)) completed_process = subprocess.run(cmd) # pylint: disable=subprocess-run-check - logging.debug('JLink return code: %d', completed_process.returncode) + logging.debug("JLink return code: %d", completed_process.returncode) return completed_process.returncode @@ -156,38 +171,42 @@ def main(argv: list): parser = argparse.ArgumentParser() - default_jlink = './JLink_Linux_V684a_x86_64/JLinkExe' + default_jlink = "./JLink_Linux_V684a_x86_64/JLinkExe" if shutil.which(default_jlink) is None: - default_jlink = 'JLinkExe' - parser.add_argument( - '--jlink', '-j', - help='JLinkExe path (default: ' + default_jlink + ')', - default=default_jlink) - + default_jlink = "JLinkExe" parser.add_argument( - '--remote', '-n', - help='Use TCP/IP host[:port] to connect to a J-Link or ' - 'JLinkRemoteServerCLExe. If unspecified, connect over USB.') + "--jlink", + "-j", + help="JLinkExe path (default: " + default_jlink + ")", + default=default_jlink, + ) - default_board = 'bloonchipper' parser.add_argument( - '--board', '-b', - help='Board (default: ' + default_board + ')', - default=default_board) + "--remote", + "-n", + help="Use TCP/IP host[:port] to connect to a J-Link or " + "JLinkRemoteServerCLExe. If unspecified, connect over USB.", + ) - default_firmware = os.path.join('./build', default_board, 'ec.bin') + default_board = "bloonchipper" parser.add_argument( - '--image', '-i', - help='Firmware binary (default: ' + default_firmware + ')', - default=default_firmware) + "--board", + "-b", + help="Board (default: " + default_board + ")", + default=default_board, + ) - log_level_choices = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] + default_firmware = os.path.join("./build", default_board, "ec.bin") parser.add_argument( - '--log_level', '-l', - choices=log_level_choices, - default='DEBUG' + "--image", + "-i", + help="Firmware binary (default: " + default_firmware + ")", + default=default_firmware, ) + log_level_choices = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + parser.add_argument("--log_level", "-l", choices=log_level_choices, default="DEBUG") + args = parser.parse_args(argv) logging.basicConfig(level=args.log_level) @@ -201,11 +220,12 @@ def main(argv: list): args.jlink = args.jlink cmd_file = create_jlink_command_file(args.image, config) - ret_code = flash(args.jlink, args.remote, config.device, config.interface, - cmd_file.name) + ret_code = flash( + args.jlink, args.remote, config.device, config.interface, cmd_file.name + ) cmd_file.close() return ret_code -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(main(sys.argv[1:])) diff --git a/util/fptool.py b/util/fptool.py index 5d73302bbc..b7f2150289 100755 --- a/util/fptool.py +++ b/util/fptool.py @@ -19,14 +19,14 @@ def cmd_flash(args: argparse.Namespace) -> int: disabled. """ - if not shutil.which('flash_fp_mcu'): - print('Error - The flash_fp_mcu utility does not exist.') + if not shutil.which("flash_fp_mcu"): + print("Error - The flash_fp_mcu utility does not exist.") return 1 - cmd = ['flash_fp_mcu'] + cmd = ["flash_fp_mcu"] if args.image: if not os.path.isfile(args.image): - print(f'Error - image {args.image} is not a file.') + print(f"Error - image {args.image} is not a file.") return 1 cmd.append(args.image) @@ -38,18 +38,17 @@ def cmd_flash(args: argparse.Namespace) -> int: def main(argv: list) -> int: parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest='subcommand', title='subcommands') + subparsers = parser.add_subparsers(dest="subcommand", title="subcommands") # This method of setting required is more compatible with older python. subparsers.required = True # Parser for "flash" subcommand. - parser_decrypt = subparsers.add_parser('flash', help=cmd_flash.__doc__) - parser_decrypt.add_argument( - 'image', nargs='?', help='Path to the firmware image') + parser_decrypt = subparsers.add_parser("flash", help=cmd_flash.__doc__) + parser_decrypt.add_argument("image", nargs="?", help="Path to the firmware image") parser_decrypt.set_defaults(func=cmd_flash) opts = parser.parse_args(argv) return opts.func(opts) -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(main(sys.argv[1:])) diff --git a/util/inject-keys.py b/util/inject-keys.py index bd10b693ad..d05d4fbed7 100755 --- a/util/inject-keys.py +++ b/util/inject-keys.py @@ -8,50 +8,124 @@ # Note: This is a py2/3 compatible file. from __future__ import print_function + import string import subprocess import sys - -KEYMATRIX = {'`': (3, 1), '1': (6, 1), '2': (6, 4), '3': (6, 2), '4': (6, 3), - '5': (3, 3), '6': (3, 6), '7': (6, 6), '8': (6, 5), '9': (6, 9), - '0': (6, 8), '-': (3, 8), '=': (0, 8), 'q': (7, 1), 'w': (7, 4), - 'e': (7, 2), 'r': (7, 3), 't': (2, 3), 'y': (2, 6), 'u': (7, 6), - 'i': (7, 5), 'o': (7, 9), 'p': (7, 8), '[': (2, 8), ']': (2, 5), - '\\': (3, 11), 'a': (4, 1), 's': (4, 4), 'd': (4, 2), 'f': (4, 3), - 'g': (1, 3), 'h': (1, 6), 'j': (4, 6), 'k': (4, 5), 'l': (4, 9), - ';': (4, 8), '\'': (1, 8), 'z': (5, 1), 'x': (5, 4), 'c': (5, 2), - 'v': (5, 3), 'b': (0, 3), 'n': (0, 6), 'm': (5, 6), ',': (5, 5), - '.': (5, 9), '/': (5, 8), ' ': (5, 11), '<right>': (6, 12), - '<alt_r>': (0, 10), '<down>': (6, 11), '<tab>': (2, 1), - '<f10>': (0, 4), '<shift_r>': (7, 7), '<ctrl_r>': (4, 0), - '<esc>': (1, 1), '<backspace>': (1, 11), '<f2>': (3, 2), - '<alt_l>': (6, 10), '<ctrl_l>': (2, 0), '<f1>': (0, 2), - '<search>': (0, 1), '<f3>': (2, 2), '<f4>': (1, 2), '<f5>': (3, 4), - '<f6>': (2, 4), '<f7>': (1, 4), '<f8>': (2, 9), '<f9>': (1, 9), - '<up>': (7, 11), '<shift_l>': (5, 7), '<enter>': (4, 11), - '<left>': (7, 12)} - - -UNSHIFT_TABLE = { '~': '`', '!': '1', '@': '2', '#': '3', '$': '4', - '%': '5', '^': '6', '&': '7', '*': '8', '(': '9', - ')': '0', '_': '-', '+': '=', '{': '[', '}': ']', - '|': '\\', - ':': ';', '"': "'", '<': ',', '>': '.', '?': '/'} +KEYMATRIX = { + "`": (3, 1), + "1": (6, 1), + "2": (6, 4), + "3": (6, 2), + "4": (6, 3), + "5": (3, 3), + "6": (3, 6), + "7": (6, 6), + "8": (6, 5), + "9": (6, 9), + "0": (6, 8), + "-": (3, 8), + "=": (0, 8), + "q": (7, 1), + "w": (7, 4), + "e": (7, 2), + "r": (7, 3), + "t": (2, 3), + "y": (2, 6), + "u": (7, 6), + "i": (7, 5), + "o": (7, 9), + "p": (7, 8), + "[": (2, 8), + "]": (2, 5), + "\\": (3, 11), + "a": (4, 1), + "s": (4, 4), + "d": (4, 2), + "f": (4, 3), + "g": (1, 3), + "h": (1, 6), + "j": (4, 6), + "k": (4, 5), + "l": (4, 9), + ";": (4, 8), + "'": (1, 8), + "z": (5, 1), + "x": (5, 4), + "c": (5, 2), + "v": (5, 3), + "b": (0, 3), + "n": (0, 6), + "m": (5, 6), + ",": (5, 5), + ".": (5, 9), + "/": (5, 8), + " ": (5, 11), + "<right>": (6, 12), + "<alt_r>": (0, 10), + "<down>": (6, 11), + "<tab>": (2, 1), + "<f10>": (0, 4), + "<shift_r>": (7, 7), + "<ctrl_r>": (4, 0), + "<esc>": (1, 1), + "<backspace>": (1, 11), + "<f2>": (3, 2), + "<alt_l>": (6, 10), + "<ctrl_l>": (2, 0), + "<f1>": (0, 2), + "<search>": (0, 1), + "<f3>": (2, 2), + "<f4>": (1, 2), + "<f5>": (3, 4), + "<f6>": (2, 4), + "<f7>": (1, 4), + "<f8>": (2, 9), + "<f9>": (1, 9), + "<up>": (7, 11), + "<shift_l>": (5, 7), + "<enter>": (4, 11), + "<left>": (7, 12), +} + + +UNSHIFT_TABLE = { + "~": "`", + "!": "1", + "@": "2", + "#": "3", + "$": "4", + "%": "5", + "^": "6", + "&": "7", + "*": "8", + "(": "9", + ")": "0", + "_": "-", + "+": "=", + "{": "[", + "}": "]", + "|": "\\", + ":": ";", + '"': "'", + "<": ",", + ">": ".", + "?": "/", +} for c in string.ascii_lowercase: UNSHIFT_TABLE[c.upper()] = c def inject_event(key, press): - if len(key) >= 2 and key[0] != '<': - key = '<' + key + '>' + if len(key) >= 2 and key[0] != "<": + key = "<" + key + ">" if key not in KEYMATRIX: print("%s: invalid key: %s" % (this_script, key)) sys.exit(1) (row, col) = KEYMATRIX[key] - subprocess.call(["ectool", "kbpress", str(row), str(col), - "1" if press else "0"]) + subprocess.call(["ectool", "kbpress", str(row), str(col), "1" if press else "0"]) def inject_key(key): @@ -73,8 +147,10 @@ def inject_string(string): def usage(): - print("Usage: %s [-s <string>] [-k <key>]" % this_script, - "[-p <pressed-key>] [-r <released-key>] ...") + print( + "Usage: %s [-s <string>] [-k <key>]" % this_script, + "[-p <pressed-key>] [-r <released-key>] ...", + ) print("Examples:") print("%s -s MyPassw0rd -k enter" % this_script) print("%s -p ctrl_l -p alt_l -k f3 -r alt_l -r ctrl_l" % this_script) @@ -85,7 +161,7 @@ def help(): print("Valid keys are:") i = 0 for key in KEYMATRIX: - print("%12s" % key, end='') + print("%12s" % key, end="") i += 1 if i % 4 == 0: print() @@ -114,12 +190,13 @@ usage_check(arg_len > 1, "not enough arguments") usage_check(arg_len % 2 == 1, "mismatched arguments") for i in range(1, arg_len, 2): - usage_check(sys.argv[i] in ("-s", "-k", "-p", "-r"), - "unknown flag: %s" % sys.argv[i]) + usage_check( + sys.argv[i] in ("-s", "-k", "-p", "-r"), "unknown flag: %s" % sys.argv[i] + ) for i in range(1, arg_len, 2): flag = sys.argv[i] - arg = sys.argv[i+1] + arg = sys.argv[i + 1] if flag == "-s": inject_string(arg) elif flag == "-k": diff --git a/util/kconfig_check.py b/util/kconfig_check.py index d1eba8e62b..04cdf9a990 100755 --- a/util/kconfig_check.py +++ b/util/kconfig_check.py @@ -32,12 +32,13 @@ import sys USE_KCONFIGLIB = False try: import kconfiglib + USE_KCONFIGLIB = True except ImportError: pass # Where we put the new config_allowed file -NEW_ALLOWED_FNAME = pathlib.Path('/tmp/new_config_allowed.txt') +NEW_ALLOWED_FNAME = pathlib.Path("/tmp/new_config_allowed.txt") def parse_args(argv): @@ -49,38 +50,72 @@ def parse_args(argv): Returns: argparse.Namespace object containing the results """ - epilog = '''Checks that new ad-hoc CONFIG options are not introduced without -a corresponding Kconfig option for Zephyr''' + epilog = """Checks that new ad-hoc CONFIG options are not introduced without +a corresponding Kconfig option for Zephyr""" parser = argparse.ArgumentParser(epilog=epilog) - parser.add_argument('-a', '--allowed', type=str, - default='util/config_allowed.txt', - help='File containing list of allowed ad-hoc CONFIGs') - parser.add_argument('-c', '--configs', type=str, default='.config', - help='File containing CONFIG options to check') - parser.add_argument('-d', '--use-defines', action='store_true', - help='Lines in the configs file use #define') parser.add_argument( - '-D', '--debug', action='store_true', - help='Enabling debugging (provides a full traceback on error)') + "-a", + "--allowed", + type=str, + default="util/config_allowed.txt", + help="File containing list of allowed ad-hoc CONFIGs", + ) + parser.add_argument( + "-c", + "--configs", + type=str, + default=".config", + help="File containing CONFIG options to check", + ) + parser.add_argument( + "-d", + "--use-defines", + action="store_true", + help="Lines in the configs file use #define", + ) + parser.add_argument( + "-D", + "--debug", + action="store_true", + help="Enabling debugging (provides a full traceback on error)", + ) + parser.add_argument( + "-i", + "--ignore", + action="append", + help="Kconfig options to ignore (without CONFIG_ prefix)", + ) parser.add_argument( - '-i', '--ignore', action='append', - help='Kconfig options to ignore (without CONFIG_ prefix)') - parser.add_argument('-I', '--search-path', type=str, action='append', - help='Search paths to look for Kconfigs') - parser.add_argument('-p', '--prefix', type=str, default='PLATFORM_EC_', - help='Prefix to string from Kconfig options') - parser.add_argument('-s', '--srctree', type=str, default='zephyr/', - help='Path to source tree to look for Kconfigs') + "-I", + "--search-path", + type=str, + action="append", + help="Search paths to look for Kconfigs", + ) + parser.add_argument( + "-p", + "--prefix", + type=str, + default="PLATFORM_EC_", + help="Prefix to string from Kconfig options", + ) + parser.add_argument( + "-s", + "--srctree", + type=str, + default="zephyr/", + help="Path to source tree to look for Kconfigs", + ) # TODO(sjg@chromium.org): The chroot uses a very old Python. Once it moves # to 3.7 or later we can use this instead: # subparsers = parser.add_subparsers(dest='cmd', required=True) - subparsers = parser.add_subparsers(dest='cmd') + subparsers = parser.add_subparsers(dest="cmd") subparsers.required = True - subparsers.add_parser('build', help='Build new list of ad-hoc CONFIGs') - subparsers.add_parser('check', help='Check for new ad-hoc CONFIGs') + subparsers.add_parser("build", help="Build new list of ad-hoc CONFIGs") + subparsers.add_parser("check", help="Check for new ad-hoc CONFIGs") return parser.parse_args(argv) @@ -107,6 +142,7 @@ class KconfigCheck: the user is exhorted to add a new Kconfig. This helps avoid adding new ad-hoc CONFIG options, eventually returning the number to zero. """ + @classmethod def find_new_adhoc(cls, configs, kconfigs, allowed): """Get a list of new ad-hoc CONFIG options @@ -172,11 +208,12 @@ class KconfigCheck: List of CONFIG_xxx options found in the file, with the 'CONFIG_' prefix removed """ - with open(configs_file, 'r') as inf: - configs = re.findall('%sCONFIG_([A-Za-z0-9_]*)%s' % - ((use_defines and '#define ' or ''), - (use_defines and ' ' or '')), - inf.read()) + with open(configs_file, "r") as inf: + configs = re.findall( + "%sCONFIG_([A-Za-z0-9_]*)%s" + % ((use_defines and "#define " or ""), (use_defines and " " or "")), + inf.read(), + ) return configs @classmethod @@ -190,8 +227,8 @@ class KconfigCheck: List of CONFIG_xxx options found in the file, with the 'CONFIG_' prefix removed """ - with open(allowed_file, 'r') as inf: - configs = re.findall('CONFIG_([A-Za-z0-9_]*)', inf.read()) + with open(allowed_file, "r") as inf: + configs = re.findall("CONFIG_([A-Za-z0-9_]*)", inf.read()) return configs @classmethod @@ -209,15 +246,17 @@ class KconfigCheck: """ kconfig_files = [] for root, dirs, files in os.walk(srcdir): - kconfig_files += [os.path.join(root, fname) - for fname in files if fname.startswith('Kconfig')] - if 'Kconfig' in dirs: - dirs.remove('Kconfig') + kconfig_files += [ + os.path.join(root, fname) + for fname in files + if fname.startswith("Kconfig") + ] + if "Kconfig" in dirs: + dirs.remove("Kconfig") return kconfig_files @classmethod - def scan_kconfigs(cls, srcdir, prefix='', search_paths=None, - try_kconfiglib=True): + def scan_kconfigs(cls, srcdir, prefix="", search_paths=None, try_kconfiglib=True): """Scan a source tree for Kconfig options Args: @@ -231,31 +270,40 @@ class KconfigCheck: List of config and menuconfig options found """ if USE_KCONFIGLIB and try_kconfiglib: - os.environ['srctree'] = srcdir - kconf = kconfiglib.Kconfig('Kconfig', warn=False, - search_paths=search_paths, - allow_empty_macros=True) + os.environ["srctree"] = srcdir + kconf = kconfiglib.Kconfig( + "Kconfig", + warn=False, + search_paths=search_paths, + allow_empty_macros=True, + ) # There is always a MODULES config, since kconfiglib is designed for # linux, but we don't want it - kconfigs = [name for name in kconf.syms if name != 'MODULES'] + kconfigs = [name for name in kconf.syms if name != "MODULES"] if prefix: - re_drop_prefix = re.compile(r'^%s' % prefix) - kconfigs = [re_drop_prefix.sub('', name) for name in kconfigs] + re_drop_prefix = re.compile(r"^%s" % prefix) + kconfigs = [re_drop_prefix.sub("", name) for name in kconfigs] else: kconfigs = [] # Remove the prefix if present - expr = re.compile(r'\n(config|menuconfig) (%s)?([A-Za-z0-9_]*)\n' % - prefix) + expr = re.compile(r"\n(config|menuconfig) (%s)?([A-Za-z0-9_]*)\n" % prefix) for fname in cls.find_kconfigs(srcdir): with open(fname) as inf: found = re.findall(expr, inf.read()) kconfigs += [name for kctype, _, name in found] return sorted(kconfigs) - def check_adhoc_configs(self, configs_file, srcdir, allowed_file, - prefix='', use_defines=False, search_paths=None): + def check_adhoc_configs( + self, + configs_file, + srcdir, + allowed_file, + prefix="", + use_defines=False, + search_paths=None, + ): """Find new and unneeded ad-hoc configs in the configs_file Args: @@ -283,8 +331,9 @@ class KconfigCheck: except kconfiglib.KconfigError: # If we don't actually have access to the full Kconfig then we may # get an error. Fall back to using manual methods. - kconfigs = self.scan_kconfigs(srcdir, prefix, search_paths, - try_kconfiglib=False) + kconfigs = self.scan_kconfigs( + srcdir, prefix, search_paths, try_kconfiglib=False + ) allowed = self.read_allowed(allowed_file) new_adhoc = self.find_new_adhoc(configs, kconfigs, allowed) @@ -292,8 +341,16 @@ class KconfigCheck: updated_adhoc = self.get_updated_adhoc(unneeded_adhoc, allowed) return new_adhoc, unneeded_adhoc, updated_adhoc - def do_check(self, configs_file, srcdir, allowed_file, prefix, use_defines, - search_paths, ignore=None): + def do_check( + self, + configs_file, + srcdir, + allowed_file, + prefix, + use_defines, + search_paths, + ignore=None, + ): """Find new ad-hoc configs in the configs_file Args: @@ -313,11 +370,12 @@ class KconfigCheck: Exit code: 0 if OK, 1 if a problem was found """ new_adhoc, unneeded_adhoc, updated_adhoc = self.check_adhoc_configs( - configs_file, srcdir, allowed_file, prefix, use_defines, - search_paths) + configs_file, srcdir, allowed_file, prefix, use_defines, search_paths + ) if new_adhoc: - file_list = '\n'.join(['CONFIG_%s' % name for name in new_adhoc]) - print(f'''Error:\tThe EC is in the process of migrating to Zephyr. + file_list = "\n".join(["CONFIG_%s" % name for name in new_adhoc]) + print( + f"""Error:\tThe EC is in the process of migrating to Zephyr. \tZephyr uses Kconfig for configuration rather than ad-hoc #defines. \tAny new EC CONFIG options must ALSO be added to Zephyr so that new \tfunctionality is available in Zephyr also. The following new ad-hoc @@ -330,19 +388,21 @@ file in zephyr/ and add a 'config' or 'menuconfig' option. Also see details in http://issuetracker.google.com/181253613 To temporarily disable this, use: ALLOW_CONFIG=1 make ... -''', file=sys.stderr) +""", + file=sys.stderr, + ) return 1 if not ignore: ignore = [] unneeded_adhoc = [name for name in unneeded_adhoc if name not in ignore] if unneeded_adhoc: - with open(NEW_ALLOWED_FNAME, 'w') as out: + with open(NEW_ALLOWED_FNAME, "w") as out: for config in updated_adhoc: - print('CONFIG_%s' % config, file=out) - now_in_kconfig = '\n'.join( - ['CONFIG_%s' % name for name in unneeded_adhoc]) - print(f'''The following options are now in Kconfig: + print("CONFIG_%s" % config, file=out) + now_in_kconfig = "\n".join(["CONFIG_%s" % name for name in unneeded_adhoc]) + print( + f"""The following options are now in Kconfig: {now_in_kconfig} @@ -350,12 +410,14 @@ Please run this to update the list of allowed ad-hoc CONFIGs and include this update in your CL: cp {NEW_ALLOWED_FNAME} util/config_allowed.txt -''') +""" + ) return 1 return 0 - def do_build(self, configs_file, srcdir, allowed_file, prefix, use_defines, - search_paths): + def do_build( + self, configs_file, srcdir, allowed_file, prefix, use_defines, search_paths + ): """Find new ad-hoc configs in the configs_file Args: @@ -372,13 +434,14 @@ update in your CL: Exit code: 0 if OK, 1 if a problem was found """ new_adhoc, _, updated_adhoc = self.check_adhoc_configs( - configs_file, srcdir, allowed_file, prefix, use_defines, - search_paths) - with open(NEW_ALLOWED_FNAME, 'w') as out: + configs_file, srcdir, allowed_file, prefix, use_defines, search_paths + ) + with open(NEW_ALLOWED_FNAME, "w") as out: combined = sorted(new_adhoc + updated_adhoc) for config in combined: - print(f'CONFIG_{config}', file=out) - print(f'New list is in {NEW_ALLOWED_FNAME}') + print(f"CONFIG_{config}", file=out) + print(f"New list is in {NEW_ALLOWED_FNAME}") + def main(argv): """Main function""" @@ -386,18 +449,27 @@ def main(argv): if not args.debug: sys.tracebacklimit = 0 checker = KconfigCheck() - if args.cmd == 'check': + if args.cmd == "check": return checker.do_check( - configs_file=args.configs, srcdir=args.srctree, - allowed_file=args.allowed, prefix=args.prefix, - use_defines=args.use_defines, search_paths=args.search_path, - ignore=args.ignore) - elif args.cmd == 'build': - return checker.do_build(configs_file=args.configs, srcdir=args.srctree, - allowed_file=args.allowed, prefix=args.prefix, - use_defines=args.use_defines, search_paths=args.search_path) + configs_file=args.configs, + srcdir=args.srctree, + allowed_file=args.allowed, + prefix=args.prefix, + use_defines=args.use_defines, + search_paths=args.search_path, + ignore=args.ignore, + ) + elif args.cmd == "build": + return checker.do_build( + configs_file=args.configs, + srcdir=args.srctree, + allowed_file=args.allowed, + prefix=args.prefix, + use_defines=args.use_defines, + search_paths=args.search_path, + ) return 2 -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(main(sys.argv[1:])) diff --git a/util/kconfiglib.py b/util/kconfiglib.py index 0e05aaaeac..a0033bba2d 100644 --- a/util/kconfiglib.py +++ b/util/kconfiglib.py @@ -553,7 +553,6 @@ import sys from glob import iglob from os.path import dirname, exists, expandvars, islink, join, realpath - VERSION = (14, 1, 0) @@ -810,6 +809,7 @@ class Kconfig(object): The current parsing location, for use in Python preprocessor functions. See the module docstring. """ + __slots__ = ( "_encoding", "_functions", @@ -848,7 +848,6 @@ class Kconfig(object): "warn_to_stderr", "warnings", "y", - # Parsing-related "_parsing_kconfigs", "_readline", @@ -866,9 +865,16 @@ class Kconfig(object): # Public interface # - def __init__(self, filename="Kconfig", warn=True, warn_to_stderr=True, - encoding="utf-8", suppress_traceback=False, search_paths=None, - allow_empty_macros=False): + def __init__( + self, + filename="Kconfig", + warn=True, + warn_to_stderr=True, + encoding="utf-8", + suppress_traceback=False, + search_paths=None, + allow_empty_macros=False, + ): """ Creates a new Kconfig object by parsing Kconfig files. Note that Kconfig files are not the same as .config files (which store @@ -972,8 +978,14 @@ class Kconfig(object): Pass True here to allow empty / undefined macros. """ try: - self._init(filename, warn, warn_to_stderr, encoding, search_paths, - allow_empty_macros) + self._init( + filename, + warn, + warn_to_stderr, + encoding, + search_paths, + allow_empty_macros, + ) except (EnvironmentError, KconfigError) as e: if suppress_traceback: cmd = sys.argv[0] # Empty string if missing @@ -985,8 +997,9 @@ class Kconfig(object): sys.exit(cmd + str(e).strip()) raise - def _init(self, filename, warn, warn_to_stderr, encoding, search_paths, - allow_empty_macros): + def _init( + self, filename, warn, warn_to_stderr, encoding, search_paths, allow_empty_macros + ): # See __init__() self._encoding = encoding @@ -1011,8 +1024,9 @@ class Kconfig(object): self.config_prefix = os.getenv("CONFIG_", "CONFIG_") # Regular expressions for parsing .config files self._set_match = _re_match(self.config_prefix + r"([^=]+)=(.*)") - self._unset_match = _re_match(r"# {}([^ ]+) is not set".format( - self.config_prefix)) + self._unset_match = _re_match( + r"# {}([^ ]+) is not set".format(self.config_prefix) + ) self.config_header = os.getenv("KCONFIG_CONFIG_HEADER", "") self.header_header = os.getenv("KCONFIG_AUTOHEADER_HEADER", "") @@ -1050,11 +1064,11 @@ class Kconfig(object): # Predefined preprocessor functions, with min/max number of arguments self._functions = { - "info": (_info_fn, 1, 1), - "error-if": (_error_if_fn, 2, 2), - "filename": (_filename_fn, 0, 0), - "lineno": (_lineno_fn, 0, 0), - "shell": (_shell_fn, 1, 1), + "info": (_info_fn, 1, 1), + "error-if": (_error_if_fn, 2, 2), + "filename": (_filename_fn, 0, 0), + "lineno": (_lineno_fn, 0, 0), + "shell": (_shell_fn, 1, 1), "warning-if": (_warning_if_fn, 2, 2), } @@ -1063,7 +1077,8 @@ class Kconfig(object): self._functions.update( importlib.import_module( os.getenv("KCONFIG_FUNCTIONS", "kconfigfunctions") - ).functions) + ).functions + ) except ImportError: pass @@ -1138,8 +1153,7 @@ class Kconfig(object): # KCONFIG_STRICT is an older alias for KCONFIG_WARN_UNDEF, supported # for backwards compatibility - if os.getenv("KCONFIG_WARN_UNDEF") == "y" or \ - os.getenv("KCONFIG_STRICT") == "y": + if os.getenv("KCONFIG_WARN_UNDEF") == "y" or os.getenv("KCONFIG_STRICT") == "y": self._check_undef_syms() @@ -1247,15 +1261,14 @@ class Kconfig(object): msg = None if filename is None: filename = standard_config_filename() - if not exists(filename) and \ - not exists(join(self.srctree, filename)): + if not exists(filename) and not exists(join(self.srctree, filename)): defconfig = self.defconfig_filename if defconfig is None: - return "Using default symbol values (no '{}')" \ - .format(filename) + return "Using default symbol values (no '{}')".format(filename) - msg = " default configuration '{}' (no '{}')" \ - .format(defconfig, filename) + msg = " default configuration '{}' (no '{}')".format( + defconfig, filename + ) filename = defconfig if not msg: @@ -1313,15 +1326,20 @@ class Kconfig(object): if sym.orig_type in _BOOL_TRISTATE: # The C implementation only checks the first character # to the right of '=', for whatever reason - if not (sym.orig_type is BOOL - and val.startswith(("y", "n")) or - sym.orig_type is TRISTATE - and val.startswith(("y", "m", "n"))): - self._warn("'{}' is not a valid value for the {} " - "symbol {}. Assignment ignored." - .format(val, TYPE_TO_STR[sym.orig_type], - sym.name_and_loc), - filename, linenr) + if not ( + sym.orig_type is BOOL + and val.startswith(("y", "n")) + or sym.orig_type is TRISTATE + and val.startswith(("y", "m", "n")) + ): + self._warn( + "'{}' is not a valid value for the {} " + "symbol {}. Assignment ignored.".format( + val, TYPE_TO_STR[sym.orig_type], sym.name_and_loc + ), + filename, + linenr, + ) continue val = val[0] @@ -1332,12 +1350,14 @@ class Kconfig(object): # to the choice symbols prev_mode = sym.choice.user_value - if prev_mode is not None and \ - TRI_TO_STR[prev_mode] != val: + if prev_mode is not None and TRI_TO_STR[prev_mode] != val: - self._warn("both m and y assigned to symbols " - "within the same choice", - filename, linenr) + self._warn( + "both m and y assigned to symbols " + "within the same choice", + filename, + linenr, + ) # Set the choice's mode sym.choice.set_value(val) @@ -1345,10 +1365,14 @@ class Kconfig(object): elif sym.orig_type is STRING: match = _conf_string_match(val) if not match: - self._warn("malformed string literal in " - "assignment to {}. Assignment ignored." - .format(sym.name_and_loc), - filename, linenr) + self._warn( + "malformed string literal in " + "assignment to {}. Assignment ignored.".format( + sym.name_and_loc + ), + filename, + linenr, + ) continue val = unescape(match.group(1)) @@ -1361,9 +1385,11 @@ class Kconfig(object): # lines or comments. 'line' has already been # rstrip()'d, so blank lines show up as "" here. if line and not line.lstrip().startswith("#"): - self._warn("ignoring malformed line '{}'" - .format(line), - filename, linenr) + self._warn( + "ignoring malformed line '{}'".format(line), + filename, + linenr, + ) continue @@ -1403,8 +1429,12 @@ class Kconfig(object): self.missing_syms.append((name, val)) if self.warn_assign_undef: self._warn( - "attempt to assign the value '{}' to the undefined symbol {}" - .format(val, name), filename, linenr) + "attempt to assign the value '{}' to the undefined symbol {}".format( + val, name + ), + filename, + linenr, + ) def _assigned_twice(self, sym, new_val, filename, linenr): # Called when a symbol is assigned more than once in a .config file @@ -1416,7 +1446,8 @@ class Kconfig(object): user_val = sym.user_value msg = '{} set more than once. Old value "{}", new value "{}".'.format( - sym.name_and_loc, user_val, new_val) + sym.name_and_loc, user_val, new_val + ) if user_val == new_val: if self.warn_assign_redun: @@ -1482,8 +1513,7 @@ class Kconfig(object): in tools, which can do e.g. print(kconf.write_autoconf()). """ if filename is None: - filename = os.getenv("KCONFIG_AUTOHEADER", - "include/generated/autoconf.h") + filename = os.getenv("KCONFIG_AUTOHEADER", "include/generated/autoconf.h") if self._write_if_changed(filename, self._autoconf_contents(header)): return "Kconfig header saved to '{}'".format(filename) @@ -1512,28 +1542,26 @@ class Kconfig(object): if sym.orig_type in _BOOL_TRISTATE: if val == "y": - add("#define {}{} 1\n" - .format(self.config_prefix, sym.name)) + add("#define {}{} 1\n".format(self.config_prefix, sym.name)) elif val == "m": - add("#define {}{}_MODULE 1\n" - .format(self.config_prefix, sym.name)) + add("#define {}{}_MODULE 1\n".format(self.config_prefix, sym.name)) elif sym.orig_type is STRING: - add('#define {}{} "{}"\n' - .format(self.config_prefix, sym.name, escape(val))) + add( + '#define {}{} "{}"\n'.format( + self.config_prefix, sym.name, escape(val) + ) + ) else: # sym.orig_type in _INT_HEX: - if sym.orig_type is HEX and \ - not val.startswith(("0x", "0X")): + if sym.orig_type is HEX and not val.startswith(("0x", "0X")): val = "0x" + val - add("#define {}{} {}\n" - .format(self.config_prefix, sym.name, val)) + add("#define {}{} {}\n".format(self.config_prefix, sym.name, val)) return "".join(chunks) - def write_config(self, filename=None, header=None, save_old=True, - verbose=None): + def write_config(self, filename=None, header=None, save_old=True, verbose=None): r""" Writes out symbol values in the .config format. The format matches the C implementation, including ordering. @@ -1647,9 +1675,12 @@ class Kconfig(object): node = node.parent # Add a comment when leaving visible menus - if node.item is MENU and expr_value(node.dep) and \ - expr_value(node.visibility) and \ - node is not self.top_node: + if ( + node.item is MENU + and expr_value(node.dep) + and expr_value(node.visibility) + and node is not self.top_node + ): add("# end of {}\n".format(node.prompt[0])) after_end_comment = True @@ -1680,9 +1711,9 @@ class Kconfig(object): add("\n") add(conf_string) - elif expr_value(node.dep) and \ - ((item is MENU and expr_value(node.visibility)) or - item is COMMENT): + elif expr_value(node.dep) and ( + (item is MENU and expr_value(node.visibility)) or item is COMMENT + ): add("\n#\n# {}\n#\n".format(node.prompt[0])) after_end_comment = False @@ -1738,8 +1769,7 @@ class Kconfig(object): # Skip symbols that cannot be changed. Only check # non-choice symbols, as selects don't affect choice # symbols. - if not sym.choice and \ - sym.visibility <= expr_value(sym.rev_dep): + if not sym.choice and sym.visibility <= expr_value(sym.rev_dep): continue # Skip symbols whose value matches their default @@ -1750,11 +1780,13 @@ class Kconfig(object): # choice, unless the choice is optional or the symbol type # isn't bool (it might be possible to set the choice mode # to n or the symbol to m in those cases). - if sym.choice and \ - not sym.choice.is_optional and \ - sym.choice._selection_from_defaults() is sym and \ - sym.orig_type is BOOL and \ - sym.tri_value == 2: + if ( + sym.choice + and not sym.choice.is_optional + and sym.choice._selection_from_defaults() is sym + and sym.orig_type is BOOL + and sym.tri_value == 2 + ): continue add(sym.config_string) @@ -1842,9 +1874,11 @@ class Kconfig(object): # making a missing symbol logically equivalent to n if sym._write_to_conf: - if sym._old_val is None and \ - sym.orig_type in _BOOL_TRISTATE and \ - val == "n": + if ( + sym._old_val is None + and sym.orig_type in _BOOL_TRISTATE + and val == "n" + ): # No old value (the symbol was missing or n), new value n. # No change. continue @@ -1924,17 +1958,20 @@ class Kconfig(object): # by passing a flag to it, plus we only need to look at symbols here. self._write_if_changed( - os.path.join(path, "auto.conf"), - self._old_vals_contents()) + os.path.join(path, "auto.conf"), self._old_vals_contents() + ) def _old_vals_contents(self): # _write_old_vals() helper. Returns the contents to write as a string. # Temporary list instead of generator makes this a bit faster - return "".join([ - sym.config_string for sym in self.unique_defined_syms + return "".join( + [ + sym.config_string + for sym in self.unique_defined_syms if not (sym.orig_type in _BOOL_TRISTATE and not sym.tri_value) - ]) + ] + ) def node_iter(self, unique_syms=False): """ @@ -2112,30 +2149,35 @@ class Kconfig(object): Returns a string with information about the Kconfig object when it is evaluated on e.g. the interactive Python prompt. """ + def status(flag): return "enabled" if flag else "disabled" - return "<{}>".format(", ".join(( - "configuration with {} symbols".format(len(self.syms)), - 'main menu prompt "{}"'.format(self.mainmenu_text), - "srctree is current directory" if not self.srctree else - 'srctree "{}"'.format(self.srctree), - 'config symbol prefix "{}"'.format(self.config_prefix), - "warnings " + status(self.warn), - "printing of warnings to stderr " + status(self.warn_to_stderr), - "undef. symbol assignment warnings " + - status(self.warn_assign_undef), - "overriding symbol assignment warnings " + - status(self.warn_assign_override), - "redundant symbol assignment warnings " + - status(self.warn_assign_redun) - ))) + return "<{}>".format( + ", ".join( + ( + "configuration with {} symbols".format(len(self.syms)), + 'main menu prompt "{}"'.format(self.mainmenu_text), + "srctree is current directory" + if not self.srctree + else 'srctree "{}"'.format(self.srctree), + 'config symbol prefix "{}"'.format(self.config_prefix), + "warnings " + status(self.warn), + "printing of warnings to stderr " + status(self.warn_to_stderr), + "undef. symbol assignment warnings " + + status(self.warn_assign_undef), + "overriding symbol assignment warnings " + + status(self.warn_assign_override), + "redundant symbol assignment warnings " + + status(self.warn_assign_redun), + ) + ) + ) # # Private methods # - # # File reading # @@ -2160,11 +2202,17 @@ class Kconfig(object): e = e2 raise _KconfigIOError( - e, "Could not open '{}' ({}: {}). Check that the $srctree " - "environment variable ({}) is set correctly." - .format(filename, errno.errorcode[e.errno], e.strerror, - "set to '{}'".format(self.srctree) if self.srctree - else "unset or blank")) + e, + "Could not open '{}' ({}: {}). Check that the $srctree " + "environment variable ({}) is set correctly.".format( + filename, + errno.errorcode[e.errno], + e.strerror, + "set to '{}'".format(self.srctree) + if self.srctree + else "unset or blank", + ), + ) def _enter_file(self, filename): # Jumps to the beginning of a sourced Kconfig file, saving the previous @@ -2179,7 +2227,7 @@ class Kconfig(object): if filename.startswith(self._srctree_prefix): # Relative path (or a redundant absolute path to within $srctree, # but it's probably fine to reduce those too) - rel_filename = filename[len(self._srctree_prefix):] + rel_filename = filename[len(self._srctree_prefix) :] else: # Absolute path rel_filename = filename @@ -2212,20 +2260,32 @@ class Kconfig(object): raise KconfigError( "\n{}:{}: recursive 'source' of '{}' detected. Check that " "environment variables are set correctly.\n" - "Include path:\n{}" - .format(self.filename, self.linenr, rel_filename, - "\n".join("{}:{}".format(name, linenr) - for name, linenr in self._include_path))) + "Include path:\n{}".format( + self.filename, + self.linenr, + rel_filename, + "\n".join( + "{}:{}".format(name, linenr) + for name, linenr in self._include_path + ), + ) + ) try: self._readline = self._open(filename, "r").readline except EnvironmentError as e: # We already know that the file exists raise _KconfigIOError( - e, "{}:{}: Could not open '{}' (in '{}') ({}: {})" - .format(self.filename, self.linenr, filename, - self._line.strip(), - errno.errorcode[e.errno], e.strerror)) + e, + "{}:{}: Could not open '{}' (in '{}') ({}: {})".format( + self.filename, + self.linenr, + filename, + self._line.strip(), + errno.errorcode[e.errno], + e.strerror, + ), + ) self.filename = rel_filename self.linenr = 0 @@ -2438,8 +2498,11 @@ class Kconfig(object): else: i = match.end() - token = self.const_syms[name] if name in STR_TO_TRI else \ - self._lookup_sym(name) + token = ( + self.const_syms[name] + if name in STR_TO_TRI + else self._lookup_sym(name) + ) else: # It's a case of missing quotes. For example, the @@ -2455,9 +2518,13 @@ class Kconfig(object): # Named choices ('choice FOO') also end up here. if token is not _T_CHOICE: - self._warn("style: quotes recommended around '{}' in '{}'" - .format(name, self._line.strip()), - self.filename, self.linenr) + self._warn( + "style: quotes recommended around '{}' in '{}'".format( + name, self._line.strip() + ), + self.filename, + self.linenr, + ) token = name i = match.end() @@ -2476,7 +2543,7 @@ class Kconfig(object): end_i = s.find(c, i + 1) + 1 if not end_i: self._parse_error("unterminated string") - val = s[i + 1:end_i - 1] + val = s[i + 1 : end_i - 1] i = end_i else: # Slow path @@ -2489,18 +2556,22 @@ class Kconfig(object): # # The preprocessor functionality changed how # environment variables are referenced, to $(FOO). - val = expandvars(s[i + 1:end_i - 1] - .replace("$UNAME_RELEASE", - _UNAME_RELEASE)) + val = expandvars( + s[i + 1 : end_i - 1].replace( + "$UNAME_RELEASE", _UNAME_RELEASE + ) + ) i = end_i # This is the only place where we don't survive with a # single token of lookback: 'option env="FOO"' does not # refer to a constant symbol named "FOO". - token = \ - val if token in _STRING_LEX or tokens[0] is _T_OPTION \ + token = ( + val + if token in _STRING_LEX or tokens[0] is _T_OPTION else self._lookup_const_sym(val) + ) elif s.startswith("&&", i): token = _T_AND @@ -2533,7 +2604,6 @@ class Kconfig(object): elif c == "#": break - # Very rare elif s.startswith("<=", i): @@ -2552,16 +2622,13 @@ class Kconfig(object): token = _T_GREATER i += 1 - else: self._parse_error("unknown tokens in line") - # Skip trailing whitespace while i < len(s) and s[i].isspace(): i += 1 - # Add the token tokens.append(token) @@ -2652,7 +2719,6 @@ class Kconfig(object): # Assigned variable name = s[:i] - # Extract assignment operator (=, :=, or +=) and value rhs_match = _assignment_rhs_match(s, i) if not rhs_match: @@ -2660,7 +2726,6 @@ class Kconfig(object): op, val = rhs_match.groups() - if name in self.variables: # Already seen variable var = self.variables[name] @@ -2686,8 +2751,9 @@ class Kconfig(object): else: # op == "+=" # += does immediate expansion if the variable was last set # with := - var.value += " " + (val if var.is_recursive else - self._expand_whole(val, ())) + var.value += " " + ( + val if var.is_recursive else self._expand_whole(val, ()) + ) def _expand_whole(self, s, args): # Expands preprocessor macros in all of 's'. Used whenever we don't @@ -2753,7 +2819,6 @@ class Kconfig(object): if not match: self._parse_error("unterminated string") - if match.group() == quote: # Found the end of the string return (s, match.end()) @@ -2762,7 +2827,7 @@ class Kconfig(object): # Replace '\x' with 'x'. 'i' ends up pointing to the character # after 'x', which allows macros to be canceled with '\$(foo)'. i = match.end() - s = s[:match.start()] + s[i:] + s = s[: match.start()] + s[i:] elif match.group() == "$(": # A macro call within the string @@ -2792,7 +2857,6 @@ class Kconfig(object): if not match: self._parse_error("missing end parenthesis in macro expansion") - if match.group() == "(": nesting += 1 i = match.end() @@ -2805,7 +2869,7 @@ class Kconfig(object): # Found the end of the macro - new_args.append(s[arg_start:match.start()]) + new_args.append(s[arg_start : match.start()]) # $(1) is replaced by the first argument to the function, etc., # provided at least that many arguments were passed @@ -2819,7 +2883,7 @@ class Kconfig(object): # and also go through the function value path res += self._fn_val(new_args) - return (res + s[match.end():], len(res)) + return (res + s[match.end() :], len(res)) elif match.group() == ",": i = match.end() @@ -2827,7 +2891,7 @@ class Kconfig(object): continue # Found the end of a macro argument - new_args.append(s[arg_start:match.start()]) + new_args.append(s[arg_start : match.start()]) arg_start = i else: # match.group() == "$(" @@ -2847,13 +2911,17 @@ class Kconfig(object): if len(args) == 1: # Plain variable if var._n_expansions: - self._parse_error("Preprocessor variable {} recursively " - "references itself".format(var.name)) + self._parse_error( + "Preprocessor variable {} recursively " + "references itself".format(var.name) + ) elif var._n_expansions > 100: # Allow functions to call themselves, but guess that functions # that are overly recursive are stuck - self._parse_error("Preprocessor function {} seems stuck " - "in infinite recursion".format(var.name)) + self._parse_error( + "Preprocessor function {} seems stuck " + "in infinite recursion".format(var.name) + ) var._n_expansions += 1 res = self._expand_whole(self.variables[fn].value, args) @@ -2865,8 +2933,9 @@ class Kconfig(object): py_fn, min_arg, max_arg = self._functions[fn] - if len(args) - 1 < min_arg or \ - (max_arg is not None and len(args) - 1 > max_arg): + if len(args) - 1 < min_arg or ( + max_arg is not None and len(args) - 1 > max_arg + ): if min_arg == max_arg: expected_args = min_arg @@ -2875,10 +2944,12 @@ class Kconfig(object): else: expected_args = "{}-{}".format(min_arg, max_arg) - raise KconfigError("{}:{}: bad number of arguments in call " - "to {}, expected {}, got {}" - .format(self.filename, self.linenr, fn, - expected_args, len(args) - 1)) + raise KconfigError( + "{}:{}: bad number of arguments in call " + "to {}, expected {}, got {}".format( + self.filename, self.linenr, fn, expected_args, len(args) - 1 + ) + ) return py_fn(self, *args) @@ -2962,7 +3033,7 @@ class Kconfig(object): node = MenuNode() node.kconfig = self node.item = sym - node.is_menuconfig = (t0 is _T_MENUCONFIG) + node.is_menuconfig = t0 is _T_MENUCONFIG node.prompt = node.help = node.list = None node.parent = parent node.filename = self.filename @@ -2974,8 +3045,11 @@ class Kconfig(object): self._parse_props(node) if node.is_menuconfig and not node.prompt: - self._warn("the menuconfig symbol {} has no prompt" - .format(sym.name_and_loc)) + self._warn( + "the menuconfig symbol {} has no prompt".format( + sym.name_and_loc + ) + ) # Equivalent to # @@ -3014,11 +3088,16 @@ class Kconfig(object): "{}:{}: '{}' not found (in '{}'). Check that " "environment variables are set correctly (e.g. " "$srctree, which is {}). Also note that unset " - "environment variables expand to the empty string." - .format(self.filename, self.linenr, pattern, - self._line.strip(), - "set to '{}'".format(self.srctree) - if self.srctree else "unset or blank")) + "environment variables expand to the empty string.".format( + self.filename, + self.linenr, + pattern, + self._line.strip(), + "set to '{}'".format(self.srctree) + if self.srctree + else "unset or blank", + ) + ) for filename in filenames: self._enter_file(filename) @@ -3125,20 +3204,28 @@ class Kconfig(object): # A valid endchoice/endif/endmenu is caught by the 'end_token' # check above self._parse_error( - "no corresponding 'choice'" if t0 is _T_ENDCHOICE else - "no corresponding 'if'" if t0 is _T_ENDIF else - "no corresponding 'menu'" if t0 is _T_ENDMENU else - "unrecognized construct") + "no corresponding 'choice'" + if t0 is _T_ENDCHOICE + else "no corresponding 'if'" + if t0 is _T_ENDIF + else "no corresponding 'menu'" + if t0 is _T_ENDMENU + else "unrecognized construct" + ) # End of file reached. Return the last node. if end_token: raise KconfigError( - "error: expected '{}' at end of '{}'" - .format("endchoice" if end_token is _T_ENDCHOICE else - "endif" if end_token is _T_ENDIF else - "endmenu", - self.filename)) + "error: expected '{}' at end of '{}'".format( + "endchoice" + if end_token is _T_ENDCHOICE + else "endif" + if end_token is _T_ENDIF + else "endmenu", + self.filename, + ) + ) return prev @@ -3187,8 +3274,7 @@ class Kconfig(object): if not self._check_token(_T_ON): self._parse_error("expected 'on' after 'depends'") - node.dep = self._make_and(node.dep, - self._expect_expr_and_eol()) + node.dep = self._make_and(node.dep, self._expect_expr_and_eol()) elif t0 is _T_HELP: self._parse_help(node) @@ -3197,42 +3283,40 @@ class Kconfig(object): if node.item.__class__ is not Symbol: self._parse_error("only symbols can select") - node.selects.append((self._expect_nonconst_sym(), - self._parse_cond())) + node.selects.append((self._expect_nonconst_sym(), self._parse_cond())) elif t0 is None: # Blank line continue elif t0 is _T_DEFAULT: - node.defaults.append((self._parse_expr(False), - self._parse_cond())) + node.defaults.append((self._parse_expr(False), self._parse_cond())) elif t0 in _DEF_TOKEN_TO_TYPE: self._set_type(node.item, _DEF_TOKEN_TO_TYPE[t0]) - node.defaults.append((self._parse_expr(False), - self._parse_cond())) + node.defaults.append((self._parse_expr(False), self._parse_cond())) elif t0 is _T_PROMPT: self._parse_prompt(node) elif t0 is _T_RANGE: - node.ranges.append((self._expect_sym(), self._expect_sym(), - self._parse_cond())) + node.ranges.append( + (self._expect_sym(), self._expect_sym(), self._parse_cond()) + ) elif t0 is _T_IMPLY: if node.item.__class__ is not Symbol: self._parse_error("only symbols can imply") - node.implies.append((self._expect_nonconst_sym(), - self._parse_cond())) + node.implies.append((self._expect_nonconst_sym(), self._parse_cond())) elif t0 is _T_VISIBLE: if not self._check_token(_T_IF): self._parse_error("expected 'if' after 'visible'") - node.visibility = self._make_and(node.visibility, - self._expect_expr_and_eol()) + node.visibility = self._make_and( + node.visibility, self._expect_expr_and_eol() + ) elif t0 is _T_OPTION: if self._check_token(_T_ENV): @@ -3244,33 +3328,42 @@ class Kconfig(object): if env_var in os.environ: node.defaults.append( - (self._lookup_const_sym(os.environ[env_var]), - self.y)) + (self._lookup_const_sym(os.environ[env_var]), self.y) + ) else: - self._warn("{1} has 'option env=\"{0}\"', " - "but the environment variable {0} is not " - "set".format(node.item.name, env_var), - self.filename, self.linenr) + self._warn( + "{1} has 'option env=\"{0}\"', " + "but the environment variable {0} is not " + "set".format(node.item.name, env_var), + self.filename, + self.linenr, + ) if env_var != node.item.name: - self._warn("Kconfiglib expands environment variables " - "in strings directly, meaning you do not " - "need 'option env=...' \"bounce\" symbols. " - "For compatibility with the C tools, " - "rename {} to {} (so that the symbol name " - "matches the environment variable name)." - .format(node.item.name, env_var), - self.filename, self.linenr) + self._warn( + "Kconfiglib expands environment variables " + "in strings directly, meaning you do not " + "need 'option env=...' \"bounce\" symbols. " + "For compatibility with the C tools, " + "rename {} to {} (so that the symbol name " + "matches the environment variable name).".format( + node.item.name, env_var + ), + self.filename, + self.linenr, + ) elif self._check_token(_T_DEFCONFIG_LIST): if not self.defconfig_list: self.defconfig_list = node.item else: - self._warn("'option defconfig_list' set on multiple " - "symbols ({0} and {1}). Only {0} will be " - "used.".format(self.defconfig_list.name, - node.item.name), - self.filename, self.linenr) + self._warn( + "'option defconfig_list' set on multiple " + "symbols ({0} and {1}). Only {0} will be " + "used.".format(self.defconfig_list.name, node.item.name), + self.filename, + self.linenr, + ) elif self._check_token(_T_MODULES): # To reduce warning spam, only warn if 'option modules' is @@ -3279,20 +3372,24 @@ class Kconfig(object): # modules besides the kernel yet, and there it's likely to # keep being called "MODULES". if node.item is not self.modules: - self._warn("the 'modules' option is not supported. " - "Let me know if this is a problem for you, " - "as it wouldn't be that hard to implement. " - "Note that modules are supported -- " - "Kconfiglib just assumes the symbol name " - "MODULES, like older versions of the C " - "implementation did when 'option modules' " - "wasn't used.", - self.filename, self.linenr) + self._warn( + "the 'modules' option is not supported. " + "Let me know if this is a problem for you, " + "as it wouldn't be that hard to implement. " + "Note that modules are supported -- " + "Kconfiglib just assumes the symbol name " + "MODULES, like older versions of the C " + "implementation did when 'option modules' " + "wasn't used.", + self.filename, + self.linenr, + ) elif self._check_token(_T_ALLNOCONFIG_Y): if node.item.__class__ is not Symbol: - self._parse_error("the 'allnoconfig_y' option is only " - "valid for symbols") + self._parse_error( + "the 'allnoconfig_y' option is only " "valid for symbols" + ) node.item.is_allnoconfig_y = True @@ -3315,8 +3412,11 @@ class Kconfig(object): # UNKNOWN is falsy if sc.orig_type and sc.orig_type is not new_type: - self._warn("{} defined with multiple types, {} will be used" - .format(sc.name_and_loc, TYPE_TO_STR[new_type])) + self._warn( + "{} defined with multiple types, {} will be used".format( + sc.name_and_loc, TYPE_TO_STR[new_type] + ) + ) sc.orig_type = new_type @@ -3326,8 +3426,10 @@ class Kconfig(object): # multiple times if node.prompt: - self._warn(node.item.name_and_loc + - " defined with multiple prompts in single location") + self._warn( + node.item.name_and_loc + + " defined with multiple prompts in single location" + ) prompt = self._tokens[1] self._tokens_i = 2 @@ -3336,8 +3438,10 @@ class Kconfig(object): self._parse_error("expected prompt string") if prompt != prompt.strip(): - self._warn(node.item.name_and_loc + - " has leading or trailing whitespace in its prompt") + self._warn( + node.item.name_and_loc + + " has leading or trailing whitespace in its prompt" + ) # This avoid issues for e.g. reStructuredText documentation, where # '*prompt *' is invalid @@ -3347,8 +3451,10 @@ class Kconfig(object): def _parse_help(self, node): if node.help is not None: - self._warn(node.item.name_and_loc + " defined with more than " - "one help text -- only the last one will be used") + self._warn( + node.item.name_and_loc + " defined with more than " + "one help text -- only the last one will be used" + ) # Micro-optimization. This code is pretty hot. readline = self._readline @@ -3403,8 +3509,7 @@ class Kconfig(object): self._line_after_help(line) def _empty_help(self, node, line): - self._warn(node.item.name_and_loc + - " has 'help' but empty help text") + self._warn(node.item.name_and_loc + " has 'help' but empty help text") node.help = "" if line: self._line_after_help(line) @@ -3447,8 +3552,11 @@ class Kconfig(object): # Return 'and_expr' directly if we have a "single-operand" OR. # Otherwise, parse the expression on the right and make an OR node. # This turns A || B || C || D into (OR, A, (OR, B, (OR, C, D))). - return and_expr if not self._check_token(_T_OR) else \ - (OR, and_expr, self._parse_expr(transform_m)) + return ( + and_expr + if not self._check_token(_T_OR) + else (OR, and_expr, self._parse_expr(transform_m)) + ) def _parse_and_expr(self, transform_m): factor = self._parse_factor(transform_m) @@ -3456,8 +3564,11 @@ class Kconfig(object): # Return 'factor' directly if we have a "single-operand" AND. # Otherwise, parse the right operand and make an AND node. This turns # A && B && C && D into (AND, A, (AND, B, (AND, C, D))). - return factor if not self._check_token(_T_AND) else \ - (AND, factor, self._parse_and_expr(transform_m)) + return ( + factor + if not self._check_token(_T_AND) + else (AND, factor, self._parse_and_expr(transform_m)) + ) def _parse_factor(self, transform_m): token = self._tokens[self._tokens_i] @@ -3481,8 +3592,7 @@ class Kconfig(object): # _T_EQUAL, _T_UNEQUAL, etc., deliberately have the same values as # EQUAL, UNEQUAL, etc., so we can just use the token directly self._tokens_i += 1 - return (self._tokens[self._tokens_i - 1], token, - self._expect_sym()) + return (self._tokens[self._tokens_i - 1], token, self._expect_sym()) if token is _T_NOT: # token == _T_NOT == NOT @@ -3689,36 +3799,43 @@ class Kconfig(object): if cur.item.__class__ in _SYMBOL_CHOICE: # Propagate 'visible if' and dependencies to the prompt if cur.prompt: - cur.prompt = (cur.prompt[0], - self._make_and( - cur.prompt[1], - self._make_and(visible_if, dep))) + cur.prompt = ( + cur.prompt[0], + self._make_and(cur.prompt[1], self._make_and(visible_if, dep)), + ) # Propagate dependencies to defaults if cur.defaults: - cur.defaults = [(default, self._make_and(cond, dep)) - for default, cond in cur.defaults] + cur.defaults = [ + (default, self._make_and(cond, dep)) + for default, cond in cur.defaults + ] # Propagate dependencies to ranges if cur.ranges: - cur.ranges = [(low, high, self._make_and(cond, dep)) - for low, high, cond in cur.ranges] + cur.ranges = [ + (low, high, self._make_and(cond, dep)) + for low, high, cond in cur.ranges + ] # Propagate dependencies to selects if cur.selects: - cur.selects = [(target, self._make_and(cond, dep)) - for target, cond in cur.selects] + cur.selects = [ + (target, self._make_and(cond, dep)) + for target, cond in cur.selects + ] # Propagate dependencies to implies if cur.implies: - cur.implies = [(target, self._make_and(cond, dep)) - for target, cond in cur.implies] + cur.implies = [ + (target, self._make_and(cond, dep)) + for target, cond in cur.implies + ] elif cur.prompt: # Not a symbol/choice # Propagate dependencies to the prompt. 'visible if' is only # propagated to symbols/choices. - cur.prompt = (cur.prompt[0], - self._make_and(cur.prompt[1], dep)) + cur.prompt = (cur.prompt[0], self._make_and(cur.prompt[1], dep)) cur = cur.next @@ -3744,16 +3861,14 @@ class Kconfig(object): # Modify the reverse dependencies of the selected symbol for target, cond in node.selects: - target.rev_dep = self._make_or( - target.rev_dep, - self._make_and(sym, cond)) + target.rev_dep = self._make_or(target.rev_dep, self._make_and(sym, cond)) # Modify the weak reverse dependencies of the implied # symbol for target, cond in node.implies: target.weak_rev_dep = self._make_or( - target.weak_rev_dep, - self._make_and(sym, cond)) + target.weak_rev_dep, self._make_and(sym, cond) + ) # # Misc. @@ -3781,82 +3896,106 @@ class Kconfig(object): for target_sym, _ in sym.selects: if target_sym.orig_type not in _BOOL_TRISTATE_UNKNOWN: - self._warn("{} selects the {} symbol {}, which is not " - "bool or tristate" - .format(sym.name_and_loc, - TYPE_TO_STR[target_sym.orig_type], - target_sym.name_and_loc)) + self._warn( + "{} selects the {} symbol {}, which is not " + "bool or tristate".format( + sym.name_and_loc, + TYPE_TO_STR[target_sym.orig_type], + target_sym.name_and_loc, + ) + ) for target_sym, _ in sym.implies: if target_sym.orig_type not in _BOOL_TRISTATE_UNKNOWN: - self._warn("{} implies the {} symbol {}, which is not " - "bool or tristate" - .format(sym.name_and_loc, - TYPE_TO_STR[target_sym.orig_type], - target_sym.name_and_loc)) + self._warn( + "{} implies the {} symbol {}, which is not " + "bool or tristate".format( + sym.name_and_loc, + TYPE_TO_STR[target_sym.orig_type], + target_sym.name_and_loc, + ) + ) elif sym.orig_type: # STRING/INT/HEX for default, _ in sym.defaults: if default.__class__ is not Symbol: raise KconfigError( "the {} symbol {} has a malformed default {} -- " - "expected a single symbol" - .format(TYPE_TO_STR[sym.orig_type], - sym.name_and_loc, expr_str(default))) + "expected a single symbol".format( + TYPE_TO_STR[sym.orig_type], + sym.name_and_loc, + expr_str(default), + ) + ) if sym.orig_type is STRING: - if not default.is_constant and not default.nodes and \ - not default.name.isupper(): + if ( + not default.is_constant + and not default.nodes + and not default.name.isupper() + ): # 'default foo' on a string symbol could be either a symbol # reference or someone leaving out the quotes. Guess that # the quotes were left out if 'foo' isn't all-uppercase # (and no symbol named 'foo' exists). - self._warn("style: quotes recommended around " - "default value for string symbol " - + sym.name_and_loc) + self._warn( + "style: quotes recommended around " + "default value for string symbol " + sym.name_and_loc + ) elif not num_ok(default, sym.orig_type): # INT/HEX - self._warn("the {0} symbol {1} has a non-{0} default {2}" - .format(TYPE_TO_STR[sym.orig_type], - sym.name_and_loc, - default.name_and_loc)) + self._warn( + "the {0} symbol {1} has a non-{0} default {2}".format( + TYPE_TO_STR[sym.orig_type], + sym.name_and_loc, + default.name_and_loc, + ) + ) if sym.selects or sym.implies: - self._warn("the {} symbol {} has selects or implies" - .format(TYPE_TO_STR[sym.orig_type], - sym.name_and_loc)) + self._warn( + "the {} symbol {} has selects or implies".format( + TYPE_TO_STR[sym.orig_type], sym.name_and_loc + ) + ) else: # UNKNOWN - self._warn("{} defined without a type" - .format(sym.name_and_loc)) - + self._warn("{} defined without a type".format(sym.name_and_loc)) if sym.ranges: if sym.orig_type not in _INT_HEX: self._warn( - "the {} symbol {} has ranges, but is not int or hex" - .format(TYPE_TO_STR[sym.orig_type], - sym.name_and_loc)) + "the {} symbol {} has ranges, but is not int or hex".format( + TYPE_TO_STR[sym.orig_type], sym.name_and_loc + ) + ) else: for low, high, _ in sym.ranges: - if not num_ok(low, sym.orig_type) or \ - not num_ok(high, sym.orig_type): - - self._warn("the {0} symbol {1} has a non-{0} " - "range [{2}, {3}]" - .format(TYPE_TO_STR[sym.orig_type], - sym.name_and_loc, - low.name_and_loc, - high.name_and_loc)) + if not num_ok(low, sym.orig_type) or not num_ok( + high, sym.orig_type + ): + + self._warn( + "the {0} symbol {1} has a non-{0} " + "range [{2}, {3}]".format( + TYPE_TO_STR[sym.orig_type], + sym.name_and_loc, + low.name_and_loc, + high.name_and_loc, + ) + ) def _check_choice_sanity(self): # Checks various choice properties that are handiest to check after # parsing. Only generates errors and warnings. def warn_select_imply(sym, expr, expr_type): - msg = "the choice symbol {} is {} by the following symbols, but " \ - "select/imply has no effect on choice symbols" \ - .format(sym.name_and_loc, expr_type) + msg = ( + "the choice symbol {} is {} by the following symbols, but " + "select/imply has no effect on choice symbols".format( + sym.name_and_loc, expr_type + ) + ) # si = select/imply for si in split_expr(expr, OR): @@ -3866,9 +4005,11 @@ class Kconfig(object): for choice in self.unique_choices: if choice.orig_type not in _BOOL_TRISTATE: - self._warn("{} defined with type {}" - .format(choice.name_and_loc, - TYPE_TO_STR[choice.orig_type])) + self._warn( + "{} defined with type {}".format( + choice.name_and_loc, TYPE_TO_STR[choice.orig_type] + ) + ) for node in choice.nodes: if node.prompt: @@ -3879,20 +4020,26 @@ class Kconfig(object): for default, _ in choice.defaults: if default.__class__ is not Symbol: raise KconfigError( - "{} has a malformed default {}" - .format(choice.name_and_loc, expr_str(default))) + "{} has a malformed default {}".format( + choice.name_and_loc, expr_str(default) + ) + ) if default.choice is not choice: - self._warn("the default selection {} of {} is not " - "contained in the choice" - .format(default.name_and_loc, - choice.name_and_loc)) + self._warn( + "the default selection {} of {} is not " + "contained in the choice".format( + default.name_and_loc, choice.name_and_loc + ) + ) for sym in choice.syms: if sym.defaults: - self._warn("default on the choice symbol {} will have " - "no effect, as defaults do not affect choice " - "symbols".format(sym.name_and_loc)) + self._warn( + "default on the choice symbol {} will have " + "no effect, as defaults do not affect choice " + "symbols".format(sym.name_and_loc) + ) if sym.rev_dep is not sym.kconfig.n: warn_select_imply(sym, sym.rev_dep, "selected") @@ -3903,19 +4050,28 @@ class Kconfig(object): for node in sym.nodes: if node.parent.item is choice: if not node.prompt: - self._warn("the choice symbol {} has no prompt" - .format(sym.name_and_loc)) + self._warn( + "the choice symbol {} has no prompt".format( + sym.name_and_loc + ) + ) elif node.prompt: - self._warn("the choice symbol {} is defined with a " - "prompt outside the choice" - .format(sym.name_and_loc)) + self._warn( + "the choice symbol {} is defined with a " + "prompt outside the choice".format(sym.name_and_loc) + ) def _parse_error(self, msg): - raise KconfigError("{}error: couldn't parse '{}': {}".format( - "" if self.filename is None else - "{}:{}: ".format(self.filename, self.linenr), - self._line.strip(), msg)) + raise KconfigError( + "{}error: couldn't parse '{}': {}".format( + "" + if self.filename is None + else "{}:{}: ".format(self.filename, self.linenr), + self._line.strip(), + msg, + ) + ) def _trailing_tokens_error(self): self._parse_error("extra tokens at end of line") @@ -3954,8 +4110,11 @@ class Kconfig(object): # - For Python 3, force the encoding. Forcing the encoding on Python 2 # turns strings into Unicode strings, which gets messy. Python 2 # doesn't decode regular strings anyway. - return open(filename, "rU" if mode == "r" else mode) if _IS_PY2 else \ - open(filename, mode, encoding=self._encoding) + return ( + open(filename, "rU" if mode == "r" else mode) + if _IS_PY2 + else open(filename, mode, encoding=self._encoding) + ) def _check_undef_syms(self): # Prints warnings for all references to undefined symbols within the @@ -3992,14 +4151,14 @@ class Kconfig(object): # symbols, but shouldn't be flagged # # - The MODULES symbol always exists - if not sym.nodes and not is_num(sym.name) and \ - sym.name != "MODULES": + if not sym.nodes and not is_num(sym.name) and sym.name != "MODULES": msg = "undefined symbol {}:".format(sym.name) for node in self.node_iter(): if sym in node.referenced: - msg += "\n\n- Referenced at {}:{}:\n\n{}" \ - .format(node.filename, node.linenr, node) + msg += "\n\n- Referenced at {}:{}:\n\n{}".format( + node.filename, node.linenr, node + ) self._warn(msg) def _warn(self, msg, filename=None, linenr=None): @@ -4274,6 +4433,7 @@ class Symbol(object): kconfig: The Kconfig instance this symbol is from. """ + __slots__ = ( "_cached_assignable", "_cached_str_val", @@ -4311,9 +4471,11 @@ class Symbol(object): """ See the class documentation. """ - if self.orig_type is TRISTATE and \ - (self.choice and self.choice.tri_value == 2 or - not self.kconfig.modules.tri_value): + if self.orig_type is TRISTATE and ( + self.choice + and self.choice.tri_value == 2 + or not self.kconfig.modules.tri_value + ): return BOOL @@ -4344,7 +4506,7 @@ class Symbol(object): # function call (property magic) vis = self.visibility - self._write_to_conf = (vis != 0) + self._write_to_conf = vis != 0 if self.orig_type in _INT_HEX: # The C implementation checks the user value against the range in a @@ -4361,10 +4523,16 @@ class Symbol(object): # The zeros are from the C implementation running strtoll() # on empty strings - low = int(low_expr.str_value, base) if \ - _is_base_n(low_expr.str_value, base) else 0 - high = int(high_expr.str_value, base) if \ - _is_base_n(high_expr.str_value, base) else 0 + low = ( + int(low_expr.str_value, base) + if _is_base_n(low_expr.str_value, base) + else 0 + ) + high = ( + int(high_expr.str_value, base) + if _is_base_n(high_expr.str_value, base) + else 0 + ) break else: @@ -4381,10 +4549,14 @@ class Symbol(object): self.kconfig._warn( "user value {} on the {} symbol {} ignored due to " "being outside the active range ([{}, {}]) -- falling " - "back on defaults" - .format(num2str(user_val), TYPE_TO_STR[self.orig_type], - self.name_and_loc, - num2str(low), num2str(high))) + "back on defaults".format( + num2str(user_val), + TYPE_TO_STR[self.orig_type], + self.name_and_loc, + num2str(low), + num2str(high), + ) + ) else: # If the user value is well-formed and satisfies range # contraints, it is stored in exactly the same form as @@ -4424,18 +4596,20 @@ class Symbol(object): if clamp is not None: # The value is rewritten to a standard form if it is # clamped - val = str(clamp) \ - if self.orig_type is INT else \ - hex(clamp) + val = str(clamp) if self.orig_type is INT else hex(clamp) if has_default: num2str = str if base == 10 else hex self.kconfig._warn( "default value {} on {} clamped to {} due to " - "being outside the active range ([{}, {}])" - .format(val_num, self.name_and_loc, - num2str(clamp), num2str(low), - num2str(high))) + "being outside the active range ([{}, {}])".format( + val_num, + self.name_and_loc, + num2str(clamp), + num2str(low), + num2str(high), + ) + ) elif self.orig_type is STRING: if vis and self.user_value is not None: @@ -4473,8 +4647,10 @@ class Symbol(object): # Would take some work to give the location here self.kconfig._warn( "The {} symbol {} is being evaluated in a logical context " - "somewhere. It will always evaluate to n." - .format(TYPE_TO_STR[self.orig_type], self.name_and_loc)) + "somewhere. It will always evaluate to n.".format( + TYPE_TO_STR[self.orig_type], self.name_and_loc + ) + ) self._cached_tri_val = 0 return 0 @@ -4482,7 +4658,7 @@ class Symbol(object): # Warning: See Symbol._rec_invalidate(), and note that this is a hidden # function call (property magic) vis = self.visibility - self._write_to_conf = (vis != 0) + self._write_to_conf = vis != 0 val = 0 @@ -4523,8 +4699,7 @@ class Symbol(object): # m is promoted to y for (1) bool symbols and (2) symbols with a # weak_rev_dep (from imply) of y - if val == 1 and \ - (self.type is BOOL or expr_value(self.weak_rev_dep) == 2): + if val == 1 and (self.type is BOOL or expr_value(self.weak_rev_dep) == 2): val = 2 elif vis == 2: @@ -4570,19 +4745,17 @@ class Symbol(object): return "" if self.orig_type in _BOOL_TRISTATE: - return "{}{}={}\n" \ - .format(self.kconfig.config_prefix, self.name, val) \ - if val != "n" else \ - "# {}{} is not set\n" \ - .format(self.kconfig.config_prefix, self.name) + return ( + "{}{}={}\n".format(self.kconfig.config_prefix, self.name, val) + if val != "n" + else "# {}{} is not set\n".format(self.kconfig.config_prefix, self.name) + ) if self.orig_type in _INT_HEX: - return "{}{}={}\n" \ - .format(self.kconfig.config_prefix, self.name, val) + return "{}{}={}\n".format(self.kconfig.config_prefix, self.name, val) # sym.orig_type is STRING - return '{}{}="{}"\n' \ - .format(self.kconfig.config_prefix, self.name, escape(val)) + return '{}{}="{}"\n'.format(self.kconfig.config_prefix, self.name, escape(val)) @property def name_and_loc(self): @@ -4646,21 +4819,31 @@ class Symbol(object): return True # Check if the value is valid for our type - if not (self.orig_type is BOOL and value in (2, 0) or - self.orig_type is TRISTATE and value in TRI_TO_STR or - value.__class__ is str and - (self.orig_type is STRING or - self.orig_type is INT and _is_base_n(value, 10) or - self.orig_type is HEX and _is_base_n(value, 16) - and int(value, 16) >= 0)): + if not ( + self.orig_type is BOOL + and value in (2, 0) + or self.orig_type is TRISTATE + and value in TRI_TO_STR + or value.__class__ is str + and ( + self.orig_type is STRING + or self.orig_type is INT + and _is_base_n(value, 10) + or self.orig_type is HEX + and _is_base_n(value, 16) + and int(value, 16) >= 0 + ) + ): # Display tristate values as n, m, y in the warning self.kconfig._warn( "the value {} is invalid for {}, which has type {} -- " - "assignment ignored" - .format(TRI_TO_STR[value] if value in TRI_TO_STR else - "'{}'".format(value), - self.name_and_loc, TYPE_TO_STR[self.orig_type])) + "assignment ignored".format( + TRI_TO_STR[value] if value in TRI_TO_STR else "'{}'".format(value), + self.name_and_loc, + TYPE_TO_STR[self.orig_type], + ) + ) return False @@ -4738,17 +4921,28 @@ class Symbol(object): add('"{}"'.format(node.prompt[0])) # Only add quotes for non-bool/tristate symbols - add("value " + (self.str_value if self.orig_type in _BOOL_TRISTATE - else '"{}"'.format(self.str_value))) + add( + "value " + + ( + self.str_value + if self.orig_type in _BOOL_TRISTATE + else '"{}"'.format(self.str_value) + ) + ) if not self.is_constant: # These aren't helpful to show for constant symbols if self.user_value is not None: # Only add quotes for non-bool/tristate symbols - add("user value " + (TRI_TO_STR[self.user_value] - if self.orig_type in _BOOL_TRISTATE - else '"{}"'.format(self.user_value))) + add( + "user value " + + ( + TRI_TO_STR[self.user_value] + if self.orig_type in _BOOL_TRISTATE + else '"{}"'.format(self.user_value) + ) + ) add("visibility " + TRI_TO_STR[self.visibility]) @@ -4798,8 +4992,7 @@ class Symbol(object): Works like Symbol.__str__(), but allows a custom format to be used for all symbol/choice references. See expr_str(). """ - return "\n\n".join(node.custom_str(sc_expr_str_fn) - for node in self.nodes) + return "\n\n".join(node.custom_str(sc_expr_str_fn) for node in self.nodes) # # Private methods @@ -4830,18 +5023,18 @@ class Symbol(object): self.implies = [] self.ranges = [] - self.user_value = \ - self.choice = \ - self.env_var = \ - self._cached_str_val = self._cached_tri_val = self._cached_vis = \ - self._cached_assignable = None + self.user_value = ( + self.choice + ) = ( + self.env_var + ) = ( + self._cached_str_val + ) = self._cached_tri_val = self._cached_vis = self._cached_assignable = None # _write_to_conf is calculated along with the value. If True, the # Symbol gets a .config entry. - self.is_allnoconfig_y = \ - self._was_set = \ - self._write_to_conf = False + self.is_allnoconfig_y = self._was_set = self._write_to_conf = False # See Kconfig._build_dep() self._dependents = set() @@ -4895,8 +5088,9 @@ class Symbol(object): def _invalidate(self): # Marks the symbol as needing to be recalculated - self._cached_str_val = self._cached_tri_val = self._cached_vis = \ - self._cached_assignable = None + self._cached_str_val = ( + self._cached_tri_val + ) = self._cached_vis = self._cached_assignable = None def _rec_invalidate(self): # Invalidates the symbol and all items that (possibly) depend on it @@ -4948,8 +5142,10 @@ class Symbol(object): return if self.kconfig._warn_assign_no_prompt: - self.kconfig._warn(self.name_and_loc + " has no prompt, meaning " - "user values have no effect on it") + self.kconfig._warn( + self.name_and_loc + " has no prompt, meaning " + "user values have no effect on it" + ) def _str_default(self): # write_min_config() helper function. Returns the value the symbol @@ -4968,9 +5164,7 @@ class Symbol(object): val = min(expr_value(default), cond_val) break - val = max(expr_value(self.rev_dep), - expr_value(self.weak_rev_dep), - val) + val = max(expr_value(self.rev_dep), expr_value(self.weak_rev_dep), val) # Transpose mod to yes if type is bool (possibly due to modules # being disabled) @@ -4992,11 +5186,15 @@ class Symbol(object): # and menus) is selected by some other symbol. Also warn if a symbol # whose direct dependencies evaluate to m is selected to y. - msg = "{} has direct dependencies {} with value {}, but is " \ - "currently being {}-selected by the following symbols:" \ - .format(self.name_and_loc, expr_str(self.direct_dep), - TRI_TO_STR[expr_value(self.direct_dep)], - TRI_TO_STR[expr_value(self.rev_dep)]) + msg = ( + "{} has direct dependencies {} with value {}, but is " + "currently being {}-selected by the following symbols:".format( + self.name_and_loc, + expr_str(self.direct_dep), + TRI_TO_STR[expr_value(self.direct_dep)], + TRI_TO_STR[expr_value(self.rev_dep)], + ) + ) # The reverse dependencies from each select are ORed together for select in split_expr(self.rev_dep, OR): @@ -5010,17 +5208,20 @@ class Symbol(object): # In both cases, we can split on AND and pick the first operand selecting_sym = split_expr(select, AND)[0] - msg += "\n - {}, with value {}, direct dependencies {} " \ - "(value: {})" \ - .format(selecting_sym.name_and_loc, - selecting_sym.str_value, - expr_str(selecting_sym.direct_dep), - TRI_TO_STR[expr_value(selecting_sym.direct_dep)]) + msg += ( + "\n - {}, with value {}, direct dependencies {} " + "(value: {})".format( + selecting_sym.name_and_loc, + selecting_sym.str_value, + expr_str(selecting_sym.direct_dep), + TRI_TO_STR[expr_value(selecting_sym.direct_dep)], + ) + ) if select.__class__ is tuple: - msg += ", and select condition {} (value: {})" \ - .format(expr_str(select[2]), - TRI_TO_STR[expr_value(select[2])]) + msg += ", and select condition {} (value: {})".format( + expr_str(select[2]), TRI_TO_STR[expr_value(select[2])] + ) self.kconfig._warn(msg) @@ -5182,6 +5383,7 @@ class Choice(object): kconfig: The Kconfig instance this choice is from. """ + __slots__ = ( "_cached_assignable", "_cached_selection", @@ -5299,16 +5501,22 @@ class Choice(object): self._was_set = True return True - if not (self.orig_type is BOOL and value in (2, 0) or - self.orig_type is TRISTATE and value in TRI_TO_STR): + if not ( + self.orig_type is BOOL + and value in (2, 0) + or self.orig_type is TRISTATE + and value in TRI_TO_STR + ): # Display tristate values as n, m, y in the warning self.kconfig._warn( "the value {} is invalid for {}, which has type {} -- " - "assignment ignored" - .format(TRI_TO_STR[value] if value in TRI_TO_STR else - "'{}'".format(value), - self.name_and_loc, TYPE_TO_STR[self.orig_type])) + "assignment ignored".format( + TRI_TO_STR[value] if value in TRI_TO_STR else "'{}'".format(value), + self.name_and_loc, + TYPE_TO_STR[self.orig_type], + ) + ) return False @@ -5346,8 +5554,10 @@ class Choice(object): Returns a string with information about the choice when it is evaluated on e.g. the interactive Python prompt. """ - fields = ["choice " + self.name if self.name else "choice", - TYPE_TO_STR[self.type]] + fields = [ + "choice " + self.name if self.name else "choice", + TYPE_TO_STR[self.type], + ] add = fields.append for node in self.nodes: @@ -5357,14 +5567,13 @@ class Choice(object): add("mode " + self.str_value) if self.user_value is not None: - add('user mode {}'.format(TRI_TO_STR[self.user_value])) + add("user mode {}".format(TRI_TO_STR[self.user_value])) if self.selection: add("{} selected".format(self.selection.name)) if self.user_selection: - user_sel_str = "{} selected by user" \ - .format(self.user_selection.name) + user_sel_str = "{} selected by user".format(self.user_selection.name) if self.selection is not self.user_selection: user_sel_str += " (overridden)" @@ -5399,8 +5608,7 @@ class Choice(object): Works like Choice.__str__(), but allows a custom format to be used for all symbol/choice references. See expr_str(). """ - return "\n\n".join(node.custom_str(sc_expr_str_fn) - for node in self.nodes) + return "\n\n".join(node.custom_str(sc_expr_str_fn) for node in self.nodes) # # Private methods @@ -5425,9 +5633,9 @@ class Choice(object): self.syms = [] self.defaults = [] - self.name = \ - self.user_value = self.user_selection = \ - self._cached_vis = self._cached_assignable = None + self.name = ( + self.user_value + ) = self.user_selection = self._cached_vis = self._cached_assignable = None self._cached_selection = _NO_CACHED_SELECTION @@ -5644,6 +5852,7 @@ class MenuNode(object): kconfig: The Kconfig instance the menu node is from. """ + __slots__ = ( "dep", "filename", @@ -5658,7 +5867,6 @@ class MenuNode(object): "parent", "prompt", "visibility", - # Properties "defaults", "selects", @@ -5689,32 +5897,28 @@ class MenuNode(object): """ See the class documentation. """ - return [(default, self._strip_dep(cond)) - for default, cond in self.defaults] + return [(default, self._strip_dep(cond)) for default, cond in self.defaults] @property def orig_selects(self): """ See the class documentation. """ - return [(select, self._strip_dep(cond)) - for select, cond in self.selects] + return [(select, self._strip_dep(cond)) for select, cond in self.selects] @property def orig_implies(self): """ See the class documentation. """ - return [(imply, self._strip_dep(cond)) - for imply, cond in self.implies] + return [(imply, self._strip_dep(cond)) for imply, cond in self.implies] @property def orig_ranges(self): """ See the class documentation. """ - return [(low, high, self._strip_dep(cond)) - for low, high, cond in self.ranges] + return [(low, high, self._strip_dep(cond)) for low, high, cond in self.ranges] @property def referenced(self): @@ -5774,8 +5978,11 @@ class MenuNode(object): add("menu node for comment") if self.prompt: - add('prompt "{}" (visibility {})'.format( - self.prompt[0], TRI_TO_STR[expr_value(self.prompt[1])])) + add( + 'prompt "{}" (visibility {})'.format( + self.prompt[0], TRI_TO_STR[expr_value(self.prompt[1])] + ) + ) if self.item.__class__ is Symbol and self.is_menuconfig: add("is menuconfig") @@ -5822,20 +6029,20 @@ class MenuNode(object): Works like MenuNode.__str__(), but allows a custom format to be used for all symbol/choice references. See expr_str(). """ - return self._menu_comment_node_str(sc_expr_str_fn) \ - if self.item in _MENU_COMMENT else \ - self._sym_choice_node_str(sc_expr_str_fn) + return ( + self._menu_comment_node_str(sc_expr_str_fn) + if self.item in _MENU_COMMENT + else self._sym_choice_node_str(sc_expr_str_fn) + ) def _menu_comment_node_str(self, sc_expr_str_fn): - s = '{} "{}"'.format("menu" if self.item is MENU else "comment", - self.prompt[0]) + s = '{} "{}"'.format("menu" if self.item is MENU else "comment", self.prompt[0]) if self.dep is not self.kconfig.y: s += "\n\tdepends on {}".format(expr_str(self.dep, sc_expr_str_fn)) if self.item is MENU and self.visibility is not self.kconfig.y: - s += "\n\tvisible if {}".format(expr_str(self.visibility, - sc_expr_str_fn)) + s += "\n\tvisible if {}".format(expr_str(self.visibility, sc_expr_str_fn)) return s @@ -5851,8 +6058,7 @@ class MenuNode(object): sc = self.item if sc.__class__ is Symbol: - lines = [("menuconfig " if self.is_menuconfig else "config ") - + sc.name] + lines = [("menuconfig " if self.is_menuconfig else "config ") + sc.name] else: lines = ["choice " + sc.name if sc.name else "choice"] @@ -5868,8 +6074,9 @@ class MenuNode(object): # Symbol defined without a type (which generates a warning) prefix = "prompt" - indent_add_cond(prefix + ' "{}"'.format(escape(self.prompt[0])), - self.orig_prompt[1]) + indent_add_cond( + prefix + ' "{}"'.format(escape(self.prompt[0])), self.orig_prompt[1] + ) if sc.__class__ is Symbol: if sc.is_allnoconfig_y: @@ -5886,13 +6093,12 @@ class MenuNode(object): for low, high, cond in self.orig_ranges: indent_add_cond( - "range {} {}".format(sc_expr_str_fn(low), - sc_expr_str_fn(high)), - cond) + "range {} {}".format(sc_expr_str_fn(low), sc_expr_str_fn(high)), + cond, + ) for default, cond in self.orig_defaults: - indent_add_cond("default " + expr_str(default, sc_expr_str_fn), - cond) + indent_add_cond("default " + expr_str(default, sc_expr_str_fn), cond) if sc.__class__ is Choice and sc.is_optional: indent_add("optional") @@ -5954,6 +6160,7 @@ class Variable(object): is_recursive: True if the variable is recursive (defined with =). """ + __slots__ = ( "_n_expansions", "is_recursive", @@ -5979,10 +6186,9 @@ class Variable(object): return self.kconfig._fn_val((self.name,) + args) def __repr__(self): - return "<variable {}, {}, value '{}'>" \ - .format(self.name, - "recursive" if self.is_recursive else "immediate", - self.value) + return "<variable {}, {}, value '{}'>".format( + self.name, "recursive" if self.is_recursive else "immediate", self.value + ) class KconfigError(Exception): @@ -5993,6 +6199,7 @@ class KconfigError(Exception): KconfigSyntaxError alias is only maintained for backwards compatibility. """ + KconfigSyntaxError = KconfigError # Backwards compatibility @@ -6010,7 +6217,8 @@ class _KconfigIOError(IOError): def __init__(self, ioerror, msg): self.msg = msg super(_KconfigIOError, self).__init__( - ioerror.errno, ioerror.strerror, ioerror.filename) + ioerror.errno, ioerror.strerror, ioerror.filename + ) def __str__(self): return self.msg @@ -6070,12 +6278,19 @@ def expr_value(expr): # parse as numbers comp = _strcmp(v1.str_value, v2.str_value) - return 2*(comp == 0 if rel is EQUAL else - comp != 0 if rel is UNEQUAL else - comp < 0 if rel is LESS else - comp <= 0 if rel is LESS_EQUAL else - comp > 0 if rel is GREATER else - comp >= 0) + return 2 * ( + comp == 0 + if rel is EQUAL + else comp != 0 + if rel is UNEQUAL + else comp < 0 + if rel is LESS + else comp <= 0 + if rel is LESS_EQUAL + else comp > 0 + if rel is GREATER + else comp >= 0 + ) def standard_sc_expr_str(sc): @@ -6115,14 +6330,18 @@ def expr_str(expr, sc_expr_str_fn=standard_sc_expr_str): return sc_expr_str_fn(expr) if expr[0] is AND: - return "{} && {}".format(_parenthesize(expr[1], OR, sc_expr_str_fn), - _parenthesize(expr[2], OR, sc_expr_str_fn)) + return "{} && {}".format( + _parenthesize(expr[1], OR, sc_expr_str_fn), + _parenthesize(expr[2], OR, sc_expr_str_fn), + ) if expr[0] is OR: # This turns A && B || C && D into "(A && B) || (C && D)", which is # redundant, but more readable - return "{} || {}".format(_parenthesize(expr[1], AND, sc_expr_str_fn), - _parenthesize(expr[2], AND, sc_expr_str_fn)) + return "{} || {}".format( + _parenthesize(expr[1], AND, sc_expr_str_fn), + _parenthesize(expr[2], AND, sc_expr_str_fn), + ) if expr[0] is NOT: if expr[1].__class__ is tuple: @@ -6133,8 +6352,9 @@ def expr_str(expr, sc_expr_str_fn=standard_sc_expr_str): # # Relation operands are always symbols (quoted strings are constant # symbols) - return "{} {} {}".format(sc_expr_str_fn(expr[1]), REL_TO_STR[expr[0]], - sc_expr_str_fn(expr[2])) + return "{} {} {}".format( + sc_expr_str_fn(expr[1]), REL_TO_STR[expr[0]], sc_expr_str_fn(expr[2]) + ) def expr_items(expr): @@ -6216,7 +6436,7 @@ def escape(s): replaced by \" and \\, respectively. """ # \ must be escaped before " to avoid double escaping - return s.replace("\\", r"\\").replace('"', r'\"') + return s.replace("\\", r"\\").replace('"', r"\"") def unescape(s): @@ -6226,6 +6446,7 @@ def unescape(s): """ return _unescape_sub(r"\1", s) + # unescape() helper _unescape_sub = re.compile(r"\\(.)").sub @@ -6245,15 +6466,16 @@ def standard_kconfig(description=None): import argparse parser = argparse.ArgumentParser( - formatter_class=argparse.RawDescriptionHelpFormatter, - description=description) + formatter_class=argparse.RawDescriptionHelpFormatter, description=description + ) parser.add_argument( "kconfig", metavar="KCONFIG", default="Kconfig", nargs="?", - help="Top-level Kconfig file (default: Kconfig)") + help="Top-level Kconfig file (default: Kconfig)", + ) return Kconfig(parser.parse_args().kconfig, suppress_traceback=True) @@ -6299,16 +6521,20 @@ def load_allconfig(kconf, filename): try: print(kconf.load_config("all.config", False)) except EnvironmentError as e2: - sys.exit("error: KCONFIG_ALLCONFIG is set, but neither {} " - "nor all.config could be opened: {}, {}" - .format(filename, std_msg(e1), std_msg(e2))) + sys.exit( + "error: KCONFIG_ALLCONFIG is set, but neither {} " + "nor all.config could be opened: {}, {}".format( + filename, std_msg(e1), std_msg(e2) + ) + ) else: try: print(kconf.load_config(allconfig, False)) except EnvironmentError as e: - sys.exit("error: KCONFIG_ALLCONFIG is set to '{}', which " - "could not be opened: {}" - .format(allconfig, std_msg(e))) + sys.exit( + "error: KCONFIG_ALLCONFIG is set to '{}', which " + "could not be opened: {}".format(allconfig, std_msg(e)) + ) kconf.warn_assign_override = old_warn_assign_override kconf.warn_assign_redun = old_warn_assign_redun @@ -6332,8 +6558,11 @@ def _visibility(sc): vis = max(vis, expr_value(node.prompt[1])) if sc.__class__ is Symbol and sc.choice: - if sc.choice.orig_type is TRISTATE and \ - sc.orig_type is not TRISTATE and sc.choice.tri_value != 2: + if ( + sc.choice.orig_type is TRISTATE + and sc.orig_type is not TRISTATE + and sc.choice.tri_value != 2 + ): # Non-tristate choice symbols are only visible in y mode return 0 @@ -6407,8 +6636,11 @@ def _sym_to_num(sym): # For BOOL and TRISTATE, n/m/y count as 0/1/2. This mirrors 9059a3493ef # ("kconfig: fix relational operators for bool and tristate symbols") in # the C implementation. - return sym.tri_value if sym.orig_type in _BOOL_TRISTATE else \ - int(sym.str_value, _TYPE_TO_BASE[sym.orig_type]) + return ( + sym.tri_value + if sym.orig_type in _BOOL_TRISTATE + else int(sym.str_value, _TYPE_TO_BASE[sym.orig_type]) + ) def _touch_dep_file(path, sym_name): @@ -6421,8 +6653,7 @@ def _touch_dep_file(path, sym_name): os.makedirs(sym_path_dir, 0o755) # A kind of truncating touch, mirroring the C tools - os.close(os.open( - sym_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)) + os.close(os.open(sym_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)) def _save_old(path): @@ -6431,6 +6662,7 @@ def _save_old(path): def copy(src, dst): # Import as needed, to save some startup time import shutil + shutil.copyfile(src, dst) if islink(path): @@ -6463,8 +6695,8 @@ def _locs(sc): if sc.nodes: return "(defined at {})".format( - ", ".join("{0.filename}:{0.linenr}".format(node) - for node in sc.nodes)) + ", ".join("{0.filename}:{0.linenr}".format(node) for node in sc.nodes) + ) return "(undefined)" @@ -6491,13 +6723,13 @@ def _expr_depends_on(expr, sym): elif left is not sym: return False - return (expr[0] is EQUAL and right is sym.kconfig.m or - right is sym.kconfig.y) or \ - (expr[0] is UNEQUAL and right is sym.kconfig.n) + return ( + expr[0] is EQUAL and right is sym.kconfig.m or right is sym.kconfig.y + ) or (expr[0] is UNEQUAL and right is sym.kconfig.n) - return expr[0] is AND and \ - (_expr_depends_on(expr[1], sym) or - _expr_depends_on(expr[2], sym)) + return expr[0] is AND and ( + _expr_depends_on(expr[1], sym) or _expr_depends_on(expr[2], sym) + ) def _auto_menu_dep(node1, node2): @@ -6505,8 +6737,7 @@ def _auto_menu_dep(node1, node2): # node2 has a prompt, we check its condition. Otherwise, we look directly # at node2.dep. - return _expr_depends_on(node2.prompt[1] if node2.prompt else node2.dep, - node1.item) + return _expr_depends_on(node2.prompt[1] if node2.prompt else node2.dep, node1.item) def _flatten(node): @@ -6521,8 +6752,7 @@ def _flatten(node): # you enter the choice at some location with a prompt. while node: - if node.list and not node.prompt and \ - node.item.__class__ is not Choice: + if node.list and not node.prompt and node.item.__class__ is not Choice: last_node = node.list while 1: @@ -6637,9 +6867,11 @@ def _check_dep_loop_sym(sym, ignore_choice): # # Since we aren't entering the choice via a choice symbol, all # choice symbols need to be checked, hence the None. - loop = _check_dep_loop_choice(dep, None) \ - if dep.__class__ is Choice \ - else _check_dep_loop_sym(dep, False) + loop = ( + _check_dep_loop_choice(dep, None) + if dep.__class__ is Choice + else _check_dep_loop_sym(dep, False) + ) if loop: # Dependency loop found @@ -6711,8 +6943,7 @@ def _found_dep_loop(loop, cur): # Yep, we have the entire loop. Throw an exception that shows it. - msg = "\nDependency loop\n" \ - "===============\n\n" + msg = "\nDependency loop\n" "===============\n\n" for item in loop: if item is not loop[0]: @@ -6720,8 +6951,7 @@ def _found_dep_loop(loop, cur): if item.__class__ is Symbol and item.choice: msg += "the choice symbol " - msg += "{}, with definition...\n\n{}\n\n" \ - .format(item.name_and_loc, item) + msg += "{}, with definition...\n\n{}\n\n".format(item.name_and_loc, item) # Small wart: Since we reuse the already calculated # Symbol/Choice._dependents sets for recursive dependency detection, we @@ -6738,12 +6968,14 @@ def _found_dep_loop(loop, cur): if item.__class__ is Symbol: if item.rev_dep is not item.kconfig.n: - msg += "(select-related dependencies: {})\n\n" \ - .format(expr_str(item.rev_dep)) + msg += "(select-related dependencies: {})\n\n".format( + expr_str(item.rev_dep) + ) if item.weak_rev_dep is not item.kconfig.n: - msg += "(imply-related dependencies: {})\n\n" \ - .format(expr_str(item.rev_dep)) + msg += "(imply-related dependencies: {})\n\n".format( + expr_str(item.rev_dep) + ) msg += "...depends again on " + loop[0].name_and_loc @@ -6765,11 +6997,14 @@ def _decoding_error(e, filename, macro_linenr=None): "Problematic data: {}\n" "Reason: {}".format( e.encoding, - "'{}'".format(filename) if macro_linenr is None else - "output from macro at {}:{}".format(filename, macro_linenr), - e.object[max(e.start - 40, 0):e.end + 40], - e.object[e.start:e.end], - e.reason)) + "'{}'".format(filename) + if macro_linenr is None + else "output from macro at {}:{}".format(filename, macro_linenr), + e.object[max(e.start - 40, 0) : e.end + 40], + e.object[e.start : e.end], + e.reason, + ) + ) def _warn_verbose_deprecated(fn_name): @@ -6779,7 +7014,8 @@ def _warn_verbose_deprecated(fn_name): "and is always generated. Do e.g. print(kconf.{0}()) if you want to " "want to show a message like \"Loaded configuration '.config'\" on " "stdout. The old API required ugly hacks to reuse messages in " - "configuration interfaces.\n".format(fn_name)) + "configuration interfaces.\n".format(fn_name) + ) # Predefined preprocessor functions @@ -6808,8 +7044,7 @@ def _warning_if_fn(kconf, _, cond, msg): def _error_if_fn(kconf, _, cond, msg): if cond == "y": - raise KconfigError("{}:{}: {}".format( - kconf.filename, kconf.linenr, msg)) + raise KconfigError("{}:{}: {}".format(kconf.filename, kconf.linenr, msg)) return "" @@ -6829,9 +7064,11 @@ def _shell_fn(kconf, _, command): _decoding_error(e, kconf.filename, kconf.linenr) if stderr: - kconf._warn("'{}' wrote to stderr: {}".format( - command, "\n".join(stderr.splitlines())), - kconf.filename, kconf.linenr) + kconf._warn( + "'{}' wrote to stderr: {}".format(command, "\n".join(stderr.splitlines())), + kconf.filename, + kconf.linenr, + ) # Universal newlines with splitlines() (to prevent e.g. stray \r's in # command output on Windows), trailing newline removal, and @@ -6842,6 +7079,7 @@ def _shell_fn(kconf, _, command): # parameter was added in 3.6), so we do this manual version instead. return "\n".join(stdout.splitlines()).rstrip("\n").replace("\n", " ") + # # Global constants # @@ -6871,6 +7109,7 @@ try: except AttributeError: # Only import as needed, to save some startup time import platform + _UNAME_RELEASE = platform.uname()[2] # The token and type constants below are safe to test with 'is', which is a bit @@ -6940,112 +7179,112 @@ except AttributeError: # Keyword to token map, with the get() method assigned directly as a small # optimization _get_keyword = { - "---help---": _T_HELP, - "allnoconfig_y": _T_ALLNOCONFIG_Y, - "bool": _T_BOOL, - "boolean": _T_BOOL, - "choice": _T_CHOICE, - "comment": _T_COMMENT, - "config": _T_CONFIG, - "def_bool": _T_DEF_BOOL, - "def_hex": _T_DEF_HEX, - "def_int": _T_DEF_INT, - "def_string": _T_DEF_STRING, - "def_tristate": _T_DEF_TRISTATE, - "default": _T_DEFAULT, + "---help---": _T_HELP, + "allnoconfig_y": _T_ALLNOCONFIG_Y, + "bool": _T_BOOL, + "boolean": _T_BOOL, + "choice": _T_CHOICE, + "comment": _T_COMMENT, + "config": _T_CONFIG, + "def_bool": _T_DEF_BOOL, + "def_hex": _T_DEF_HEX, + "def_int": _T_DEF_INT, + "def_string": _T_DEF_STRING, + "def_tristate": _T_DEF_TRISTATE, + "default": _T_DEFAULT, "defconfig_list": _T_DEFCONFIG_LIST, - "depends": _T_DEPENDS, - "endchoice": _T_ENDCHOICE, - "endif": _T_ENDIF, - "endmenu": _T_ENDMENU, - "env": _T_ENV, - "grsource": _T_ORSOURCE, # Backwards compatibility - "gsource": _T_OSOURCE, # Backwards compatibility - "help": _T_HELP, - "hex": _T_HEX, - "if": _T_IF, - "imply": _T_IMPLY, - "int": _T_INT, - "mainmenu": _T_MAINMENU, - "menu": _T_MENU, - "menuconfig": _T_MENUCONFIG, - "modules": _T_MODULES, - "on": _T_ON, - "option": _T_OPTION, - "optional": _T_OPTIONAL, - "orsource": _T_ORSOURCE, - "osource": _T_OSOURCE, - "prompt": _T_PROMPT, - "range": _T_RANGE, - "rsource": _T_RSOURCE, - "select": _T_SELECT, - "source": _T_SOURCE, - "string": _T_STRING, - "tristate": _T_TRISTATE, - "visible": _T_VISIBLE, + "depends": _T_DEPENDS, + "endchoice": _T_ENDCHOICE, + "endif": _T_ENDIF, + "endmenu": _T_ENDMENU, + "env": _T_ENV, + "grsource": _T_ORSOURCE, # Backwards compatibility + "gsource": _T_OSOURCE, # Backwards compatibility + "help": _T_HELP, + "hex": _T_HEX, + "if": _T_IF, + "imply": _T_IMPLY, + "int": _T_INT, + "mainmenu": _T_MAINMENU, + "menu": _T_MENU, + "menuconfig": _T_MENUCONFIG, + "modules": _T_MODULES, + "on": _T_ON, + "option": _T_OPTION, + "optional": _T_OPTIONAL, + "orsource": _T_ORSOURCE, + "osource": _T_OSOURCE, + "prompt": _T_PROMPT, + "range": _T_RANGE, + "rsource": _T_RSOURCE, + "select": _T_SELECT, + "source": _T_SOURCE, + "string": _T_STRING, + "tristate": _T_TRISTATE, + "visible": _T_VISIBLE, }.get # The constants below match the value of the corresponding tokens to remove the # need for conversion # Node types -MENU = _T_MENU +MENU = _T_MENU COMMENT = _T_COMMENT # Expression types -AND = _T_AND -OR = _T_OR -NOT = _T_NOT -EQUAL = _T_EQUAL -UNEQUAL = _T_UNEQUAL -LESS = _T_LESS -LESS_EQUAL = _T_LESS_EQUAL -GREATER = _T_GREATER +AND = _T_AND +OR = _T_OR +NOT = _T_NOT +EQUAL = _T_EQUAL +UNEQUAL = _T_UNEQUAL +LESS = _T_LESS +LESS_EQUAL = _T_LESS_EQUAL +GREATER = _T_GREATER GREATER_EQUAL = _T_GREATER_EQUAL REL_TO_STR = { - EQUAL: "=", - UNEQUAL: "!=", - LESS: "<", - LESS_EQUAL: "<=", - GREATER: ">", + EQUAL: "=", + UNEQUAL: "!=", + LESS: "<", + LESS_EQUAL: "<=", + GREATER: ">", GREATER_EQUAL: ">=", } # Symbol/choice types. UNKNOWN is 0 (falsy) to simplify some checks. # Client code shouldn't rely on it though, as it was non-zero in # older versions. -UNKNOWN = 0 -BOOL = _T_BOOL +UNKNOWN = 0 +BOOL = _T_BOOL TRISTATE = _T_TRISTATE -STRING = _T_STRING -INT = _T_INT -HEX = _T_HEX +STRING = _T_STRING +INT = _T_INT +HEX = _T_HEX TYPE_TO_STR = { - UNKNOWN: "unknown", - BOOL: "bool", + UNKNOWN: "unknown", + BOOL: "bool", TRISTATE: "tristate", - STRING: "string", - INT: "int", - HEX: "hex", + STRING: "string", + INT: "int", + HEX: "hex", } # Used in comparisons. 0 means the base is inferred from the format of the # string. _TYPE_TO_BASE = { - HEX: 16, - INT: 10, - STRING: 0, - UNKNOWN: 0, + HEX: 16, + INT: 10, + STRING: 0, + UNKNOWN: 0, } # def_bool -> BOOL, etc. _DEF_TOKEN_TO_TYPE = { - _T_DEF_BOOL: BOOL, - _T_DEF_HEX: HEX, - _T_DEF_INT: INT, - _T_DEF_STRING: STRING, + _T_DEF_BOOL: BOOL, + _T_DEF_HEX: HEX, + _T_DEF_INT: INT, + _T_DEF_STRING: STRING, _T_DEF_TRISTATE: TRISTATE, } @@ -7056,91 +7295,115 @@ _DEF_TOKEN_TO_TYPE = { # Identifier-like lexemes ("missing quotes") are also treated as strings after # these tokens. _T_CHOICE is included to avoid symbols being registered for # named choices. -_STRING_LEX = frozenset({ - _T_BOOL, - _T_CHOICE, - _T_COMMENT, - _T_HEX, - _T_INT, - _T_MAINMENU, - _T_MENU, - _T_ORSOURCE, - _T_OSOURCE, - _T_PROMPT, - _T_RSOURCE, - _T_SOURCE, - _T_STRING, - _T_TRISTATE, -}) +_STRING_LEX = frozenset( + { + _T_BOOL, + _T_CHOICE, + _T_COMMENT, + _T_HEX, + _T_INT, + _T_MAINMENU, + _T_MENU, + _T_ORSOURCE, + _T_OSOURCE, + _T_PROMPT, + _T_RSOURCE, + _T_SOURCE, + _T_STRING, + _T_TRISTATE, + } +) # Various sets for quick membership tests. Gives a single global lookup and # avoids creating temporary dicts/tuples. -_TYPE_TOKENS = frozenset({ - _T_BOOL, - _T_TRISTATE, - _T_INT, - _T_HEX, - _T_STRING, -}) - -_SOURCE_TOKENS = frozenset({ - _T_SOURCE, - _T_RSOURCE, - _T_OSOURCE, - _T_ORSOURCE, -}) - -_REL_SOURCE_TOKENS = frozenset({ - _T_RSOURCE, - _T_ORSOURCE, -}) +_TYPE_TOKENS = frozenset( + { + _T_BOOL, + _T_TRISTATE, + _T_INT, + _T_HEX, + _T_STRING, + } +) + +_SOURCE_TOKENS = frozenset( + { + _T_SOURCE, + _T_RSOURCE, + _T_OSOURCE, + _T_ORSOURCE, + } +) + +_REL_SOURCE_TOKENS = frozenset( + { + _T_RSOURCE, + _T_ORSOURCE, + } +) # Obligatory (non-optional) sources -_OBL_SOURCE_TOKENS = frozenset({ - _T_SOURCE, - _T_RSOURCE, -}) - -_BOOL_TRISTATE = frozenset({ - BOOL, - TRISTATE, -}) - -_BOOL_TRISTATE_UNKNOWN = frozenset({ - BOOL, - TRISTATE, - UNKNOWN, -}) - -_INT_HEX = frozenset({ - INT, - HEX, -}) - -_SYMBOL_CHOICE = frozenset({ - Symbol, - Choice, -}) - -_MENU_COMMENT = frozenset({ - MENU, - COMMENT, -}) - -_EQUAL_UNEQUAL = frozenset({ - EQUAL, - UNEQUAL, -}) - -_RELATIONS = frozenset({ - EQUAL, - UNEQUAL, - LESS, - LESS_EQUAL, - GREATER, - GREATER_EQUAL, -}) +_OBL_SOURCE_TOKENS = frozenset( + { + _T_SOURCE, + _T_RSOURCE, + } +) + +_BOOL_TRISTATE = frozenset( + { + BOOL, + TRISTATE, + } +) + +_BOOL_TRISTATE_UNKNOWN = frozenset( + { + BOOL, + TRISTATE, + UNKNOWN, + } +) + +_INT_HEX = frozenset( + { + INT, + HEX, + } +) + +_SYMBOL_CHOICE = frozenset( + { + Symbol, + Choice, + } +) + +_MENU_COMMENT = frozenset( + { + MENU, + COMMENT, + } +) + +_EQUAL_UNEQUAL = frozenset( + { + EQUAL, + UNEQUAL, + } +) + +_RELATIONS = frozenset( + { + EQUAL, + UNEQUAL, + LESS, + LESS_EQUAL, + GREATER, + GREATER_EQUAL, + } +) # Helper functions for getting compiled regular expressions, with the needed # matching function returned directly as a small optimization. @@ -7189,7 +7452,7 @@ _string_special_search = _re_search(r'"|\'|\\|\$\(') # Special characters/strings while expanding a symbol name. Also includes # end-of-line, in case the macro is the last thing on the line. -_name_special_search = _re_search(r'[^A-Za-z0-9_$/.-]|\$\(|$') +_name_special_search = _re_search(r"[^A-Za-z0-9_$/.-]|\$\(|$") # A valid right-hand side for an assignment to a string symbol in a .config # file, including escaped characters. Extracts the contents. diff --git a/util/run_ects.py b/util/run_ects.py index 9178328e5f..9293f60779 100644 --- a/util/run_ects.py +++ b/util/run_ects.py @@ -16,81 +16,81 @@ import subprocess import sys # List of tests to run. -TESTS = ['meta', 'gpio', 'hook', 'i2c', 'interrupt', 'mutex', 'task', 'timer'] +TESTS = ["meta", "gpio", "hook", "i2c", "interrupt", "mutex", "task", "timer"] class CtsRunner(object): - """Class running eCTS tests.""" - - def __init__(self, ec_dir, dryrun): - self.ec_dir = ec_dir - self.cts_py = [] - if dryrun: - self.cts_py += ['echo'] - self.cts_py += [os.path.join(ec_dir, 'cts/cts.py')] - - def run_cmd(self, cmd): - try: - rc = subprocess.call(cmd) - if rc != 0: - return False - except OSError: - return False - return True - - def run_test(self, test): - cmd = self.cts_py + ['-m', test] - self.run_cmd(cmd) - - def run(self, tests): - for test in tests: - logging.info('Running', test, 'test.') - self.run_test(test) - - def sync(self): - logging.info('Syncing tree...') - os.chdir(self.ec_dir) - cmd = ['repo', 'sync', '.'] - return self.run_cmd(cmd) - - def upload(self): - logging.info('Uploading results...') + """Class running eCTS tests.""" + + def __init__(self, ec_dir, dryrun): + self.ec_dir = ec_dir + self.cts_py = [] + if dryrun: + self.cts_py += ["echo"] + self.cts_py += [os.path.join(ec_dir, "cts/cts.py")] + + def run_cmd(self, cmd): + try: + rc = subprocess.call(cmd) + if rc != 0: + return False + except OSError: + return False + return True + + def run_test(self, test): + cmd = self.cts_py + ["-m", test] + self.run_cmd(cmd) + + def run(self, tests): + for test in tests: + logging.info("Running", test, "test.") + self.run_test(test) + + def sync(self): + logging.info("Syncing tree...") + os.chdir(self.ec_dir) + cmd = ["repo", "sync", "."] + return self.run_cmd(cmd) + + def upload(self): + logging.info("Uploading results...") def main(): - if not os.path.exists('/etc/cros_chroot_version'): - logging.error('This script has to run inside chroot.') - sys.exit(-1) - - ec_dir = os.path.realpath(os.path.dirname(__file__) + '/..') - - parser = argparse.ArgumentParser(description='Run eCTS and report results.') - parser.add_argument('-d', - '--dryrun', - action='store_true', - help='Echo commands to be executed without running them.') - parser.add_argument('-s', - '--sync', - action='store_true', - help='Sync tree before running tests.') - parser.add_argument('-u', - '--upload', - action='store_true', - help='Upload test results.') - args = parser.parse_args() - - runner = CtsRunner(ec_dir, args.dryrun) - - if args.sync: - if not runner.sync(): - logging.error('Failed to sync.') - sys.exit(-1) - - runner.run(TESTS) - - if args.upload: - runner.upload() - - -if __name__ == '__main__': - main() + if not os.path.exists("/etc/cros_chroot_version"): + logging.error("This script has to run inside chroot.") + sys.exit(-1) + + ec_dir = os.path.realpath(os.path.dirname(__file__) + "/..") + + parser = argparse.ArgumentParser(description="Run eCTS and report results.") + parser.add_argument( + "-d", + "--dryrun", + action="store_true", + help="Echo commands to be executed without running them.", + ) + parser.add_argument( + "-s", "--sync", action="store_true", help="Sync tree before running tests." + ) + parser.add_argument( + "-u", "--upload", action="store_true", help="Upload test results." + ) + args = parser.parse_args() + + runner = CtsRunner(ec_dir, args.dryrun) + + if args.sync: + if not runner.sync(): + logging.error("Failed to sync.") + sys.exit(-1) + + runner.run(TESTS) + + if args.upload: + runner.upload() + + +if __name__ == "__main__": + main() diff --git a/util/test_kconfig_check.py b/util/test_kconfig_check.py index cd1b9bf098..db73e7ee71 100644 --- a/util/test_kconfig_check.py +++ b/util/test_kconfig_check.py @@ -16,7 +16,8 @@ import kconfig_check # Prefix that we strip from each Kconfig option, when considering whether it is # equivalent to a CONFIG option with the same name -PREFIX = 'PLATFORM_EC_' +PREFIX = "PLATFORM_EC_" + @contextlib.contextmanager def capture_sys_output(): @@ -39,38 +40,49 @@ def capture_sys_output(): # directly from Python. You can still run this test with 'pytest' if you like. class KconfigCheck(unittest.TestCase): """Tests for the KconfigCheck class""" + def test_simple_check(self): """Check it detected a new ad-hoc CONFIG""" checker = kconfig_check.KconfigCheck() - self.assertEqual(['NEW_ONE'], checker.find_new_adhoc( - configs=['NEW_ONE', 'OLD_ONE', 'IN_KCONFIG'], - kconfigs=['IN_KCONFIG'], - allowed=['OLD_ONE'])) + self.assertEqual( + ["NEW_ONE"], + checker.find_new_adhoc( + configs=["NEW_ONE", "OLD_ONE", "IN_KCONFIG"], + kconfigs=["IN_KCONFIG"], + allowed=["OLD_ONE"], + ), + ) def test_sorted_check(self): """Check it sorts the results in order""" checker = kconfig_check.KconfigCheck() self.assertSequenceEqual( - ['ANOTHER_NEW_ONE', 'NEW_ONE'], + ["ANOTHER_NEW_ONE", "NEW_ONE"], checker.find_new_adhoc( - configs=['NEW_ONE', 'ANOTHER_NEW_ONE', 'OLD_ONE', 'IN_KCONFIG'], - kconfigs=['IN_KCONFIG'], - allowed=['OLD_ONE'])) + configs=["NEW_ONE", "ANOTHER_NEW_ONE", "OLD_ONE", "IN_KCONFIG"], + kconfigs=["IN_KCONFIG"], + allowed=["OLD_ONE"], + ), + ) def check_read_configs(self, use_defines): checker = kconfig_check.KconfigCheck() with tempfile.NamedTemporaryFile() as configs: - with open(configs.name, 'w') as out: - prefix = '#define ' if use_defines else '' - suffix = ' ' if use_defines else '=' - out.write(f'''{prefix}CONFIG_OLD_ONE{suffix}y + with open(configs.name, "w") as out: + prefix = "#define " if use_defines else "" + suffix = " " if use_defines else "=" + out.write( + f"""{prefix}CONFIG_OLD_ONE{suffix}y {prefix}NOT_A_CONFIG{suffix} {prefix}CONFIG_STRING{suffix}"something" {prefix}CONFIG_INT{suffix}123 {prefix}CONFIG_HEX{suffix}45ab -''') - self.assertEqual(['OLD_ONE', 'STRING', 'INT', 'HEX'], - checker.read_configs(configs.name, use_defines)) +""" + ) + self.assertEqual( + ["OLD_ONE", "STRING", "INT", "HEX"], + checker.read_configs(configs.name, use_defines), + ) def test_read_configs(self): """Test KconfigCheck.read_configs()""" @@ -87,22 +99,24 @@ class KconfigCheck(unittest.TestCase): Args: srctree: Directory to write to """ - with open(os.path.join(srctree, 'Kconfig'), 'w') as out: - out.write(f'''config {PREFIX}MY_KCONFIG + with open(os.path.join(srctree, "Kconfig"), "w") as out: + out.write( + f"""config {PREFIX}MY_KCONFIG \tbool "my kconfig" rsource "subdir/Kconfig.wibble" -''') - subdir = os.path.join(srctree, 'subdir') +""" + ) + subdir = os.path.join(srctree, "subdir") os.mkdir(subdir) - with open(os.path.join(subdir, 'Kconfig.wibble'), 'w') as out: - out.write('menuconfig %sMENU_KCONFIG\n' % PREFIX) + with open(os.path.join(subdir, "Kconfig.wibble"), "w") as out: + out.write("menuconfig %sMENU_KCONFIG\n" % PREFIX) # Add a directory which should be ignored - bad_subdir = os.path.join(subdir, 'Kconfig') + bad_subdir = os.path.join(subdir, "Kconfig") os.mkdir(bad_subdir) - with open(os.path.join(bad_subdir, 'Kconfig.bad'), 'w') as out: - out.write('menuconfig %sBAD_KCONFIG' % PREFIX) + with open(os.path.join(bad_subdir, "Kconfig.bad"), "w") as out: + out.write("menuconfig %sBAD_KCONFIG" % PREFIX) def test_find_kconfigs(self): """Test KconfigCheck.find_kconfigs()""" @@ -110,20 +124,20 @@ rsource "subdir/Kconfig.wibble" with tempfile.TemporaryDirectory() as srctree: self.setup_srctree(srctree) files = checker.find_kconfigs(srctree) - fnames = [fname[len(srctree):] for fname in files] - self.assertEqual(['/Kconfig', '/subdir/Kconfig.wibble'], fnames) + fnames = [fname[len(srctree) :] for fname in files] + self.assertEqual(["/Kconfig", "/subdir/Kconfig.wibble"], fnames) def test_scan_kconfigs(self): """Test KconfigCheck.scan_configs()""" checker = kconfig_check.KconfigCheck() with tempfile.TemporaryDirectory() as srctree: self.setup_srctree(srctree) - self.assertEqual(['MENU_KCONFIG', 'MY_KCONFIG'], - checker.scan_kconfigs(srctree, PREFIX)) + self.assertEqual( + ["MENU_KCONFIG", "MY_KCONFIG"], checker.scan_kconfigs(srctree, PREFIX) + ) @classmethod - def setup_allowed_and_configs(cls, allowed_fname, configs_fname, - add_new_one=True): + def setup_allowed_and_configs(cls, allowed_fname, configs_fname, add_new_one=True): """Set up the 'allowed' and 'configs' files for tests Args: @@ -131,14 +145,14 @@ rsource "subdir/Kconfig.wibble" configs_fname: Filename to which CONFIGs to check should be written add_new_one: True to add CONFIG_NEW_ONE to the configs_fname file """ - with open(allowed_fname, 'w') as out: - out.write('CONFIG_OLD_ONE\n') - out.write('CONFIG_MENU_KCONFIG\n') - with open(configs_fname, 'w') as out: - to_add = ['CONFIG_OLD_ONE', 'CONFIG_MY_KCONFIG'] + with open(allowed_fname, "w") as out: + out.write("CONFIG_OLD_ONE\n") + out.write("CONFIG_MENU_KCONFIG\n") + with open(configs_fname, "w") as out: + to_add = ["CONFIG_OLD_ONE", "CONFIG_MY_KCONFIG"] if add_new_one: - to_add.append('CONFIG_NEW_ONE') - out.write('\n'.join(to_add)) + to_add.append("CONFIG_NEW_ONE") + out.write("\n".join(to_add)) def test_check_adhoc_configs(self): """Test KconfigCheck.check_adhoc_configs()""" @@ -148,12 +162,16 @@ rsource "subdir/Kconfig.wibble" with tempfile.NamedTemporaryFile() as allowed: with tempfile.NamedTemporaryFile() as configs: self.setup_allowed_and_configs(allowed.name, configs.name) - new_adhoc, unneeded_adhoc, updated_adhoc = ( - checker.check_adhoc_configs( - configs.name, srctree, allowed.name, PREFIX)) - self.assertEqual(['NEW_ONE'], new_adhoc) - self.assertEqual(['MENU_KCONFIG'], unneeded_adhoc) - self.assertEqual(['OLD_ONE'], updated_adhoc) + ( + new_adhoc, + unneeded_adhoc, + updated_adhoc, + ) = checker.check_adhoc_configs( + configs.name, srctree, allowed.name, PREFIX + ) + self.assertEqual(["NEW_ONE"], new_adhoc) + self.assertEqual(["MENU_KCONFIG"], unneeded_adhoc) + self.assertEqual(["OLD_ONE"], updated_adhoc) def test_check(self): """Test running the 'check' subcommand""" @@ -162,29 +180,39 @@ rsource "subdir/Kconfig.wibble" self.setup_srctree(srctree) with tempfile.NamedTemporaryFile() as allowed: with tempfile.NamedTemporaryFile() as configs: - self.setup_allowed_and_configs(allowed.name, - configs.name) + self.setup_allowed_and_configs(allowed.name, configs.name) ret_code = kconfig_check.main( - ['-c', configs.name, '-s', srctree, - '-a', allowed.name, '-p', PREFIX, 'check']) + [ + "-c", + configs.name, + "-s", + srctree, + "-a", + allowed.name, + "-p", + PREFIX, + "check", + ] + ) self.assertEqual(1, ret_code) - self.assertEqual('', stdout.getvalue()) - found = re.findall('(CONFIG_.*)', stderr.getvalue()) - self.assertEqual(['CONFIG_NEW_ONE'], found) + self.assertEqual("", stdout.getvalue()) + found = re.findall("(CONFIG_.*)", stderr.getvalue()) + self.assertEqual(["CONFIG_NEW_ONE"], found) def test_real_kconfig(self): """Same Kconfig should be returned for kconfiglib / adhoc""" if not kconfig_check.USE_KCONFIGLIB: - self.skipTest('No kconfiglib available') - zephyr_path = pathlib.Path('../../third_party/zephyr/main').resolve() + self.skipTest("No kconfiglib available") + zephyr_path = pathlib.Path("../../third_party/zephyr/main").resolve() if not zephyr_path.exists(): - self.skipTest('No zephyr tree available') + self.skipTest("No zephyr tree available") checker = kconfig_check.KconfigCheck() - srcdir = 'zephyr' + srcdir = "zephyr" search_paths = [zephyr_path] kc_version = checker.scan_kconfigs( - srcdir, search_paths=search_paths, try_kconfiglib=True) + srcdir, search_paths=search_paths, try_kconfiglib=True + ) adhoc_version = checker.scan_kconfigs(srcdir, try_kconfiglib=False) # List of things missing from the Kconfig @@ -192,15 +220,17 @@ rsource "subdir/Kconfig.wibble" # The Kconfig is disjoint in some places, e.g. the boards have their # own Kconfig files which are not included from the main Kconfig - missing = [item for item in missing - if not item.startswith('BOARD') and - not item.startswith('VARIANT')] + missing = [ + item + for item in missing + if not item.startswith("BOARD") and not item.startswith("VARIANT") + ] # Similarly, some other items are defined in files that are not included # in all cases, only for particular values of $(ARCH) self.assertEqual( - ['FLASH_LOAD_OFFSET', 'NPCX_HEADER', 'SYS_CLOCK_HW_CYCLES_PER_SEC'], - missing) + ["FLASH_LOAD_OFFSET", "NPCX_HEADER", "SYS_CLOCK_HW_CYCLES_PER_SEC"], missing + ) def test_check_unneeded(self): """Test running the 'check' subcommand with unneeded ad-hoc configs""" @@ -209,18 +239,29 @@ rsource "subdir/Kconfig.wibble" self.setup_srctree(srctree) with tempfile.NamedTemporaryFile() as allowed: with tempfile.NamedTemporaryFile() as configs: - self.setup_allowed_and_configs(allowed.name, - configs.name, False) + self.setup_allowed_and_configs( + allowed.name, configs.name, False + ) ret_code = kconfig_check.main( - ['-c', configs.name, '-s', srctree, - '-a', allowed.name, '-p', PREFIX, 'check']) + [ + "-c", + configs.name, + "-s", + srctree, + "-a", + allowed.name, + "-p", + PREFIX, + "check", + ] + ) self.assertEqual(1, ret_code) - self.assertEqual('', stderr.getvalue()) - found = re.findall('(CONFIG_.*)', stdout.getvalue()) - self.assertEqual(['CONFIG_MENU_KCONFIG'], found) + self.assertEqual("", stderr.getvalue()) + found = re.findall("(CONFIG_.*)", stdout.getvalue()) + self.assertEqual(["CONFIG_MENU_KCONFIG"], found) allowed = kconfig_check.NEW_ALLOWED_FNAME.read_text().splitlines() - self.assertEqual(['CONFIG_OLD_ONE'], allowed) + self.assertEqual(["CONFIG_OLD_ONE"], allowed) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/util/uart_stress_tester.py b/util/uart_stress_tester.py index b3db60060e..a89fe730c9 100755 --- a/util/uart_stress_tester.py +++ b/util/uart_stress_tester.py @@ -21,9 +21,7 @@ Prerequisite: e.g. dut-control cr50_uart_timestamp:off """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +from __future__ import absolute_import, division, print_function import argparse import atexit @@ -36,472 +34,501 @@ import time import serial -BAUDRATE = 115200 # Default baudrate setting for UART port -CROS_USERNAME = 'root' # Account name to login to ChromeOS -CROS_PASSWORD = 'test0000' # Password to login to ChromeOS -CHARGEN_TXT = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' - # The result of 'chargen 62 62' +BAUDRATE = 115200 # Default baudrate setting for UART port +CROS_USERNAME = "root" # Account name to login to ChromeOS +CROS_PASSWORD = "test0000" # Password to login to ChromeOS +CHARGEN_TXT = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +# The result of 'chargen 62 62' CHARGEN_TXT_LEN = len(CHARGEN_TXT) -CR = '\r' # Carriage Return -LF = '\n' # Line Feed +CR = "\r" # Carriage Return +LF = "\n" # Line Feed CRLF = CR + LF -FLAG_FILENAME = '/tmp/chargen_testing' -TPM_CMD = ('trunks_client --key_create --rsa=2048 --usage=sign' - ' --key_blob=/tmp/blob &> /dev/null') - # A ChromeOS TPM command for the cr50 stress - # purpose. -CR50_LOAD_GEN_CMD = ('while [[ -f %s ]]; do %s; done &' - % (FLAG_FILENAME, TPM_CMD)) - # A command line to run TPM_CMD in background - # infinitely. +FLAG_FILENAME = "/tmp/chargen_testing" +TPM_CMD = ( + "trunks_client --key_create --rsa=2048 --usage=sign" + " --key_blob=/tmp/blob &> /dev/null" +) +# A ChromeOS TPM command for the cr50 stress +# purpose. +CR50_LOAD_GEN_CMD = "while [[ -f %s ]]; do %s; done &" % (FLAG_FILENAME, TPM_CMD) +# A command line to run TPM_CMD in background +# infinitely. class ChargenTestError(Exception): - """Exception for Uart Stress Test Error""" - pass + """Exception for Uart Stress Test Error""" + pass -class UartSerial(object): - """Test Object for a single UART serial device - - Attributes: - UART_DEV_PROFILES - char_loss_occurrences: Number that character loss happens - cleanup_cli: Command list to perform before the test exits - cr50_workload: True if cr50 should be stressed, or False otherwise - usb_output: True if output should be generated to USB channel - dev_prof: Dictionary of device profile - duration: Time to keep chargen running - eol: Characters to add at the end of input - logger: object that store the log - num_ch_exp: Expected number of characters in output - num_ch_cap: Number of captured characters in output - test_cli: Command list to run for chargen test - test_thread: Thread object that captures the UART output - serial: serial.Serial object - """ - UART_DEV_PROFILES = ( - # Kernel - { - 'prompt':'localhost login:', - 'device_type':'AP', - 'prepare_cmd':[ - CROS_USERNAME, # Login - CROS_PASSWORD, # Password - 'dmesg -D', # Disable console message - 'touch ' + FLAG_FILENAME, # Create a temp file - ], - 'cleanup_cmd':[ - 'rm -f ' + FLAG_FILENAME, # Remove the temp file - 'dmesg -E', # Enable console message - 'logout', # Logout - ], - 'end_of_input':LF, - }, - # EC - { - 'prompt':'> ', - 'device_type':'EC', - 'prepare_cmd':[ - 'chan save', - 'chan 0' # Disable console message - ], - 'cleanup_cmd':['', 'chan restore'], - 'end_of_input':CRLF, - }, - ) - - def __init__(self, port, duration, timeout=1, - baudrate=BAUDRATE, cr50_workload=False, - usb_output=False): - """Initialize UartSerial - - Args: - port: UART device path. e.g. /dev/ttyUSB0 - duration: Time to test, in seconds - timeout: Read timeout value. - baudrate: Baud rate such as 9600 or 115200. - cr50_workload: True if a workload should be generated on cr50 - usb_output: True if a workload should be generated to USB channel - """ - - # Initialize serial object - self.serial = serial.Serial() - self.serial.port = port - self.serial.timeout = timeout - self.serial.baudrate = baudrate - - self.duration = duration - self.cr50_workload = cr50_workload - self.usb_output = usb_output - - self.logger = logging.getLogger(type(self).__name__ + '| ' + port) - self.test_thread = threading.Thread(target=self.stress_test_thread) - self.dev_prof = {} - self.cleanup_cli = [] - self.test_cli = [] - self.eol = CRLF - self.num_ch_exp = 0 - self.num_ch_cap = 0 - self.char_loss_occurrences = 0 - atexit.register(self.cleanup) - - def run_command(self, command_lines, delay=0): - """Run command(s) at UART prompt - - Args: - command_lines: list of commands to run. - delay: delay after a command in second +class UartSerial(object): + """Test Object for a single UART serial device + + Attributes: + UART_DEV_PROFILES + char_loss_occurrences: Number that character loss happens + cleanup_cli: Command list to perform before the test exits + cr50_workload: True if cr50 should be stressed, or False otherwise + usb_output: True if output should be generated to USB channel + dev_prof: Dictionary of device profile + duration: Time to keep chargen running + eol: Characters to add at the end of input + logger: object that store the log + num_ch_exp: Expected number of characters in output + num_ch_cap: Number of captured characters in output + test_cli: Command list to run for chargen test + test_thread: Thread object that captures the UART output + serial: serial.Serial object """ - for cli in command_lines: - self.logger.debug('run %r', cli) - - self.serial.write((cli + self.eol).encode()) - self.serial.flush() - if delay: - time.sleep(delay) - - def cleanup(self): - """Before termination, clean up the UART device.""" - self.logger.debug('Closing...') - - self.serial.open() - self.run_command(self.cleanup_cli) # Run cleanup commands - self.serial.close() - self.logger.debug('Cleanup done') + UART_DEV_PROFILES = ( + # Kernel + { + "prompt": "localhost login:", + "device_type": "AP", + "prepare_cmd": [ + CROS_USERNAME, # Login + CROS_PASSWORD, # Password + "dmesg -D", # Disable console message + "touch " + FLAG_FILENAME, # Create a temp file + ], + "cleanup_cmd": [ + "rm -f " + FLAG_FILENAME, # Remove the temp file + "dmesg -E", # Enable console message + "logout", # Logout + ], + "end_of_input": LF, + }, + # EC + { + "prompt": "> ", + "device_type": "EC", + "prepare_cmd": ["chan save", "chan 0"], # Disable console message + "cleanup_cmd": ["", "chan restore"], + "end_of_input": CRLF, + }, + ) + + def __init__( + self, + port, + duration, + timeout=1, + baudrate=BAUDRATE, + cr50_workload=False, + usb_output=False, + ): + """Initialize UartSerial + + Args: + port: UART device path. e.g. /dev/ttyUSB0 + duration: Time to test, in seconds + timeout: Read timeout value. + baudrate: Baud rate such as 9600 or 115200. + cr50_workload: True if a workload should be generated on cr50 + usb_output: True if a workload should be generated to USB channel + """ + + # Initialize serial object + self.serial = serial.Serial() + self.serial.port = port + self.serial.timeout = timeout + self.serial.baudrate = baudrate + + self.duration = duration + self.cr50_workload = cr50_workload + self.usb_output = usb_output + + self.logger = logging.getLogger(type(self).__name__ + "| " + port) + self.test_thread = threading.Thread(target=self.stress_test_thread) + + self.dev_prof = {} + self.cleanup_cli = [] + self.test_cli = [] + self.eol = CRLF + self.num_ch_exp = 0 + self.num_ch_cap = 0 + self.char_loss_occurrences = 0 + atexit.register(self.cleanup) + + def run_command(self, command_lines, delay=0): + """Run command(s) at UART prompt + + Args: + command_lines: list of commands to run. + delay: delay after a command in second + """ + for cli in command_lines: + self.logger.debug("run %r", cli) + + self.serial.write((cli + self.eol).encode()) + self.serial.flush() + if delay: + time.sleep(delay) + + def cleanup(self): + """Before termination, clean up the UART device.""" + self.logger.debug("Closing...") + + self.serial.open() + self.run_command(self.cleanup_cli) # Run cleanup commands + self.serial.close() + + self.logger.debug("Cleanup done") + + def get_output(self): + """Capture the UART output + + Args: + stop_char: Read output buffer until it reads stop_char. + + Returns: + text from UART output. + """ + if self.serial.inWaiting() == 0: + time.sleep(1) + + return self.serial.read(self.serial.inWaiting()).decode() + + def prepare(self): + """Prepare the test: + + Identify the type of UART device (EC or Kernel?), then + decide what kind of commands to use to generate stress loads. + + Raises: + ChargenTestError if UART source can't be identified. + """ + try: + self.logger.info("Preparing...") + + self.serial.open() + + # Prepare the device for test + self.serial.flushInput() + self.serial.flushOutput() + + self.get_output() # drain data + + # Give a couple of line feeds, and capture the prompt text + self.run_command(["", ""]) + prompt_txt = self.get_output() + + # Detect the device source: EC or AP? + # Detect if the device is AP or EC console based on the captured. + for dev_prof in self.UART_DEV_PROFILES: + if dev_prof["prompt"] in prompt_txt: + self.dev_prof = dev_prof + break + else: + # No prompt patterns were found. UART seems not responding or in + # an undesirable status. + if prompt_txt: + raise ChargenTestError( + "%s: Got an unknown prompt text: %s\n" + "Check manually whether %s is available." + % (self.serial.port, prompt_txt, self.serial.port) + ) + else: + raise ChargenTestError( + "%s: Got no input. Close any other connections" + " to this port, and try it again." % self.serial.port + ) + + self.logger.info("Detected as %s UART", self.dev_prof["device_type"]) + # Log displays the UART type (AP|EC) instead of device filename. + self.logger = logging.getLogger( + type(self).__name__ + "| " + self.dev_prof["device_type"] + ) + + # Either login to AP or run some commands to prepare the device + # for test + self.eol = self.dev_prof["end_of_input"] + self.run_command(self.dev_prof["prepare_cmd"], delay=2) + self.cleanup_cli += self.dev_prof["cleanup_cmd"] + + # 'chargen' of AP does not have option for USB output. + # Force it work on UART. + if self.dev_prof["device_type"] == "AP": + self.usb_output = False + + # Check whether the command 'chargen' is available in the device. + # 'chargen 1 4' is supposed to print '0000' + self.get_output() # drain data + + chargen_cmd = "chargen 1 4" + if self.usb_output: + chargen_cmd += " usb" + self.run_command([chargen_cmd]) + tmp_txt = self.get_output() + + # Check whether chargen command is available. + if "0000" not in tmp_txt: + raise ChargenTestError( + "%s: Chargen got an unexpected result: %s" + % (self.dev_prof["device_type"], tmp_txt) + ) + + self.num_ch_exp = int(self.serial.baudrate * self.duration / 10) + chargen_cmd = "chargen " + str(CHARGEN_TXT_LEN) + " " + str(self.num_ch_exp) + if self.usb_output: + chargen_cmd += " usb" + self.test_cli = [chargen_cmd] + + self.logger.info("Ready to test") + finally: + self.serial.close() + + def stress_test_thread(self): + """Test thread + + Raises: + ChargenTestError: if broken character is found. + """ + try: + self.serial.open() + self.serial.flushInput() + self.serial.flushOutput() + + # Run TPM command in background to burden cr50. + if self.dev_prof["device_type"] == "AP" and self.cr50_workload: + self.run_command([CR50_LOAD_GEN_CMD]) + self.logger.debug("run TPM job while %s exists", FLAG_FILENAME) + + # Run the command 'chargen', one time + self.run_command([""]) # Give a line feed + self.get_output() # Drain the output + self.run_command(self.test_cli) + self.serial.readline() # Drain the echoed command line. + + err_msg = "%s: Expected %r but got %s after %d char received" + + # Keep capturing the output until the test timer is expired. + self.num_ch_cap = 0 + self.char_loss_occurrences = 0 + data_starve_count = 0 + + total_num_ch = self.num_ch_exp # Expected number of characters in total + ch_exp = CHARGEN_TXT[0] + ch_cap = "z" # any character value is ok for loop initial condition. + while self.num_ch_cap < total_num_ch: + captured = self.get_output() + + if captured: + # There is some output data. Reset the data starvation count. + data_starve_count = 0 + else: + data_starve_count += 1 + if data_starve_count > 1: + # If nothing was captured more than once, then terminate the test. + self.logger.debug("No more output") + break + + for ch_cap in captured: + if ch_cap not in CHARGEN_TXT: + # If it is not alpha-numeric, terminate the test. + if ch_cap not in CRLF: + # If it is neither a CR nor LF, then it is an error case. + self.logger.error("Whole captured characters: %r", captured) + raise ChargenTestError( + err_msg + % ( + "Broken char captured", + ch_exp, + hex(ord(ch_cap)), + self.num_ch_cap, + ) + ) + + # Set the loop termination condition true. + total_num_ch = self.num_ch_cap + + if self.num_ch_cap >= total_num_ch: + break + + if ch_exp != ch_cap: + # If it is alpha-numeric but not continuous, then some characters + # are lost. + self.logger.error( + err_msg, + "Char loss detected", + ch_exp, + repr(ch_cap), + self.num_ch_cap, + ) + self.char_loss_occurrences += 1 + + # Recalculate the expected number of characters to adjust + # termination condition. The loss might be bigger than this + # adjustment, but it is okay since it will terminates by either + # CR/LF detection or by data starvation. + idx_ch_exp = CHARGEN_TXT.find(ch_exp) + idx_ch_cap = CHARGEN_TXT.find(ch_cap) + if idx_ch_cap < idx_ch_exp: + idx_ch_cap += len(CHARGEN_TXT) + total_num_ch -= idx_ch_cap - idx_ch_exp + + self.num_ch_cap += 1 + + # Determine What character is expected next? + ch_exp = CHARGEN_TXT[ + (CHARGEN_TXT.find(ch_cap) + 1) % CHARGEN_TXT_LEN + ] + + finally: + self.serial.close() + + def start_test(self): + """Start the test thread""" + self.logger.info("Test thread starts") + self.test_thread.start() + + def wait_test_done(self): + """Wait until the test thread get done and join""" + self.test_thread.join() + self.logger.info("Test thread is done") + + def get_result(self): + """Display the result + + Returns: + Integer = the number of lost character + + Raises: + ChargenTestError: if the capture is corrupted. + """ + # If more characters than expected are captured, it means some messages + # from other than chargen are mixed. Stop processing further. + if self.num_ch_exp < self.num_ch_cap: + raise ChargenTestError( + "%s: UART output is corrupted." % self.dev_prof["device_type"] + ) + + # Get the count difference between the expected to the captured + # as the number of lost character. + char_lost = self.num_ch_exp - self.num_ch_cap + self.logger.info( + "%8d char lost / %10d (%.1f %%)", + char_lost, + self.num_ch_exp, + char_lost * 100.0 / self.num_ch_exp, + ) + + return char_lost, self.num_ch_exp, self.char_loss_occurrences - def get_output(self): - """Capture the UART output - Args: - stop_char: Read output buffer until it reads stop_char. +class ChargenTest(object): + """UART stress tester - Returns: - text from UART output. + Attributes: + logger: logging object + serials: Dictionary where key is filename of UART device, and the value is + UartSerial object """ - if self.serial.inWaiting() == 0: - time.sleep(1) - - return self.serial.read(self.serial.inWaiting()).decode() - def prepare(self): - """Prepare the test: - - Identify the type of UART device (EC or Kernel?), then - decide what kind of commands to use to generate stress loads. - - Raises: - ChargenTestError if UART source can't be identified. - """ - try: - self.logger.info('Preparing...') - - self.serial.open() - - # Prepare the device for test - self.serial.flushInput() - self.serial.flushOutput() - - self.get_output() # drain data - - # Give a couple of line feeds, and capture the prompt text - self.run_command(['', '']) - prompt_txt = self.get_output() - - # Detect the device source: EC or AP? - # Detect if the device is AP or EC console based on the captured. - for dev_prof in self.UART_DEV_PROFILES: - if dev_prof['prompt'] in prompt_txt: - self.dev_prof = dev_prof - break - else: - # No prompt patterns were found. UART seems not responding or in - # an undesirable status. - if prompt_txt: - raise ChargenTestError('%s: Got an unknown prompt text: %s\n' - 'Check manually whether %s is available.' % - (self.serial.port, prompt_txt, - self.serial.port)) - else: - raise ChargenTestError('%s: Got no input. Close any other connections' - ' to this port, and try it again.' % - self.serial.port) - - self.logger.info('Detected as %s UART', self.dev_prof['device_type']) - # Log displays the UART type (AP|EC) instead of device filename. - self.logger = logging.getLogger(type(self).__name__ + '| ' + - self.dev_prof['device_type']) - - # Either login to AP or run some commands to prepare the device - # for test - self.eol = self.dev_prof['end_of_input'] - self.run_command(self.dev_prof['prepare_cmd'], delay=2) - self.cleanup_cli += self.dev_prof['cleanup_cmd'] - - # 'chargen' of AP does not have option for USB output. - # Force it work on UART. - if self.dev_prof['device_type'] == 'AP': - self.usb_output = False - - # Check whether the command 'chargen' is available in the device. - # 'chargen 1 4' is supposed to print '0000' - self.get_output() # drain data - - chargen_cmd = 'chargen 1 4' - if self.usb_output: - chargen_cmd += ' usb' - self.run_command([chargen_cmd]) - tmp_txt = self.get_output() - - # Check whether chargen command is available. - if '0000' not in tmp_txt: - raise ChargenTestError('%s: Chargen got an unexpected result: %s' % - (self.dev_prof['device_type'], tmp_txt)) - - self.num_ch_exp = int(self.serial.baudrate * self.duration / 10) - chargen_cmd = 'chargen ' + str(CHARGEN_TXT_LEN) + ' ' + \ - str(self.num_ch_exp) - if self.usb_output: - chargen_cmd += ' usb' - self.test_cli = [chargen_cmd] - - self.logger.info('Ready to test') - finally: - self.serial.close() - - def stress_test_thread(self): - """Test thread - - Raises: - ChargenTestError: if broken character is found. - """ - try: - self.serial.open() - self.serial.flushInput() - self.serial.flushOutput() - - # Run TPM command in background to burden cr50. - if self.dev_prof['device_type'] == 'AP' and self.cr50_workload: - self.run_command([CR50_LOAD_GEN_CMD]) - self.logger.debug('run TPM job while %s exists', FLAG_FILENAME) - - # Run the command 'chargen', one time - self.run_command(['']) # Give a line feed - self.get_output() # Drain the output - self.run_command(self.test_cli) - self.serial.readline() # Drain the echoed command line. - - err_msg = '%s: Expected %r but got %s after %d char received' - - # Keep capturing the output until the test timer is expired. - self.num_ch_cap = 0 - self.char_loss_occurrences = 0 - data_starve_count = 0 - - total_num_ch = self.num_ch_exp # Expected number of characters in total - ch_exp = CHARGEN_TXT[0] - ch_cap = 'z' # any character value is ok for loop initial condition. - while self.num_ch_cap < total_num_ch: - captured = self.get_output() - - if captured: - # There is some output data. Reset the data starvation count. - data_starve_count = 0 + def __init__(self, ports, duration, cr50_workload=False, usb_output=False): + """Initialize UART stress tester + + Args: + ports: List of UART ports to test. + duration: Time to keep testing in seconds. + cr50_workload: True if a workload should be generated on cr50 + usb_output: True if a workload should be generated to USB channel + + Raises: + ChargenTestError: if any of ports is not a valid character device. + """ + + # Save the arguments + for port in ports: + try: + mode = os.stat(port).st_mode + except OSError as e: + raise ChargenTestError(e) + if not stat.S_ISCHR(mode): + raise ChargenTestError("%s is not a character device." % port) + + if duration <= 0: + raise ChargenTestError("Input error: duration is not positive.") + + # Initialize logging object + self.logger = logging.getLogger(type(self).__name__) + + # Create an UartSerial object per UART port + self.serials = {} # UartSerial objects + for port in ports: + self.serials[port] = UartSerial( + port=port, + duration=duration, + cr50_workload=cr50_workload, + usb_output=usb_output, + ) + + def prepare(self): + """Prepare the test for each UART port""" + self.logger.info("Prepare ports for test") + for _, ser in self.serials.items(): + ser.prepare() + self.logger.info("Ports are ready to test") + + def print_result(self): + """Display the test result for each UART port + + Returns: + char_lost: Total number of characters lost + """ + char_lost = 0 + for _, ser in self.serials.items(): + (tmp_lost, _, _) = ser.get_result() + char_lost += tmp_lost + + # If any characters are lost, then test fails. + msg = "lost %d character(s) from the test" % char_lost + if char_lost > 0: + self.logger.error("FAIL: %s", msg) else: - data_starve_count += 1 - if data_starve_count > 1: - # If nothing was captured more than once, then terminate the test. - self.logger.debug('No more output') - break - - for ch_cap in captured: - if ch_cap not in CHARGEN_TXT: - # If it is not alpha-numeric, terminate the test. - if ch_cap not in CRLF: - # If it is neither a CR nor LF, then it is an error case. - self.logger.error('Whole captured characters: %r', captured) - raise ChargenTestError(err_msg % ('Broken char captured', ch_exp, - hex(ord(ch_cap)), - self.num_ch_cap)) - - # Set the loop termination condition true. - total_num_ch = self.num_ch_cap - - if self.num_ch_cap >= total_num_ch: - break - - if ch_exp != ch_cap: - # If it is alpha-numeric but not continuous, then some characters - # are lost. - self.logger.error(err_msg, 'Char loss detected', - ch_exp, repr(ch_cap), self.num_ch_cap) - self.char_loss_occurrences += 1 - - # Recalculate the expected number of characters to adjust - # termination condition. The loss might be bigger than this - # adjustment, but it is okay since it will terminates by either - # CR/LF detection or by data starvation. - idx_ch_exp = CHARGEN_TXT.find(ch_exp) - idx_ch_cap = CHARGEN_TXT.find(ch_cap) - if idx_ch_cap < idx_ch_exp: - idx_ch_cap += len(CHARGEN_TXT) - total_num_ch -= (idx_ch_cap - idx_ch_exp) - - self.num_ch_cap += 1 - - # Determine What character is expected next? - ch_exp = CHARGEN_TXT[(CHARGEN_TXT.find(ch_cap) + 1) % CHARGEN_TXT_LEN] - - finally: - self.serial.close() - - def start_test(self): - """Start the test thread""" - self.logger.info('Test thread starts') - self.test_thread.start() - - def wait_test_done(self): - """Wait until the test thread get done and join""" - self.test_thread.join() - self.logger.info('Test thread is done') - - def get_result(self): - """Display the result + self.logger.info("PASS: %s", msg) - Returns: - Integer = the number of lost character + return char_lost - Raises: - ChargenTestError: if the capture is corrupted. - """ - # If more characters than expected are captured, it means some messages - # from other than chargen are mixed. Stop processing further. - if self.num_ch_exp < self.num_ch_cap: - raise ChargenTestError('%s: UART output is corrupted.' % - self.dev_prof['device_type']) + def run(self): + """Run the stress test on UART port(s) - # Get the count difference between the expected to the captured - # as the number of lost character. - char_lost = self.num_ch_exp - self.num_ch_cap - self.logger.info('%8d char lost / %10d (%.1f %%)', - char_lost, self.num_ch_exp, - char_lost * 100.0 / self.num_ch_exp) + Raises: + ChargenTestError: If any characters are lost. + """ - return char_lost, self.num_ch_exp, self.char_loss_occurrences + # Detect UART source type, and decide which command to test. + self.prepare() + # Run the test on each UART port in thread. + self.logger.info("Test starts") + for _, ser in self.serials.items(): + ser.start_test() -class ChargenTest(object): - """UART stress tester + # Wait all tests to finish. + for _, ser in self.serials.items(): + ser.wait_test_done() - Attributes: - logger: logging object - serials: Dictionary where key is filename of UART device, and the value is - UartSerial object - """ + # Print the result. + char_lost = self.print_result() + if char_lost: + raise ChargenTestError("Test failed: lost %d character(s)" % char_lost) - def __init__(self, ports, duration, cr50_workload=False, - usb_output=False): - """Initialize UART stress tester + self.logger.info("Test is done") - Args: - ports: List of UART ports to test. - duration: Time to keep testing in seconds. - cr50_workload: True if a workload should be generated on cr50 - usb_output: True if a workload should be generated to USB channel - Raises: - ChargenTestError: if any of ports is not a valid character device. - """ +def parse_args(cmdline): + """Parse command line arguments. - # Save the arguments - for port in ports: - try: - mode = os.stat(port).st_mode - except OSError as e: - raise ChargenTestError(e) - if not stat.S_ISCHR(mode): - raise ChargenTestError('%s is not a character device.' % port) - - if duration <= 0: - raise ChargenTestError('Input error: duration is not positive.') - - # Initialize logging object - self.logger = logging.getLogger(type(self).__name__) - - # Create an UartSerial object per UART port - self.serials = {} # UartSerial objects - for port in ports: - self.serials[port] = UartSerial(port=port, duration=duration, - cr50_workload=cr50_workload, - usb_output=usb_output) - - def prepare(self): - """Prepare the test for each UART port""" - self.logger.info('Prepare ports for test') - for _, ser in self.serials.items(): - ser.prepare() - self.logger.info('Ports are ready to test') - - def print_result(self): - """Display the test result for each UART port + Args: + cmdline: list to be parsed Returns: - char_lost: Total number of characters lost - """ - char_lost = 0 - for _, ser in self.serials.items(): - (tmp_lost, _, _) = ser.get_result() - char_lost += tmp_lost - - # If any characters are lost, then test fails. - msg = 'lost %d character(s) from the test' % char_lost - if char_lost > 0: - self.logger.error('FAIL: %s', msg) - else: - self.logger.info('PASS: %s', msg) - - return char_lost - - def run(self): - """Run the stress test on UART port(s) - - Raises: - ChargenTestError: If any characters are lost. + tuple (options, args) where args is a list of cmdline arguments that the + parser was unable to match i.e. they're servod controls, not options. """ - - # Detect UART source type, and decide which command to test. - self.prepare() - - # Run the test on each UART port in thread. - self.logger.info('Test starts') - for _, ser in self.serials.items(): - ser.start_test() - - # Wait all tests to finish. - for _, ser in self.serials.items(): - ser.wait_test_done() - - # Print the result. - char_lost = self.print_result() - if char_lost: - raise ChargenTestError('Test failed: lost %d character(s)' % - char_lost) - - self.logger.info('Test is done') - -def parse_args(cmdline): - """Parse command line arguments. - - Args: - cmdline: list to be parsed - - Returns: - tuple (options, args) where args is a list of cmdline arguments that the - parser was unable to match i.e. they're servod controls, not options. - """ - description = """%(prog)s repeats sending a uart console command + description = """%(prog)s repeats sending a uart console command to each UART device for a given time, and check if output has any missing characters. @@ -511,52 +538,70 @@ Examples: %(prog)s /dev/ttyUSB1 /dev/ttyUSB2 --cr50 """ - parser = argparse.ArgumentParser(description=description, - formatter_class=argparse.RawTextHelpFormatter - ) - parser.add_argument('port', type=str, nargs='*', - help='UART device path to test') - parser.add_argument('-c', '--cr50', action='store_true', default=False, - help='generate TPM workload on cr50') - parser.add_argument('-d', '--debug', action='store_true', default=False, - help='enable debug messages') - parser.add_argument('-t', '--time', type=int, - help='Test duration in second', default=300) - parser.add_argument('-u', '--usb', action='store_true', default=False, - help='Generate output to USB channel instead') - return parser.parse_known_args(cmdline) + parser = argparse.ArgumentParser( + description=description, formatter_class=argparse.RawTextHelpFormatter + ) + parser.add_argument("port", type=str, nargs="*", help="UART device path to test") + parser.add_argument( + "-c", + "--cr50", + action="store_true", + default=False, + help="generate TPM workload on cr50", + ) + parser.add_argument( + "-d", + "--debug", + action="store_true", + default=False, + help="enable debug messages", + ) + parser.add_argument( + "-t", "--time", type=int, help="Test duration in second", default=300 + ) + parser.add_argument( + "-u", + "--usb", + action="store_true", + default=False, + help="Generate output to USB channel instead", + ) + return parser.parse_known_args(cmdline) def main(): - """Main function wrapper""" - try: - (options, _) = parse_args(sys.argv[1:]) - - # Set Log format - log_format = '%(asctime)s %(levelname)-6s | %(name)-25s' - date_format = '%Y-%m-%d %H:%M:%S' - if options.debug: - log_format += ' | %(filename)s:%(lineno)4d:%(funcName)-18s' - loglevel = logging.DEBUG - else: - loglevel = logging.INFO - log_format += ' | %(message)s' - - logging.basicConfig(level=loglevel, format=log_format, - datefmt=date_format) - - # Create a ChargenTest object - utest = ChargenTest(options.port, options.time, - cr50_workload=options.cr50, - usb_output=options.usb) - utest.run() # Run - - except KeyboardInterrupt: - sys.exit(0) - - except ChargenTestError as e: - logging.error(str(e)) - sys.exit(1) - -if __name__ == '__main__': - main() + """Main function wrapper""" + try: + (options, _) = parse_args(sys.argv[1:]) + + # Set Log format + log_format = "%(asctime)s %(levelname)-6s | %(name)-25s" + date_format = "%Y-%m-%d %H:%M:%S" + if options.debug: + log_format += " | %(filename)s:%(lineno)4d:%(funcName)-18s" + loglevel = logging.DEBUG + else: + loglevel = logging.INFO + log_format += " | %(message)s" + + logging.basicConfig(level=loglevel, format=log_format, datefmt=date_format) + + # Create a ChargenTest object + utest = ChargenTest( + options.port, + options.time, + cr50_workload=options.cr50, + usb_output=options.usb, + ) + utest.run() # Run + + except KeyboardInterrupt: + sys.exit(0) + + except ChargenTestError as e: + logging.error(str(e)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/util/unpack_ftb.py b/util/unpack_ftb.py index 03127a7089..a68662d82b 100755 --- a/util/unpack_ftb.py +++ b/util/unpack_ftb.py @@ -10,26 +10,28 @@ # Note: This is a py2/3 compatible file. from __future__ import print_function + import argparse import ctypes import os class Header(ctypes.Structure): - _pack_ = 1 - _fields_ = [ - ('signature', ctypes.c_uint32), - ('ftb_ver', ctypes.c_uint32), - ('chip_id', ctypes.c_uint32), - ('svn_ver', ctypes.c_uint32), - ('fw_ver', ctypes.c_uint32), - ('config_id', ctypes.c_uint32), - ('config_ver', ctypes.c_uint32), - ('reserved', ctypes.c_uint8 * 8), - ('release_info', ctypes.c_ulonglong), - ('sec_size', ctypes.c_uint32 * 4), - ('crc', ctypes.c_uint32), - ] + _pack_ = 1 + _fields_ = [ + ("signature", ctypes.c_uint32), + ("ftb_ver", ctypes.c_uint32), + ("chip_id", ctypes.c_uint32), + ("svn_ver", ctypes.c_uint32), + ("fw_ver", ctypes.c_uint32), + ("config_id", ctypes.c_uint32), + ("config_ver", ctypes.c_uint32), + ("reserved", ctypes.c_uint8 * 8), + ("release_info", ctypes.c_ulonglong), + ("sec_size", ctypes.c_uint32 * 4), + ("crc", ctypes.c_uint32), + ] + FW_HEADER_SIZE = 64 FW_HEADER_SIGNATURE = 0xAA55AA55 @@ -44,7 +46,7 @@ FLASH_SEC_ADDR = [ 0x0000 * 4, # CODE 0x7C00 * 4, # CONFIG 0x7000 * 4, # CX - None # This section shouldn't exist + None, # This section shouldn't exist ] UPDATE_PDU_SIZE = 4096 @@ -59,64 +61,66 @@ OUTPUT_FILE_SIZE = UPDATE_PDU_SIZE + 128 * 1024 def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--input', '-i', required=True) - parser.add_argument('--output', '-o', required=True) - args = parser.parse_args() + parser = argparse.ArgumentParser() + parser.add_argument("--input", "-i", required=True) + parser.add_argument("--output", "-o", required=True) + args = parser.parse_args() - with open(args.input, 'rb') as f: - bs = f.read() + with open(args.input, "rb") as f: + bs = f.read() - size = len(bs) - if size < FW_HEADER_SIZE + FW_BYTES_ALIGN: - raise Exception('FW size too small') + size = len(bs) + if size < FW_HEADER_SIZE + FW_BYTES_ALIGN: + raise Exception("FW size too small") - print('FTB file size:', size) + print("FTB file size:", size) - header = Header() - assert ctypes.sizeof(header) == FW_HEADER_SIZE + header = Header() + assert ctypes.sizeof(header) == FW_HEADER_SIZE - ctypes.memmove(ctypes.addressof(header), bs, ctypes.sizeof(header)) - if (header.signature != FW_HEADER_SIGNATURE or - header.ftb_ver != FW_FTB_VER or - header.chip_id != FW_CHIP_ID): - raise Exception('Invalid header') + ctypes.memmove(ctypes.addressof(header), bs, ctypes.sizeof(header)) + if ( + header.signature != FW_HEADER_SIGNATURE + or header.ftb_ver != FW_FTB_VER + or header.chip_id != FW_CHIP_ID + ): + raise Exception("Invalid header") - for key, _ in header._fields_: - v = getattr(header, key) - if isinstance(v, ctypes.Array): - print(key, list(map(hex, v))) - else: - print(key, hex(v)) + for key, _ in header._fields_: + v = getattr(header, key) + if isinstance(v, ctypes.Array): + print(key, list(map(hex, v))) + else: + print(key, hex(v)) - dimension = sum(header.sec_size) + dimension = sum(header.sec_size) - assert dimension + FW_HEADER_SIZE + FW_BYTES_ALIGN == size - data = bs[FW_HEADER_SIZE:FW_HEADER_SIZE + dimension] + assert dimension + FW_HEADER_SIZE + FW_BYTES_ALIGN == size + data = bs[FW_HEADER_SIZE : FW_HEADER_SIZE + dimension] - with open(args.output, 'wb') as f: - # ensure the file size - f.seek(OUTPUT_FILE_SIZE - 1, os.SEEK_SET) - f.write(b'\x00') + with open(args.output, "wb") as f: + # ensure the file size + f.seek(OUTPUT_FILE_SIZE - 1, os.SEEK_SET) + f.write(b"\x00") - f.seek(0, os.SEEK_SET) - f.write(bs[0 : ctypes.sizeof(header)]) + f.seek(0, os.SEEK_SET) + f.write(bs[0 : ctypes.sizeof(header)]) - offset = 0 - # write each sections - for i, addr in enumerate(FLASH_SEC_ADDR): - size = header.sec_size[i] - assert addr is not None or size == 0 + offset = 0 + # write each sections + for i, addr in enumerate(FLASH_SEC_ADDR): + size = header.sec_size[i] + assert addr is not None or size == 0 - if size == 0: - continue + if size == 0: + continue - f.seek(UPDATE_PDU_SIZE + addr, os.SEEK_SET) - f.write(data[offset : offset + size]) - offset += size + f.seek(UPDATE_PDU_SIZE + addr, os.SEEK_SET) + f.write(data[offset : offset + size]) + offset += size - f.flush() + f.flush() -if __name__ == '__main__': - main() +if __name__ == "__main__": + main() diff --git a/util/update_release_branch.py b/util/update_release_branch.py index b9063d4970..4d9c89df4a 100755 --- a/util/update_release_branch.py +++ b/util/update_release_branch.py @@ -19,8 +19,7 @@ import subprocess import sys import textwrap - -BUG_NONE_PATTERN = re.compile('none', flags=re.IGNORECASE) +BUG_NONE_PATTERN = re.compile("none", flags=re.IGNORECASE) def git_commit_msg(branch, head, merge_head, rel_paths, cmd): @@ -42,18 +41,17 @@ def git_commit_msg(branch, head, merge_head, rel_paths, cmd): A String containing the git commit message with the exception of the Signed-Off-By field and Change-ID field. """ - relevant_commits_cmd, relevant_commits = get_relevant_commits(head, - merge_head, - '--oneline', - rel_paths) + relevant_commits_cmd, relevant_commits = get_relevant_commits( + head, merge_head, "--oneline", rel_paths + ) - _, relevant_bugs = get_relevant_commits(head, merge_head, '', rel_paths) - relevant_bugs = set(re.findall('BUG=(.*)', relevant_bugs)) + _, relevant_bugs = get_relevant_commits(head, merge_head, "", rel_paths) + relevant_bugs = set(re.findall("BUG=(.*)", relevant_bugs)) # Filter out "none" from set of bugs filtered = [] for bug_line in relevant_bugs: - bug_line = bug_line.replace(',', ' ') - bugs = bug_line.split(' ') + bug_line = bug_line.replace(",", " ") + bugs = bug_line.split(" ") for bug in bugs: if bug and not BUG_NONE_PATTERN.match(bug): filtered.append(bug) @@ -82,18 +80,20 @@ Cq-Include-Trybots: chromeos/cq:cq-orchestrator # 72 cols. relevant_commits_cmd = textwrap.fill(relevant_commits_cmd, width=72) # Wrap at 68 cols to save room for 'BUG=' - bugs = textwrap.wrap(' '.join(relevant_bugs), width=68) - bug_field = '' + bugs = textwrap.wrap(" ".join(relevant_bugs), width=68) + bug_field = "" for line in bugs: - bug_field += 'BUG=' + line + '\n' + bug_field += "BUG=" + line + "\n" # Remove the final newline since the template adds it for us. bug_field = bug_field[:-1] - return COMMIT_MSG_TEMPLATE.format(BRANCH=branch, - RELEVANT_COMMITS_CMD=relevant_commits_cmd, - RELEVANT_COMMITS=relevant_commits, - BUG_FIELD=bug_field, - COMMAND_LINE=cmd) + return COMMIT_MSG_TEMPLATE.format( + BRANCH=branch, + RELEVANT_COMMITS_CMD=relevant_commits_cmd, + RELEVANT_COMMITS=relevant_commits, + BUG_FIELD=bug_field, + COMMAND_LINE=cmd, + ) def get_relevant_boards(baseboard): @@ -105,15 +105,16 @@ def get_relevant_boards(baseboard): Returns: A list of strings containing the boards based off of the baseboard. """ - proc = subprocess.run(['git', 'grep', 'BASEBOARD:=' + baseboard, '--', - 'board/'], - stdout=subprocess.PIPE, - encoding='utf-8', - check=True) + proc = subprocess.run( + ["git", "grep", "BASEBOARD:=" + baseboard, "--", "board/"], + stdout=subprocess.PIPE, + encoding="utf-8", + check=True, + ) boards = [] res = proc.stdout.splitlines() for line in res: - boards.append(line.split('/')[1]) + boards.append(line.split("/")[1]) return boards @@ -135,21 +136,18 @@ def get_relevant_commits(head, merge_head, fmt, relevant_paths): stdout. """ if fmt: - cmd = ['git', 'log', fmt, head + '..' + merge_head, '--', - relevant_paths] + cmd = ["git", "log", fmt, head + ".." + merge_head, "--", relevant_paths] else: - cmd = ['git', 'log', head + '..' + merge_head, '--', relevant_paths] + cmd = ["git", "log", head + ".." + merge_head, "--", relevant_paths] # Pass cmd as a string to subprocess.run() since we need to run with shell # equal to True. The reason we are using shell equal to True is to take # advantage of the glob expansion for the relevant paths. - cmd = ' '.join(cmd) - proc = subprocess.run(cmd, - stdout=subprocess.PIPE, - encoding='utf-8', - check=True, - shell=True) - return ''.join(proc.args), proc.stdout + cmd = " ".join(cmd) + proc = subprocess.run( + cmd, stdout=subprocess.PIPE, encoding="utf-8", check=True, shell=True + ) + return "".join(proc.args), proc.stdout def main(argv): @@ -165,46 +163,61 @@ def main(argv): argv: A list of the command line arguments passed to this script. """ # Set up argument parser. - parser = argparse.ArgumentParser(description=('A script that generates a ' - 'merge commit from cros/main' - ' to a desired release ' - 'branch. By default, the ' - '"recursive" merge strategy ' - 'with the "theirs" strategy ' - 'option is used.')) - parser.add_argument('--baseboard') - parser.add_argument('--board') - parser.add_argument('release_branch', help=('The name of the target release' - ' branch')) - parser.add_argument('--relevant_paths_file', - help=('A path to a text file which includes other ' - 'relevant paths of interest for this board ' - 'or baseboard')) - parser.add_argument('--merge_strategy', '-s', default='recursive', - help='The merge strategy to pass to `git merge -s`') - parser.add_argument('--strategy_option', '-X', - help=('The strategy option for the chosen merge ' - 'strategy')) + parser = argparse.ArgumentParser( + description=( + "A script that generates a " + "merge commit from cros/main" + " to a desired release " + "branch. By default, the " + '"recursive" merge strategy ' + 'with the "theirs" strategy ' + "option is used." + ) + ) + parser.add_argument("--baseboard") + parser.add_argument("--board") + parser.add_argument( + "release_branch", help=("The name of the target release" " branch") + ) + parser.add_argument( + "--relevant_paths_file", + help=( + "A path to a text file which includes other " + "relevant paths of interest for this board " + "or baseboard" + ), + ) + parser.add_argument( + "--merge_strategy", + "-s", + default="recursive", + help="The merge strategy to pass to `git merge -s`", + ) + parser.add_argument( + "--strategy_option", + "-X", + help=("The strategy option for the chosen merge " "strategy"), + ) opts = parser.parse_args(argv[1:]) - baseboard_dir = '' - board_dir = '' + baseboard_dir = "" + board_dir = "" if opts.baseboard: # Dereference symlinks so "git log" works as expected. - baseboard_dir = os.path.relpath('baseboard/' + opts.baseboard) + baseboard_dir = os.path.relpath("baseboard/" + opts.baseboard) baseboard_dir = os.path.relpath(os.path.realpath(baseboard_dir)) boards = get_relevant_boards(opts.baseboard) elif opts.board: - board_dir = os.path.relpath('board/' + opts.board) + board_dir = os.path.relpath("board/" + opts.board) board_dir = os.path.relpath(os.path.realpath(board_dir)) boards = [opts.board] else: - parser.error('You must specify a board OR a baseboard') + parser.error("You must specify a board OR a baseboard") - print('Gathering relevant paths...') + print("Gathering relevant paths...") relevant_paths = [] if opts.baseboard: relevant_paths.append(baseboard_dir) @@ -212,65 +225,91 @@ def main(argv): relevant_paths.append(board_dir) for board in boards: - relevant_paths.append('board/' + board) + relevant_paths.append("board/" + board) # Check for the existence of a file that has other paths of interest. if opts.relevant_paths_file and os.path.exists(opts.relevant_paths_file): - with open(opts.relevant_paths_file, 'r') as relevant_paths_file: + with open(opts.relevant_paths_file, "r") as relevant_paths_file: for line in relevant_paths_file: - if not line.startswith('#'): + if not line.startswith("#"): relevant_paths.append(line.rstrip()) - relevant_paths.append('util/getversion.sh') - relevant_paths = ' '.join(relevant_paths) + relevant_paths.append("util/getversion.sh") + relevant_paths = " ".join(relevant_paths) # Check if we are already in merge process - result = subprocess.run(['git', 'rev-parse', '--quiet', '--verify', - 'MERGE_HEAD'], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, check=False) + result = subprocess.run( + ["git", "rev-parse", "--quiet", "--verify", "MERGE_HEAD"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) if result.returncode: # Let's perform the merge - print('Updating remote...') - subprocess.run(['git', 'remote', 'update'], check=True) - subprocess.run(['git', 'checkout', '-B', opts.release_branch, 'cros/' + - opts.release_branch], check=True) - print('Attempting git merge...') - if opts.merge_strategy == 'recursive' and not opts.strategy_option: - opts.strategy_option = 'theirs' - print('Using "%s" merge strategy' % opts.merge_strategy, - ("with strategy option '%s'" % opts.strategy_option - if opts.strategy_option else '')) - arglist = ['git', 'merge', '--no-ff', '--no-commit', 'cros/main', '-s', - opts.merge_strategy] + print("Updating remote...") + subprocess.run(["git", "remote", "update"], check=True) + subprocess.run( + [ + "git", + "checkout", + "-B", + opts.release_branch, + "cros/" + opts.release_branch, + ], + check=True, + ) + print("Attempting git merge...") + if opts.merge_strategy == "recursive" and not opts.strategy_option: + opts.strategy_option = "theirs" + print( + 'Using "%s" merge strategy' % opts.merge_strategy, + ( + "with strategy option '%s'" % opts.strategy_option + if opts.strategy_option + else "" + ), + ) + arglist = [ + "git", + "merge", + "--no-ff", + "--no-commit", + "cros/main", + "-s", + opts.merge_strategy, + ] if opts.strategy_option: - arglist.append('-X' + opts.strategy_option) + arglist.append("-X" + opts.strategy_option) subprocess.run(arglist, check=True) else: - print('We have already started merge process.', - 'Attempt to generate commit.') - - print('Generating commit message...') - branch = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], - stdout=subprocess.PIPE, - encoding='utf-8', - check=True).stdout.rstrip() - head = subprocess.run(['git', 'rev-parse', '--short', 'HEAD'], - stdout=subprocess.PIPE, - encoding='utf-8', - check=True).stdout.rstrip() - merge_head = subprocess.run(['git', 'rev-parse', '--short', - 'MERGE_HEAD'], - stdout=subprocess.PIPE, - encoding='utf-8', - check=True).stdout.rstrip() - - cmd = ' '.join(argv) - print('Typing as fast as I can...') + print("We have already started merge process.", "Attempt to generate commit.") + + print("Generating commit message...") + branch = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + stdout=subprocess.PIPE, + encoding="utf-8", + check=True, + ).stdout.rstrip() + head = subprocess.run( + ["git", "rev-parse", "--short", "HEAD"], + stdout=subprocess.PIPE, + encoding="utf-8", + check=True, + ).stdout.rstrip() + merge_head = subprocess.run( + ["git", "rev-parse", "--short", "MERGE_HEAD"], + stdout=subprocess.PIPE, + encoding="utf-8", + check=True, + ).stdout.rstrip() + + cmd = " ".join(argv) + print("Typing as fast as I can...") commit_msg = git_commit_msg(branch, head, merge_head, relevant_paths, cmd) - subprocess.run(['git', 'commit', '--signoff', '-m', commit_msg], check=True) - subprocess.run(['git', 'commit', '--amend'], check=True) - print(("Finished! **Please review the commit to see if it's to your " - 'liking.**')) + subprocess.run(["git", "commit", "--signoff", "-m", commit_msg], check=True) + subprocess.run(["git", "commit", "--amend"], check=True) + print(("Finished! **Please review the commit to see if it's to your " "liking.**")) -if __name__ == '__main__': +if __name__ == "__main__": main(sys.argv) |