1#!/usr/bin/env python 2#------------------------------------------------------------------------------- 3# test/run_readelf_tests.py 4# 5# Automatic test runner for elftools & readelf 6# 7# Eli Bendersky (eliben@gmail.com) 8# This code is in the public domain 9#------------------------------------------------------------------------------- 10import os, sys 11import re 12from difflib import SequenceMatcher 13from optparse import OptionParser 14import logging 15import platform 16from utils import setup_syspath; setup_syspath() 17from utils import run_exe, is_in_rootdir, dump_output_to_temp_files 18 19 20# Create a global logger object 21# 22testlog = logging.getLogger('run_tests') 23testlog.setLevel(logging.DEBUG) 24testlog.addHandler(logging.StreamHandler(sys.stdout)) 25 26# Set the path for calling readelf. We carry our own version of readelf around, 27# because binutils tend to change its output even between daily builds of the 28# same minor release and keeping track is a headache. 29READELF_PATH = 'test/external_tools/readelf' 30if not os.path.exists(READELF_PATH): 31 READELF_PATH = 'readelf' 32 33def discover_testfiles(rootdir): 34 """ Discover test files in the given directory. Yield them one by one. 35 """ 36 for filename in os.listdir(rootdir): 37 _, ext = os.path.splitext(filename) 38 if ext == '.elf': 39 yield os.path.join(rootdir, filename) 40 41 42def run_test_on_file(filename, verbose=False): 43 """ Runs a test on the given input filename. Return True if all test 44 runs succeeded. 45 """ 46 success = True 47 testlog.info("Test file '%s'" % filename) 48 for option in [ 49 '-e', '-d', '-s', '-r', '-x.text', '-p.shstrtab', '-V', 50 '--debug-dump=info', '--debug-dump=decodedline', 51 '--debug-dump=frames', '--debug-dump=frames-interp']: 52 if verbose: testlog.info("..option='%s'" % option) 53 # stdouts will be a 2-element list: output of readelf and output 54 # of scripts/readelf.py 55 stdouts = [] 56 for exe_path in [READELF_PATH, 'scripts/readelf.py']: 57 args = [option, filename] 58 if verbose: testlog.info("....executing: '%s %s'" % ( 59 exe_path, ' '.join(args))) 60 rc, stdout = run_exe(exe_path, args) 61 if rc != 0: 62 testlog.error("@@ aborting - '%s' returned '%s'" % (exe_path, rc)) 63 return False 64 stdouts.append(stdout) 65 if verbose: testlog.info('....comparing output...') 66 rc, errmsg = compare_output(*stdouts) 67 if rc: 68 if verbose: testlog.info('.......................SUCCESS') 69 else: 70 success = False 71 testlog.info('.......................FAIL') 72 testlog.info('....for option "%s"' % option) 73 testlog.info('....Output #1 is readelf, Output #2 is pyelftools') 74 testlog.info('@@ ' + errmsg) 75 dump_output_to_temp_files(testlog, *stdouts) 76 return success 77 78 79def compare_output(s1, s2): 80 """ Compare stdout strings s1 and s2. 81 s1 is from readelf, s2 from elftools readelf.py 82 Return pair success, errmsg. If comparison succeeds, success is True 83 and errmsg is empty. Otherwise success is False and errmsg holds a 84 description of the mismatch. 85 86 Note: this function contains some rather horrible hacks to ignore 87 differences which are not important for the verification of pyelftools. 88 This is due to some intricacies of binutils's readelf which pyelftools 89 doesn't currently implement, features that binutils doesn't support, 90 or silly inconsistencies in the output of readelf, which I was reluctant 91 to replicate. Read the documentation for more details. 92 """ 93 def prepare_lines(s): 94 return [line for line in s.lower().splitlines() if line.strip() != ''] 95 def filter_readelf_lines(lines): 96 filter_out = False 97 for line in lines: 98 if 'of the .eh_frame section' in line: 99 filter_out = True 100 elif 'of the .debug_frame section' in line: 101 filter_out = False 102 if not filter_out: 103 if not line.startswith('unknown: length'): 104 yield line 105 106 lines1 = prepare_lines(s1) 107 lines2 = prepare_lines(s2) 108 109 lines1 = list(filter_readelf_lines(lines1)) 110 111 flag_after_symtable = False 112 113 if len(lines1) != len(lines2): 114 return False, 'Number of lines different: %s vs %s' % ( 115 len(lines1), len(lines2)) 116 117 for i in range(len(lines1)): 118 if 'symbol table' in lines1[i]: 119 flag_after_symtable = True 120 121 # Compare ignoring whitespace 122 lines1_parts = lines1[i].split() 123 lines2_parts = lines2[i].split() 124 125 if ''.join(lines1_parts) != ''.join(lines2_parts): 126 ok = False 127 128 try: 129 # Ignore difference in precision of hex representation in the 130 # last part (i.e. 008f3b vs 8f3b) 131 if (''.join(lines1_parts[:-1]) == ''.join(lines2_parts[:-1]) and 132 int(lines1_parts[-1], 16) == int(lines2_parts[-1], 16)): 133 ok = True 134 except ValueError: 135 pass 136 137 sm = SequenceMatcher() 138 sm.set_seqs(lines1[i], lines2[i]) 139 changes = sm.get_opcodes() 140 if flag_after_symtable: 141 # Detect readelf's adding @ with lib and version after 142 # symbol name. 143 if ( len(changes) == 2 and changes[1][0] == 'delete' and 144 lines1[i][changes[1][1]] == '@'): 145 ok = True 146 elif 'at_const_value' in lines1[i]: 147 # On 32-bit machines, readelf doesn't correctly represent 148 # some boundary LEB128 numbers 149 val = lines2_parts[-1] 150 num2 = int(val, 16 if val.startswith('0x') else 10) 151 if num2 <= -2**31 and '32' in platform.architecture()[0]: 152 ok = True 153 elif 'os/abi' in lines1[i]: 154 if 'unix - gnu' in lines1[i] and 'unix - linux' in lines2[i]: 155 ok = True 156 elif ( 'unknown at value' in lines1[i] and 157 'dw_at_apple' in lines2[i]): 158 ok = True 159 else: 160 for s in ('t (tls)', 'l (large)'): 161 if s in lines1[i] or s in lines2[i]: 162 ok = True 163 break 164 if not ok: 165 errmsg = 'Mismatch on line #%s:\n>>%s<<\n>>%s<<\n (%r)' % ( 166 i, lines1[i], lines2[i], changes) 167 return False, errmsg 168 return True, '' 169 170 171def main(): 172 if not is_in_rootdir(): 173 testlog.error('Error: Please run me from the root dir of pyelftools!') 174 return 1 175 176 optparser = OptionParser( 177 usage='usage: %prog [options] [file] [file] ...', 178 prog='run_readelf_tests.py') 179 optparser.add_option('-V', '--verbose', 180 action='store_true', dest='verbose', 181 help='Verbose output') 182 options, args = optparser.parse_args() 183 184 if options.verbose: 185 testlog.info('Running in verbose mode') 186 testlog.info('Python executable = %s' % sys.executable) 187 testlog.info('readelf path = %s' % READELF_PATH) 188 testlog.info('Given list of files: %s' % args) 189 190 # If file names are given as command-line arguments, only these files 191 # are taken as inputs. Otherwise, autodiscovery is performed. 192 # 193 if len(args) > 0: 194 filenames = args 195 else: 196 filenames = list(discover_testfiles('test/testfiles_for_readelf')) 197 198 success = True 199 for filename in filenames: 200 if success: 201 success = success and run_test_on_file( 202 filename, 203 verbose=options.verbose) 204 205 if success: 206 testlog.info('\nConclusion: SUCCESS') 207 return 0 208 else: 209 testlog.info('\nConclusion: FAIL') 210 return 1 211 212 213if __name__ == '__main__': 214 sys.exit(main()) 215 216