diff options
Diffstat (limited to 'scripts/test_printers_common.py')
-rw-r--r-- | scripts/test_printers_common.py | 364 |
1 files changed, 364 insertions, 0 deletions
diff --git a/scripts/test_printers_common.py b/scripts/test_printers_common.py new file mode 100644 index 0000000000..c79d7e3be2 --- /dev/null +++ b/scripts/test_printers_common.py @@ -0,0 +1,364 @@ +# Common functions and variables for testing the Python pretty printers. +# +# Copyright (C) 2016 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 +# <http://www.gnu.org/licenses/>. + +"""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 <PKGVERSION><version>", 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.') + 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)) + +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) |