# Common functions and variables for testing the Python pretty printers. # # Copyright (C) 2016-2018 Free Software Foundation, Inc. # This file is part of the GNU C Library. # # The GNU C Library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # The GNU C Library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with the GNU C Library; if not, see # . """These tests require PExpect 4.0 or newer. Exported constants: PASS, FAIL, UNSUPPORTED (int): Test exit codes, as per evaluate-test.sh. """ import os import re from test_printers_exceptions import * PASS = 0 FAIL = 1 UNSUPPORTED = 77 gdb_bin = 'gdb' gdb_options = '-q -nx' gdb_invocation = '{0} {1}'.format(gdb_bin, gdb_options) pexpect_min_version = 4 gdb_min_version = (7, 8) encoding = 'utf-8' try: import pexpect except ImportError: print('PExpect 4.0 or newer must be installed to test the pretty printers.') exit(UNSUPPORTED) pexpect_version = pexpect.__version__.split('.')[0] if int(pexpect_version) < pexpect_min_version: print('PExpect 4.0 or newer must be installed to test the pretty printers.') exit(UNSUPPORTED) if not pexpect.which(gdb_bin): print('gdb 7.8 or newer must be installed to test the pretty printers.') exit(UNSUPPORTED) timeout = 5 TIMEOUTFACTOR = os.environ.get('TIMEOUTFACTOR') if TIMEOUTFACTOR: timeout = int(TIMEOUTFACTOR) try: # Check the gdb version. version_cmd = '{0} --version'.format(gdb_invocation, timeout=timeout) gdb_version_out = pexpect.run(version_cmd, encoding=encoding) # The gdb version string is "GNU gdb ", where # PKGVERSION can be any text. We assume that there'll always be a space # between PKGVERSION and the version number for the sake of the regexp. version_match = re.search(r'GNU gdb .* ([1-9]+)\.([0-9]+)', gdb_version_out) if not version_match: print('The gdb version string (gdb -v) is incorrectly formatted.') exit(UNSUPPORTED) gdb_version = (int(version_match.group(1)), int(version_match.group(2))) if gdb_version < gdb_min_version: print('gdb 7.8 or newer must be installed to test the pretty printers.') exit(UNSUPPORTED) # Check if gdb supports Python. gdb_python_cmd = '{0} -ex "python import os" -batch'.format(gdb_invocation, timeout=timeout) gdb_python_error = pexpect.run(gdb_python_cmd, encoding=encoding) if gdb_python_error: print('gdb must have python support to test the pretty printers.') print('gdb output: {!r}'.format(gdb_python_error)) exit(UNSUPPORTED) # If everything's ok, spawn the gdb process we'll use for testing. gdb = pexpect.spawn(gdb_invocation, echo=False, timeout=timeout, encoding=encoding) gdb_prompt = u'\(gdb\)' gdb.expect(gdb_prompt) except pexpect.ExceptionPexpect as exception: print('Error: {0}'.format(exception)) exit(FAIL) def test(command, pattern=None): """Sends 'command' to gdb and expects the given 'pattern'. If 'pattern' is None, simply consumes everything up to and including the gdb prompt. Args: command (string): The command we'll send to gdb. pattern (raw string): A pattern the gdb output should match. Returns: string: The string that matched 'pattern', or an empty string if 'pattern' was None. """ match = '' gdb.sendline(command) if pattern: # PExpect does a non-greedy match for '+' and '*'. Since it can't look # ahead on the gdb output stream, if 'pattern' ends with a '+' or a '*' # we may end up matching only part of the required output. # To avoid this, we'll consume 'pattern' and anything that follows it # up to and including the gdb prompt, then extract 'pattern' later. index = gdb.expect([u'{0}.+{1}'.format(pattern, gdb_prompt), pexpect.TIMEOUT]) if index == 0: # gdb.after now contains the whole match. Extract the text that # matches 'pattern'. match = re.match(pattern, gdb.after, re.DOTALL).group() elif index == 1: # We got a timeout exception. Print information on what caused it # and bail out. error = ('Response does not match the expected pattern.\n' 'Command: {0}\n' 'Expected pattern: {1}\n' 'Response: {2}'.format(command, pattern, gdb.before)) raise pexpect.TIMEOUT(error) else: # Consume just the the gdb prompt. gdb.expect(gdb_prompt) return match def init_test(test_bin, printer_files, printer_names): """Loads the test binary file and the required pretty printers to gdb. Args: test_bin (string): The name of the test binary file. pretty_printers (list of strings): A list with the names of the pretty printer files. """ # Load all the pretty printer files. We're assuming these are safe. for printer_file in printer_files: test('source {0}'.format(printer_file)) # Disable all the pretty printers. test('disable pretty-printer', r'0 of [0-9]+ printers enabled') # Enable only the required printers. for printer in printer_names: test('enable pretty-printer {0}'.format(printer), r'[1-9][0-9]* of [1-9]+ printers enabled') # Finally, load the test binary. test('file {0}'.format(test_bin)) # Disable lock elision. test('set environment GLIBC_TUNABLES glibc.elision.enable=0') def go_to_main(): """Executes a gdb 'start' command, which takes us to main.""" test('start', r'main') def get_line_number(file_name, string): """Returns the number of the line in which 'string' appears within a file. Args: file_name (string): The name of the file we'll search through. string (string): The string we'll look for. Returns: int: The number of the line in which 'string' appears, starting from 1. """ number = -1 with open(file_name) as src_file: for i, line in enumerate(src_file): if string in line: number = i + 1 break if number == -1: raise NoLineError(file_name, string) return number def break_at(file_name, string, temporary=True, thread=None): """Places a breakpoint on the first line in 'file_name' containing 'string'. 'string' is usually a comment like "Stop here". Notice this may fail unless the comment is placed inline next to actual code, e.g.: ... /* Stop here */ ... may fail, while: ... some_func(); /* Stop here */ ... will succeed. If 'thread' isn't None, the breakpoint will be set for all the threads. Otherwise, it'll be set only for 'thread'. Args: file_name (string): The name of the file we'll place the breakpoint in. string (string): A string we'll look for inside the file. We'll place a breakpoint on the line which contains it. temporary (bool): Whether the breakpoint should be automatically deleted after we reach it. thread (int): The number of the thread we'll place the breakpoint for, as seen by gdb. If specified, it should be greater than zero. """ if not thread: thread_str = '' else: thread_str = 'thread {0}'.format(thread) if temporary: command = 'tbreak' break_type = 'Temporary breakpoint' else: command = 'break' break_type = 'Breakpoint' line_number = str(get_line_number(file_name, string)) test('{0} {1}:{2} {3}'.format(command, file_name, line_number, thread_str), r'{0} [0-9]+ at 0x[a-f0-9]+: file {1}, line {2}\.'.format(break_type, file_name, line_number)) def continue_cmd(thread=None): """Executes a gdb 'continue' command. If 'thread' isn't None, the command will be applied to all the threads. Otherwise, it'll be applied only to 'thread'. Args: thread (int): The number of the thread we'll apply the command to, as seen by gdb. If specified, it should be greater than zero. """ if not thread: command = 'continue' else: command = 'thread apply {0} continue'.format(thread) test(command) def next_cmd(count=1, thread=None): """Executes a gdb 'next' command. If 'thread' isn't None, the command will be applied to all the threads. Otherwise, it'll be applied only to 'thread'. Args: count (int): The 'count' argument of the 'next' command. thread (int): The number of the thread we'll apply the command to, as seen by gdb. If specified, it should be greater than zero. """ if not thread: command = 'next' else: command = 'thread apply {0} next' test('{0} {1}'.format(command, count)) def select_thread(thread): """Selects the thread indicated by 'thread'. Args: thread (int): The number of the thread we'll switch to, as seen by gdb. This should be greater than zero. """ if thread > 0: test('thread {0}'.format(thread)) def get_current_thread_lwpid(): """Gets the current thread's Lightweight Process ID. Returns: string: The current thread's LWP ID. """ # It's easier to get the LWP ID through the Python API than the gdb CLI. command = 'python print(gdb.selected_thread().ptid[1])' return test(command, r'[0-9]+') def set_scheduler_locking(mode): """Executes the gdb 'set scheduler-locking' command. Args: mode (bool): Whether the scheduler locking mode should be 'on'. """ modes = { True: 'on', False: 'off' } test('set scheduler-locking {0}'.format(modes[mode])) def test_printer(var, to_string, children=None, is_ptr=True): """ Tests the output of a pretty printer. For a variable called 'var', this tests whether its associated printer outputs the expected 'to_string' and children (if any). Args: var (string): The name of the variable we'll print. to_string (raw string): The expected output of the printer's 'to_string' method. children (map {raw string->raw string}): A map with the expected output of the printer's children' method. is_ptr (bool): Whether 'var' is a pointer, and thus should be dereferenced. """ if is_ptr: var = '*{0}'.format(var) test('print {0}'.format(var), to_string) if children: for name, value in children.items(): # Children are shown as 'name = value'. test('print {0}'.format(var), r'{0} = {1}'.format(name, value)) def check_debug_symbol(symbol): """ Tests whether a given debugging symbol exists. If the symbol doesn't exist, raises a DebugError. Args: symbol (string): The symbol we're going to check for. """ try: test('ptype {0}'.format(symbol), r'type = {0}'.format(symbol)) except pexpect.TIMEOUT: # The symbol doesn't exist. raise DebugError(symbol)