1# Common functions and variables for testing the Python pretty printers.
2#
3# Copyright (C) 2016-2022 Free Software Foundation, Inc.
4# This file is part of the GNU C Library.
5#
6# The GNU C Library is free software; you can redistribute it and/or
7# modify it under the terms of the GNU Lesser General Public
8# License as published by the Free Software Foundation; either
9# version 2.1 of the License, or (at your option) any later version.
10#
11# The GNU C Library is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14# Lesser General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public
17# License along with the GNU C Library; if not, see
18# <https://www.gnu.org/licenses/>.
19
20"""These tests require PExpect 4.0 or newer.
21
22Exported constants:
23    PASS, FAIL, UNSUPPORTED (int): Test exit codes, as per evaluate-test.sh.
24"""
25
26import os
27import re
28from test_printers_exceptions import *
29
30PASS = 0
31FAIL = 1
32UNSUPPORTED = 77
33
34gdb_bin = 'gdb'
35gdb_options = '-q -nx'
36gdb_invocation = '{0} {1}'.format(gdb_bin, gdb_options)
37pexpect_min_version = 4
38gdb_min_version = (7, 8)
39encoding = 'utf-8'
40
41try:
42    import pexpect
43except ImportError:
44    print('PExpect 4.0 or newer must be installed to test the pretty printers.')
45    exit(UNSUPPORTED)
46
47pexpect_version = pexpect.__version__.split('.')[0]
48
49if int(pexpect_version) < pexpect_min_version:
50    print('PExpect 4.0 or newer must be installed to test the pretty printers.')
51    exit(UNSUPPORTED)
52
53if not pexpect.which(gdb_bin):
54    print('gdb 7.8 or newer must be installed to test the pretty printers.')
55    exit(UNSUPPORTED)
56
57timeout = 5
58TIMEOUTFACTOR = os.environ.get('TIMEOUTFACTOR')
59
60if TIMEOUTFACTOR:
61    timeout = int(TIMEOUTFACTOR)
62
63# Otherwise GDB is run in interactive mode and readline may send escape
64# sequences confusing output for pexpect.
65os.environ["TERM"]="dumb"
66
67try:
68    # Check the gdb version.
69    version_cmd = '{0} --version'.format(gdb_invocation, timeout=timeout)
70    gdb_version_out = pexpect.run(version_cmd, encoding=encoding)
71
72    # The gdb version string is "GNU gdb <PKGVERSION><version>", where
73    # PKGVERSION can be any text.  We assume that there'll always be a space
74    # between PKGVERSION and the version number for the sake of the regexp.
75    version_match = re.search(r'GNU gdb .* ([1-9][0-9]*)\.([0-9]+)',
76                              gdb_version_out)
77
78    if not version_match:
79        print('The gdb version string (gdb -v) is incorrectly formatted.')
80        exit(UNSUPPORTED)
81
82    gdb_version = (int(version_match.group(1)), int(version_match.group(2)))
83
84    if gdb_version < gdb_min_version:
85        print('gdb 7.8 or newer must be installed to test the pretty printers.')
86        exit(UNSUPPORTED)
87
88    # Check if gdb supports Python.
89    gdb_python_cmd = '{0} -ex "python import os" -batch'.format(gdb_invocation,
90                                                                timeout=timeout)
91    gdb_python_error = pexpect.run(gdb_python_cmd, encoding=encoding)
92
93    if gdb_python_error:
94        print('gdb must have python support to test the pretty printers.')
95        print('gdb output: {!r}'.format(gdb_python_error))
96        exit(UNSUPPORTED)
97
98    # If everything's ok, spawn the gdb process we'll use for testing.
99    gdb = pexpect.spawn(gdb_invocation, echo=False, timeout=timeout,
100                        encoding=encoding)
101    gdb_prompt = u'\(gdb\)'
102    gdb.expect(gdb_prompt)
103
104except pexpect.ExceptionPexpect as exception:
105    print('Error: {0}'.format(exception))
106    exit(FAIL)
107
108def test(command, pattern=None):
109    """Sends 'command' to gdb and expects the given 'pattern'.
110
111    If 'pattern' is None, simply consumes everything up to and including
112    the gdb prompt.
113
114    Args:
115        command (string): The command we'll send to gdb.
116        pattern (raw string): A pattern the gdb output should match.
117
118    Returns:
119        string: The string that matched 'pattern', or an empty string if
120            'pattern' was None.
121    """
122
123    match = ''
124
125    gdb.sendline(command)
126
127    if pattern:
128        # PExpect does a non-greedy match for '+' and '*'.  Since it can't look
129        # ahead on the gdb output stream, if 'pattern' ends with a '+' or a '*'
130        # we may end up matching only part of the required output.
131        # To avoid this, we'll consume 'pattern' and anything that follows it
132        # up to and including the gdb prompt, then extract 'pattern' later.
133        index = gdb.expect([u'{0}.+{1}'.format(pattern, gdb_prompt),
134                            pexpect.TIMEOUT])
135
136        if index == 0:
137            # gdb.after now contains the whole match.  Extract the text that
138            # matches 'pattern'.
139            match = re.match(pattern, gdb.after, re.DOTALL).group()
140        elif index == 1:
141            # We got a timeout exception.  Print information on what caused it
142            # and bail out.
143            error = ('Response does not match the expected pattern.\n'
144                     'Command: {0}\n'
145                     'Expected pattern: {1}\n'
146                     'Response: {2}'.format(command, pattern, gdb.before))
147
148            raise pexpect.TIMEOUT(error)
149    else:
150        # Consume just the the gdb prompt.
151        gdb.expect(gdb_prompt)
152
153    return match
154
155def init_test(test_bin, printer_files, printer_names):
156    """Loads the test binary file and the required pretty printers to gdb.
157
158    Args:
159        test_bin (string): The name of the test binary file.
160        pretty_printers (list of strings): A list with the names of the pretty
161            printer files.
162    """
163
164    # Disable debuginfod to avoid GDB messages like:
165    #
166    # This GDB supports auto-downloading debuginfo from the following URLs:
167    # https://debuginfod.fedoraproject.org/
168    # Enable debuginfod for this session? (y or [n])
169    #
170    try:
171        test('set debuginfod enabled off')
172    except Exception:
173        pass
174
175    # Load all the pretty printer files.  We're assuming these are safe.
176    for printer_file in printer_files:
177        test('source {0}'.format(printer_file))
178
179    # Disable all the pretty printers.
180    test('disable pretty-printer', r'0 of [0-9]+ printers enabled')
181
182    # Enable only the required printers.
183    for printer in printer_names:
184        test('enable pretty-printer {0}'.format(printer),
185             r'[1-9][0-9]* of [1-9]+ printers enabled')
186
187    # Finally, load the test binary.
188    test('file {0}'.format(test_bin))
189
190    # Disable lock elision.
191    test('set environment GLIBC_TUNABLES glibc.elision.enable=0')
192
193def go_to_main():
194    """Executes a gdb 'start' command, which takes us to main."""
195
196    test('start', r'main')
197
198def get_line_number(file_name, string):
199    """Returns the number of the line in which 'string' appears within a file.
200
201    Args:
202        file_name (string): The name of the file we'll search through.
203        string (string): The string we'll look for.
204
205    Returns:
206        int: The number of the line in which 'string' appears, starting from 1.
207    """
208    number = -1
209
210    with open(file_name) as src_file:
211        for i, line in enumerate(src_file):
212            if string in line:
213                number = i + 1
214                break
215
216    if number == -1:
217        raise NoLineError(file_name, string)
218
219    return number
220
221def break_at(file_name, string, temporary=True, thread=None):
222    """Places a breakpoint on the first line in 'file_name' containing 'string'.
223
224    'string' is usually a comment like "Stop here".  Notice this may fail unless
225    the comment is placed inline next to actual code, e.g.:
226
227        ...
228        /* Stop here */
229        ...
230
231    may fail, while:
232
233        ...
234        some_func(); /* Stop here */
235        ...
236
237    will succeed.
238
239    If 'thread' isn't None, the breakpoint will be set for all the threads.
240    Otherwise, it'll be set only for 'thread'.
241
242    Args:
243        file_name (string): The name of the file we'll place the breakpoint in.
244        string (string): A string we'll look for inside the file.
245            We'll place a breakpoint on the line which contains it.
246        temporary (bool): Whether the breakpoint should be automatically deleted
247            after we reach it.
248        thread (int): The number of the thread we'll place the breakpoint for,
249            as seen by gdb.  If specified, it should be greater than zero.
250    """
251
252    if not thread:
253        thread_str = ''
254    else:
255        thread_str = 'thread {0}'.format(thread)
256
257    if temporary:
258        command = 'tbreak'
259        break_type = 'Temporary breakpoint'
260    else:
261        command = 'break'
262        break_type = 'Breakpoint'
263
264    line_number = str(get_line_number(file_name, string))
265
266    test('{0} {1}:{2} {3}'.format(command, file_name, line_number, thread_str),
267         r'{0} [0-9]+ at 0x[a-f0-9]+: file {1}, line {2}\.'.format(break_type,
268                                                                   file_name,
269                                                                   line_number))
270
271def continue_cmd(thread=None):
272    """Executes a gdb 'continue' command.
273
274    If 'thread' isn't None, the command will be applied to all the threads.
275    Otherwise, it'll be applied only to 'thread'.
276
277    Args:
278        thread (int): The number of the thread we'll apply the command to,
279            as seen by gdb.  If specified, it should be greater than zero.
280    """
281
282    if not thread:
283        command = 'continue'
284    else:
285        command = 'thread apply {0} continue'.format(thread)
286
287    test(command)
288
289def next_cmd(count=1, thread=None):
290    """Executes a gdb 'next' command.
291
292    If 'thread' isn't None, the command will be applied to all the threads.
293    Otherwise, it'll be applied only to 'thread'.
294
295    Args:
296        count (int): The 'count' argument of the 'next' command.
297        thread (int): The number of the thread we'll apply the command to,
298            as seen by gdb.  If specified, it should be greater than zero.
299    """
300
301    if not thread:
302        command = 'next'
303    else:
304        command = 'thread apply {0} next'
305
306    test('{0} {1}'.format(command, count))
307
308def select_thread(thread):
309    """Selects the thread indicated by 'thread'.
310
311    Args:
312        thread (int): The number of the thread we'll switch to, as seen by gdb.
313            This should be greater than zero.
314    """
315
316    if thread > 0:
317        test('thread {0}'.format(thread))
318
319def get_current_thread_lwpid():
320    """Gets the current thread's Lightweight Process ID.
321
322    Returns:
323        string: The current thread's LWP ID.
324    """
325
326    # It's easier to get the LWP ID through the Python API than the gdb CLI.
327    command = 'python print(gdb.selected_thread().ptid[1])'
328
329    return test(command, r'[0-9]+')
330
331def set_scheduler_locking(mode):
332    """Executes the gdb 'set scheduler-locking' command.
333
334    Args:
335        mode (bool): Whether the scheduler locking mode should be 'on'.
336    """
337    modes = {
338        True: 'on',
339        False: 'off'
340    }
341
342    test('set scheduler-locking {0}'.format(modes[mode]))
343
344def test_printer(var, to_string, children=None, is_ptr=True):
345    """ Tests the output of a pretty printer.
346
347    For a variable called 'var', this tests whether its associated printer
348    outputs the expected 'to_string' and children (if any).
349
350    Args:
351        var (string): The name of the variable we'll print.
352        to_string (raw string): The expected output of the printer's 'to_string'
353            method.
354        children (map {raw string->raw string}): A map with the expected output
355            of the printer's children' method.
356        is_ptr (bool): Whether 'var' is a pointer, and thus should be
357            dereferenced.
358    """
359
360    if is_ptr:
361        var = '*{0}'.format(var)
362
363    test('print {0}'.format(var), to_string)
364
365    if children:
366        for name, value in children.items():
367            # Children are shown as 'name = value'.
368            test('print {0}'.format(var), r'{0} = {1}'.format(name, value))
369
370def check_debug_symbol(symbol):
371    """ Tests whether a given debugging symbol exists.
372
373    If the symbol doesn't exist, raises a DebugError.
374
375    Args:
376        symbol (string): The symbol we're going to check for.
377    """
378
379    try:
380        test('ptype {0}'.format(symbol), r'type = {0}'.format(symbol))
381
382    except pexpect.TIMEOUT:
383        # The symbol doesn't exist.
384        raise DebugError(symbol)
385