diff options
Diffstat (limited to 'scripts')
-rw-r--r-- | scripts/gen-py-const.awk | 118 | ||||
-rw-r--r-- | scripts/test_printers_common.py | 364 | ||||
-rw-r--r-- | scripts/test_printers_exceptions.py | 61 |
3 files changed, 543 insertions, 0 deletions
diff --git a/scripts/gen-py-const.awk b/scripts/gen-py-const.awk new file mode 100644 index 0000000000..4586f59d89 --- /dev/null +++ b/scripts/gen-py-const.awk @@ -0,0 +1,118 @@ +# Script to generate constants for 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/>. + +# This script is a smaller version of the clever gen-asm-const.awk hack used to +# generate ASM constants from .sym files. We'll use this to generate constants +# for Python pretty printers. +# +# The input to this script are .pysym files that look like: +# #C_Preprocessor_Directive... +# NAME1 +# NAME2 expression... +# +# A line giving just a name implies an expression consisting of just that name. +# Comments start with '--'. +# +# The output of this script is a 'dummy' function containing 'asm' declarations +# for each non-preprocessor line in the .pysym file. The expression values +# will appear as input operands to the 'asm' declaration. For example, if we +# have: +# +# /* header.h */ +# #define MACRO 42 +# +# struct S { +# char c1; +# char c2; +# char c3; +# }; +# +# enum E { +# ZERO, +# ONE +# }; +# +# /* symbols.pysym */ +# #include <stddef.h> +# #include "header.h" +# -- This is a comment +# MACRO +# C3_OFFSET offsetof(struct S, c3) +# E_ONE ONE +# +# the output will be: +# +# #include <stddef.h> +# #include "header.h" +# void dummy(void) +# { +# asm ("@name@MACRO@value@%0@" : : "i" (MACRO)); +# asm ("@name@C3_OFFSET@value@%0@" : : "i" (offsetof(struct S, c3))); +# asm ("@name@E_ONE@value@%0@" : : "i" (ONE)); +# } +# +# We'll later feed this output to gcc -S. Since '-S' tells gcc to compile but +# not assemble, gcc will output something like: +# +# dummy: +# ... +# @name@MACRO@value@$42@ +# @name@C3_OFFSET@value@$2@ +# @name@E_ONE@value@$1@ +# +# Finally, we can process that output to extract the constant values. +# Notice gcc may prepend a special character such as '$' to each value. + +# found_symbol indicates whether we found a non-comment, non-preprocessor line. +BEGIN { found_symbol = 0 } + +# C preprocessor directives go straight through. +/^#/ { print; next; } + +# Skip comments. +/--/ { next; } + +# Trim leading whitespace. +{ sub(/^[[:blank:]]*/, ""); } + +# If we found a non-comment, non-preprocessor line, print the 'dummy' function +# header. +NF > 0 && !found_symbol { + print "void dummy(void)\n{"; + found_symbol = 1; +} + +# If the line contains just a name, duplicate it so we can use that name +# as the value of the expression. +NF == 1 { sub(/^.*$/, "& &"); } + +# If a line contains a name and an expression... +NF > 1 { + name = $1; + + # Remove any characters before the second field. + sub(/^[^[:blank:]]+[[:blank:]]+/, ""); + + # '$0' ends up being everything that appeared after the first field + # separator. + printf " asm (\"@name@%s@value@%0@\" : : \"i\" (%s));\n", name, $0; +} + +# Close the 'dummy' function. +END { if (found_symbol) print "}"; } 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) diff --git a/scripts/test_printers_exceptions.py b/scripts/test_printers_exceptions.py new file mode 100644 index 0000000000..17034b544e --- /dev/null +++ b/scripts/test_printers_exceptions.py @@ -0,0 +1,61 @@ +# Exception classes used when 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/>. + +class NoLineError(Exception): + """Custom exception to indicate that a test file doesn't contain + the requested string. + """ + + def __init__(self, file_name, string): + """Constructor. + + Args: + file_name (string): The name of the test file. + string (string): The string that was requested. + """ + + super(NoLineError, self).__init__() + self.file_name = file_name + self.string = string + + def __str__(self): + """Shows a readable representation of the exception.""" + + return ('File {0} has no line containing the following string: {1}' + .format(self.file_name, self.string)) + +class DebugError(Exception): + """Custom exception to indicate that a required debugging symbol is missing. + """ + + def __init__(self, symbol): + """Constructor. + + Args: + symbol (string): The name of the entity whose debug info is missing. + """ + + super(DebugError, self).__init__() + self.symbol = symbol + + def __str__(self): + """Shows a readable representation of the exception.""" + + return ('The required debugging information for {0} is missing.' + .format(self.symbol)) |