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