1import subprocess
2import unittest
3import sys
4import os
5import imp
6from tempfile import mkdtemp
7from shutil import rmtree
8import mozunit
9
10from UserString import UserString
11# Create a controlled configuration for use by expandlibs
12config_win = {
13    'AR': 'lib',
14    'AR_EXTRACT': '',
15    'DLL_PREFIX': '',
16    'LIB_PREFIX': '',
17    'OBJ_SUFFIX': '.obj',
18    'LIB_SUFFIX': '.lib',
19    'DLL_SUFFIX': '.dll',
20    'IMPORT_LIB_SUFFIX': '.lib',
21    'LIBS_DESC_SUFFIX': '.desc',
22    'EXPAND_LIBS_LIST_STYLE': 'list',
23}
24config_unix = {
25    'AR': 'ar',
26    'AR_EXTRACT': 'ar -x',
27    'DLL_PREFIX': 'lib',
28    'LIB_PREFIX': 'lib',
29    'OBJ_SUFFIX': '.o',
30    'LIB_SUFFIX': '.a',
31    'DLL_SUFFIX': '.so',
32    'IMPORT_LIB_SUFFIX': '',
33    'LIBS_DESC_SUFFIX': '.desc',
34    'EXPAND_LIBS_LIST_STYLE': 'linkerscript',
35}
36
37config = sys.modules['expandlibs_config'] = imp.new_module('expandlibs_config')
38
39from expandlibs import LibDescriptor, ExpandArgs, relativize
40from expandlibs_gen import generate
41from expandlibs_exec import ExpandArgsMore, SectionFinder
42
43def Lib(name):
44    return config.LIB_PREFIX + name + config.LIB_SUFFIX
45
46def Obj(name):
47    return name + config.OBJ_SUFFIX
48
49def Dll(name):
50    return config.DLL_PREFIX + name + config.DLL_SUFFIX
51
52def ImportLib(name):
53    if not len(config.IMPORT_LIB_SUFFIX): return Dll(name)
54    return config.LIB_PREFIX + name + config.IMPORT_LIB_SUFFIX
55
56class TestRelativize(unittest.TestCase):
57    def test_relativize(self):
58        '''Test relativize()'''
59        os_path_exists = os.path.exists
60        def exists(path):
61            return True
62        os.path.exists = exists
63        self.assertEqual(relativize(os.path.abspath(os.curdir)), os.curdir)
64        self.assertEqual(relativize(os.path.abspath(os.pardir)), os.pardir)
65        self.assertEqual(relativize(os.path.join(os.curdir, 'a')), 'a')
66        self.assertEqual(relativize(os.path.join(os.path.abspath(os.curdir), 'a')), 'a')
67        # relativize is expected to return the absolute path if it is shorter
68        self.assertEqual(relativize(os.sep), os.sep)
69        os.path.exists = os.path.exists
70
71class TestLibDescriptor(unittest.TestCase):
72    def test_serialize(self):
73        '''Test LibDescriptor's serialization'''
74        desc = LibDescriptor()
75        desc[LibDescriptor.KEYS[0]] = ['a', 'b']
76        self.assertEqual(str(desc), "{0} = a b".format(LibDescriptor.KEYS[0]))
77        desc['unsupported-key'] = ['a']
78        self.assertEqual(str(desc), "{0} = a b".format(LibDescriptor.KEYS[0]))
79        desc[LibDescriptor.KEYS[1]] = ['c', 'd', 'e']
80        self.assertEqual(str(desc),
81                         "{0} = a b\n{1} = c d e"
82                         .format(LibDescriptor.KEYS[0], LibDescriptor.KEYS[1]))
83        desc[LibDescriptor.KEYS[0]] = []
84        self.assertEqual(str(desc), "{0} = c d e".format(LibDescriptor.KEYS[1]))
85
86    def test_read(self):
87        '''Test LibDescriptor's initialization'''
88        desc_list = ["# Comment",
89                     "{0} = a b".format(LibDescriptor.KEYS[1]),
90                     "", # Empty line
91                     "foo = bar", # Should be discarded
92                     "{0} = c d e".format(LibDescriptor.KEYS[0])]
93        desc = LibDescriptor(desc_list)
94        self.assertEqual(desc[LibDescriptor.KEYS[1]], ['a', 'b'])
95        self.assertEqual(desc[LibDescriptor.KEYS[0]], ['c', 'd', 'e'])
96        self.assertEqual(False, 'foo' in desc)
97
98def wrap_method(conf, wrapped_method):
99    '''Wrapper used to call a test with a specific configuration'''
100    def _method(self):
101        for key in conf:
102            setattr(config, key, conf[key])
103        self.init()
104        try:
105            wrapped_method(self)
106        except:
107            raise
108        finally:
109            self.cleanup()
110    return _method
111
112class ReplicateTests(type):
113    '''Replicates tests for unix and windows variants'''
114    def __new__(cls, clsName, bases, dict):
115        for name in [key for key in dict if key.startswith('test_')]:
116            dict[name + '_unix'] = wrap_method(config_unix, dict[name])
117            dict[name + '_unix'].__doc__ = dict[name].__doc__ + ' (unix)'
118            dict[name + '_win'] = wrap_method(config_win, dict[name])
119            dict[name + '_win'].__doc__ = dict[name].__doc__ + ' (win)'
120            del dict[name]
121        return type.__new__(cls, clsName, bases, dict)
122
123class TestCaseWithTmpDir(unittest.TestCase):
124    __metaclass__ = ReplicateTests
125    def init(self):
126        self.tmpdir = os.path.abspath(mkdtemp(dir=os.curdir))
127
128    def cleanup(self):
129        rmtree(self.tmpdir)
130
131    def touch(self, files):
132        for f in files:
133            open(f, 'w').close()
134
135    def tmpfile(self, *args):
136        return os.path.join(self.tmpdir, *args)
137
138class TestExpandLibsGen(TestCaseWithTmpDir):
139    def test_generate(self):
140        '''Test library descriptor generation'''
141        files = [self.tmpfile(f) for f in
142                 [Lib('a'), Obj('b'), Lib('c'), Obj('d'), Obj('e'), Lib('f')]]
143        self.touch(files[:-1])
144        self.touch([files[-1] + config.LIBS_DESC_SUFFIX])
145
146        desc = generate(files)
147        self.assertEqual(desc['OBJS'], [self.tmpfile(Obj(s)) for s in ['b', 'd', 'e']])
148        self.assertEqual(desc['LIBS'], [self.tmpfile(Lib(s)) for s in ['a', 'c', 'f']])
149
150        self.assertRaises(Exception, generate, files + [self.tmpfile(Obj('z'))])
151        self.assertRaises(Exception, generate, files + [self.tmpfile(Lib('y'))])
152
153class TestExpandInit(TestCaseWithTmpDir):
154    def init(self):
155        ''' Initializes test environment for library expansion tests'''
156        super(TestExpandInit, self).init()
157        # Create 2 fake libraries, each containing 3 objects, and the second
158        # including the first one and another library.
159        os.mkdir(self.tmpfile('libx'))
160        os.mkdir(self.tmpfile('liby'))
161        self.libx_files = [self.tmpfile('libx', Obj(f)) for f in ['g', 'h', 'i']]
162        self.liby_files = [self.tmpfile('liby', Obj(f)) for f in ['j', 'k', 'l']] + [self.tmpfile('liby', Lib('z'))]
163        self.touch(self.libx_files + self.liby_files)
164        with open(self.tmpfile('libx', Lib('x') + config.LIBS_DESC_SUFFIX), 'w') as f:
165            f.write(str(generate(self.libx_files)))
166        with open(self.tmpfile('liby', Lib('y') + config.LIBS_DESC_SUFFIX), 'w') as f:
167            f.write(str(generate(self.liby_files + [self.tmpfile('libx', Lib('x'))])))
168
169        # Create various objects and libraries
170        self.arg_files = [self.tmpfile(f) for f in [Lib('a'), Obj('b'), Obj('c'), Lib('d'), Obj('e')]]
171        # We always give library names (LIB_PREFIX/SUFFIX), even for
172        # dynamic/import libraries
173        self.files = self.arg_files + [self.tmpfile(ImportLib('f'))]
174        self.arg_files += [self.tmpfile(Lib('f'))]
175        self.touch(self.files)
176
177    def assertRelEqual(self, args1, args2):
178        self.assertEqual(args1, [relativize(a) for a in args2])
179
180class TestExpandArgs(TestExpandInit):
181    def test_expand(self):
182        '''Test library expansion'''
183        # Expanding arguments means libraries with a descriptor are expanded
184        # with the descriptor content, and import libraries are used when
185        # a library doesn't exist
186        args = ExpandArgs(['foo', '-bar'] + self.arg_files + [self.tmpfile('liby', Lib('y'))])
187        self.assertRelEqual(args, ['foo', '-bar'] + self.files + self.liby_files + self.libx_files)
188
189        # When a library exists at the same time as a descriptor, we still use
190        # the descriptor.
191        self.touch([self.tmpfile('libx', Lib('x'))])
192        args = ExpandArgs(['foo', '-bar'] + self.arg_files + [self.tmpfile('liby', Lib('y'))])
193        self.assertRelEqual(args, ['foo', '-bar'] + self.files + self.liby_files + self.libx_files)
194
195        self.touch([self.tmpfile('liby', Lib('y'))])
196        args = ExpandArgs(['foo', '-bar'] + self.arg_files + [self.tmpfile('liby', Lib('y'))])
197        self.assertRelEqual(args, ['foo', '-bar'] + self.files + self.liby_files + self.libx_files)
198
199class TestExpandArgsMore(TestExpandInit):
200    def test_makelist(self):
201        '''Test grouping object files in lists'''
202        # ExpandArgsMore does the same as ExpandArgs
203        with ExpandArgsMore(['foo', '-bar'] + self.arg_files + [self.tmpfile('liby', Lib('y'))]) as args:
204            self.assertRelEqual(args, ['foo', '-bar'] + self.files + self.liby_files + self.libx_files)
205
206            # But also has an extra method replacing object files with a list
207            args.makelist()
208            # self.files has objects at #1, #2, #4
209            self.assertRelEqual(args[:3], ['foo', '-bar'] + self.files[:1])
210            self.assertRelEqual(args[4:], [self.files[3]] + self.files[5:] + [self.tmpfile('liby', Lib('z'))])
211
212            # Check the list file content
213            objs = [f for f in self.files + self.liby_files + self.libx_files if f.endswith(config.OBJ_SUFFIX)]
214            if config.EXPAND_LIBS_LIST_STYLE == "linkerscript":
215                self.assertNotEqual(args[3][0], '@')
216                filename = args[3]
217                content = ['INPUT("{0}")'.format(relativize(f)) for f in objs]
218                with open(filename, 'r') as f:
219                    self.assertEqual([l.strip() for l in f.readlines() if len(l.strip())], content)
220            elif config.EXPAND_LIBS_LIST_STYLE == "list":
221                self.assertEqual(args[3][0], '@')
222                filename = args[3][1:]
223                content = objs
224                with open(filename, 'r') as f:
225                    self.assertRelEqual([l.strip() for l in f.readlines() if len(l.strip())], content)
226
227            tmp = args.tmp
228        # Check that all temporary files are properly removed
229        self.assertEqual(True, all([not os.path.exists(f) for f in tmp]))
230
231    def test_extract(self):
232        '''Test library extraction'''
233        # Divert subprocess.call
234        subprocess_call = subprocess.call
235        subprocess_check_output = subprocess.check_output
236        def call(args, **kargs):
237            if config.AR == 'lib':
238                self.assertEqual(args[:2], [config.AR, '-NOLOGO'])
239                self.assertTrue(args[2].startswith('-EXTRACT:'))
240                extract = [args[2][len('-EXTRACT:'):]]
241                self.assertTrue(extract)
242                args = args[3:]
243            else:
244                # The command called is always AR_EXTRACT
245                ar_extract = config.AR_EXTRACT.split()
246                self.assertEqual(args[:len(ar_extract)], ar_extract)
247                args = args[len(ar_extract):]
248            # Remaining argument is always one library
249            self.assertEqual(len(args), 1)
250            arg = args[0]
251            self.assertEqual(os.path.splitext(arg)[1], config.LIB_SUFFIX)
252            # Simulate file extraction
253            lib = os.path.splitext(os.path.basename(arg))[0]
254            if config.AR != 'lib':
255                extract = [lib, lib + '2']
256            extract = [os.path.join(kargs['cwd'], f) for f in extract]
257            if config.AR != 'lib':
258                extract = [Obj(f) for f in extract]
259            if not lib in extracted:
260                extracted[lib] = []
261            extracted[lib].extend(extract)
262            self.touch(extract)
263        subprocess.call = call
264
265        def check_output(args, **kargs):
266            # The command called is always AR
267            ar = config.AR
268            self.assertEqual(args[0:3], [ar, '-NOLOGO', '-LIST'])
269            # Remaining argument is always one library
270            self.assertRelEqual([os.path.splitext(arg)[1] for arg in args[3:]],
271[config.LIB_SUFFIX])
272            # Simulate LIB -NOLOGO -LIST
273            lib = os.path.splitext(os.path.basename(args[3]))[0]
274            return '%s\n%s\n' % (Obj(lib), Obj(lib + '2'))
275        subprocess.check_output = check_output
276
277        # ExpandArgsMore does the same as ExpandArgs
278        self.touch([self.tmpfile('liby', Lib('y'))])
279        for iteration in (1, 2):
280            with ExpandArgsMore(['foo', '-bar'] + self.arg_files + [self.tmpfile('liby', Lib('y'))]) as args:
281                files = self.files + self.liby_files + self.libx_files
282
283                self.assertRelEqual(args, ['foo', '-bar'] + files)
284
285                extracted = {}
286                # ExpandArgsMore also has an extra method extracting static libraries
287                # when possible
288                args.extract()
289
290                # With AR_EXTRACT, it uses the descriptors when there are, and
291                # actually
292                # extracts the remaining libraries
293                extracted_args = []
294                for f in files:
295                    if f.endswith(config.LIB_SUFFIX):
296                        base = os.path.splitext(os.path.basename(f))[0]
297                        # On the first iteration, we test the behavior of
298                        # extracting archives that don't have a copy of their
299                        # contents next to them, which is to use the file
300                        # extracted from the archive in a temporary directory.
301                        # On the second iteration, we test extracting archives
302                        # that do have a copy of their contents next to them,
303                        # in which case those contents are used instead of the
304                        # temporarily extracted files.
305                        if iteration == 1:
306                            extracted_args.extend(sorted(extracted[base]))
307                        else:
308                            dirname = os.path.dirname(f[len(self.tmpdir)+1:])
309                            if base.endswith('f'):
310                                dirname = os.path.join(dirname, 'foo', 'bar')
311                            extracted_args.extend([self.tmpfile(dirname, Obj(base)), self.tmpfile(dirname, Obj(base + '2'))])
312                    else:
313                        extracted_args.append(f)
314                self.assertRelEqual(args, ['foo', '-bar'] + extracted_args)
315
316                tmp = args.tmp
317            # Check that all temporary files are properly removed
318            self.assertEqual(True, all([not os.path.exists(f) for f in tmp]))
319
320            # Create archives contents next to them for the second iteration.
321            base = os.path.splitext(Lib('_'))[0]
322            self.touch(self.tmpfile(Obj(base.replace('_', suffix))) for suffix in ('a', 'a2', 'd', 'd2'))
323            try:
324                os.makedirs(self.tmpfile('foo', 'bar'))
325            except:
326                pass
327            self.touch(self.tmpfile('foo', 'bar', Obj(base.replace('_', suffix))) for suffix in ('f', 'f2'))
328            self.touch(self.tmpfile('liby', Obj(base.replace('_', suffix))) for suffix in ('z', 'z2'))
329
330        # Restore subprocess.call and subprocess.check_output
331        subprocess.call = subprocess_call
332        subprocess.check_output = subprocess_check_output
333
334class FakeProcess(object):
335    def __init__(self, out, err = ''):
336        self.out = out
337        self.err = err
338
339    def communicate(self):
340        return (self.out, self.err)
341
342OBJDUMPS = {
343'foo.o': '''
34400000000 g     F .text\t00000001 foo
34500000000 g     F .text._Z6foobarv\t00000001 _Z6foobarv
34600000000 g     F .text.hello\t00000001 hello
34700000000 g     F .text._ZThn4_6foobarv\t00000001 _ZThn4_6foobarv
348''',
349'bar.o': '''
35000000000 g     F .text.hi\t00000001 hi
35100000000 g     F .text.hot._Z6barbazv\t00000001 .hidden _Z6barbazv
352''',
353}
354
355PRINT_ICF = '''
356ld: ICF folding section '.text.hello' in file 'foo.o'into '.text.hi' in file 'bar.o'
357ld: ICF folding section '.foo' in file 'foo.o'into '.foo' in file 'bar.o'
358'''
359
360class SubprocessPopen(object):
361    def __init__(self, test):
362        self.test = test
363
364    def __call__(self, args, stdout = None, stderr = None):
365        self.test.assertEqual(stdout, subprocess.PIPE)
366        self.test.assertEqual(stderr, subprocess.PIPE)
367        if args[0] == 'objdump':
368            self.test.assertEqual(args[1], '-t')
369            self.test.assertTrue(args[2] in OBJDUMPS)
370            return FakeProcess(OBJDUMPS[args[2]])
371        else:
372            return FakeProcess('', PRINT_ICF)
373
374class TestSectionFinder(unittest.TestCase):
375    def test_getSections(self):
376        '''Test SectionFinder'''
377        # Divert subprocess.Popen
378        subprocess_popen = subprocess.Popen
379        subprocess.Popen = SubprocessPopen(self)
380        config.EXPAND_LIBS_ORDER_STYLE = 'linkerscript'
381        config.OBJ_SUFFIX = '.o'
382        config.LIB_SUFFIX = '.a'
383        finder = SectionFinder(['foo.o', 'bar.o'])
384        self.assertEqual(finder.getSections('foobar'), [])
385        self.assertEqual(finder.getSections('_Z6barbazv'), ['.text.hot._Z6barbazv'])
386        self.assertEqual(finder.getSections('_Z6foobarv'), ['.text._Z6foobarv', '.text._ZThn4_6foobarv'])
387        self.assertEqual(finder.getSections('_ZThn4_6foobarv'), ['.text._Z6foobarv', '.text._ZThn4_6foobarv'])
388        subprocess.Popen = subprocess_popen
389
390class TestSymbolOrder(unittest.TestCase):
391    def test_getOrderedSections(self):
392        '''Test ExpandMoreArgs' _getOrderedSections'''
393        # Divert subprocess.Popen
394        subprocess_popen = subprocess.Popen
395        subprocess.Popen = SubprocessPopen(self)
396        config.EXPAND_LIBS_ORDER_STYLE = 'linkerscript'
397        config.OBJ_SUFFIX = '.o'
398        config.LIB_SUFFIX = '.a'
399        config.LD_PRINT_ICF_SECTIONS = ''
400        args = ExpandArgsMore(['foo', '-bar', 'bar.o', 'foo.o'])
401        self.assertEqual(args._getOrderedSections(['_Z6foobarv', '_Z6barbazv']), ['.text._Z6foobarv', '.text._ZThn4_6foobarv', '.text.hot._Z6barbazv'])
402        self.assertEqual(args._getOrderedSections(['_ZThn4_6foobarv', '_Z6barbazv']), ['.text._Z6foobarv', '.text._ZThn4_6foobarv', '.text.hot._Z6barbazv'])
403        subprocess.Popen = subprocess_popen
404
405    def test_getFoldedSections(self):
406        '''Test ExpandMoreArgs' _getFoldedSections'''
407        # Divert subprocess.Popen
408        subprocess_popen = subprocess.Popen
409        subprocess.Popen = SubprocessPopen(self)
410        config.LD_PRINT_ICF_SECTIONS = '-Wl,--print-icf-sections'
411        args = ExpandArgsMore(['foo', '-bar', 'bar.o', 'foo.o'])
412        self.assertEqual(args._getFoldedSections(), {'.text.hello': ['.text.hi'], '.text.hi': ['.text.hello']})
413        subprocess.Popen = subprocess_popen
414
415    def test_getOrderedSectionsWithICF(self):
416        '''Test ExpandMoreArgs' _getOrderedSections, with ICF'''
417        # Divert subprocess.Popen
418        subprocess_popen = subprocess.Popen
419        subprocess.Popen = SubprocessPopen(self)
420        config.EXPAND_LIBS_ORDER_STYLE = 'linkerscript'
421        config.OBJ_SUFFIX = '.o'
422        config.LIB_SUFFIX = '.a'
423        config.LD_PRINT_ICF_SECTIONS = '-Wl,--print-icf-sections'
424        args = ExpandArgsMore(['foo', '-bar', 'bar.o', 'foo.o'])
425        self.assertEqual(args._getOrderedSections(['hello', '_Z6barbazv']), ['.text.hello', '.text.hi', '.text.hot._Z6barbazv'])
426        self.assertEqual(args._getOrderedSections(['_ZThn4_6foobarv', 'hi', '_Z6barbazv']), ['.text._Z6foobarv', '.text._ZThn4_6foobarv', '.text.hi', '.text.hello', '.text.hot._Z6barbazv'])
427        subprocess.Popen = subprocess_popen
428
429
430if __name__ == '__main__':
431    mozunit.main()
432