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