Merge pull request #89 from sethml/feature/improved-DWARFv4
[pyelftools.git] / test / run_readelf_tests.py
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 #-------------------------------------------------------------------------------
10 import os, sys
11 import re
12 from difflib import SequenceMatcher
13 from optparse import OptionParser
14 import logging
15 import platform
16 from utils import setup_syspath; setup_syspath()
17 from utils import run_exe, is_in_rootdir, dump_output_to_temp_files
18
19
20 # Create a global logger object
21 #
22 testlog = logging.getLogger('run_tests')
23 testlog.setLevel(logging.DEBUG)
24 testlog.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.
29 READELF_PATH = 'test/external_tools/readelf'
30 if not os.path.exists(READELF_PATH):
31 READELF_PATH = 'readelf'
32
33 def 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
42 def 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', '-n', '-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
79 def 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
171 def 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
213 if __name__ == '__main__':
214 sys.exit(main())