import subprocess import unittest import sys import os import imp from tempfile import mkdtemp from shutil import rmtree import mozunit from UserString import UserString # Create a controlled configuration for use by expandlibs config_win = { 'AR': 'lib', 'AR_EXTRACT': '', 'DLL_PREFIX': '', 'LIB_PREFIX': '', 'OBJ_SUFFIX': '.obj', 'LIB_SUFFIX': '.lib', 'DLL_SUFFIX': '.dll', 'IMPORT_LIB_SUFFIX': '.lib', 'LIBS_DESC_SUFFIX': '.desc', 'EXPAND_LIBS_LIST_STYLE': 'list', } config_unix = { 'AR': 'ar', 'AR_EXTRACT': 'ar -x', 'DLL_PREFIX': 'lib', 'LIB_PREFIX': 'lib', 'OBJ_SUFFIX': '.o', 'LIB_SUFFIX': '.a', 'DLL_SUFFIX': '.so', 'IMPORT_LIB_SUFFIX': '', 'LIBS_DESC_SUFFIX': '.desc', 'EXPAND_LIBS_LIST_STYLE': 'linkerscript', } config = sys.modules['expandlibs_config'] = imp.new_module('expandlibs_config') from expandlibs import LibDescriptor, ExpandArgs, relativize from expandlibs_gen import generate from expandlibs_exec import ExpandArgsMore, SectionFinder def Lib(name): return config.LIB_PREFIX + name + config.LIB_SUFFIX def Obj(name): return name + config.OBJ_SUFFIX def Dll(name): return config.DLL_PREFIX + name + config.DLL_SUFFIX def ImportLib(name): if not len(config.IMPORT_LIB_SUFFIX): return Dll(name) return config.LIB_PREFIX + name + config.IMPORT_LIB_SUFFIX class TestRelativize(unittest.TestCase): def test_relativize(self): '''Test relativize()''' os_path_exists = os.path.exists def exists(path): return True os.path.exists = exists self.assertEqual(relativize(os.path.abspath(os.curdir)), os.curdir) self.assertEqual(relativize(os.path.abspath(os.pardir)), os.pardir) self.assertEqual(relativize(os.path.join(os.curdir, 'a')), 'a') self.assertEqual(relativize(os.path.join(os.path.abspath(os.curdir), 'a')), 'a') # relativize is expected to return the absolute path if it is shorter self.assertEqual(relativize(os.sep), os.sep) os.path.exists = os.path.exists class TestLibDescriptor(unittest.TestCase): def test_serialize(self): '''Test LibDescriptor's serialization''' desc = LibDescriptor() desc[LibDescriptor.KEYS[0]] = ['a', 'b'] self.assertEqual(str(desc), "{0} = a b".format(LibDescriptor.KEYS[0])) desc['unsupported-key'] = ['a'] self.assertEqual(str(desc), "{0} = a b".format(LibDescriptor.KEYS[0])) desc[LibDescriptor.KEYS[1]] = ['c', 'd', 'e'] self.assertEqual(str(desc), "{0} = a b\n{1} = c d e" .format(LibDescriptor.KEYS[0], LibDescriptor.KEYS[1])) desc[LibDescriptor.KEYS[0]] = [] self.assertEqual(str(desc), "{0} = c d e".format(LibDescriptor.KEYS[1])) def test_read(self): '''Test LibDescriptor's initialization''' desc_list = ["# Comment", "{0} = a b".format(LibDescriptor.KEYS[1]), "", # Empty line "foo = bar", # Should be discarded "{0} = c d e".format(LibDescriptor.KEYS[0])] desc = LibDescriptor(desc_list) self.assertEqual(desc[LibDescriptor.KEYS[1]], ['a', 'b']) self.assertEqual(desc[LibDescriptor.KEYS[0]], ['c', 'd', 'e']) self.assertEqual(False, 'foo' in desc) def wrap_method(conf, wrapped_method): '''Wrapper used to call a test with a specific configuration''' def _method(self): for key in conf: setattr(config, key, conf[key]) self.init() try: wrapped_method(self) except: raise finally: self.cleanup() return _method class ReplicateTests(type): '''Replicates tests for unix and windows variants''' def __new__(cls, clsName, bases, dict): for name in [key for key in dict if key.startswith('test_')]: dict[name + '_unix'] = wrap_method(config_unix, dict[name]) dict[name + '_unix'].__doc__ = dict[name].__doc__ + ' (unix)' dict[name + '_win'] = wrap_method(config_win, dict[name]) dict[name + '_win'].__doc__ = dict[name].__doc__ + ' (win)' del dict[name] return type.__new__(cls, clsName, bases, dict) class TestCaseWithTmpDir(unittest.TestCase): __metaclass__ = ReplicateTests def init(self): self.tmpdir = os.path.abspath(mkdtemp(dir=os.curdir)) def cleanup(self): rmtree(self.tmpdir) def touch(self, files): for f in files: open(f, 'w').close() def tmpfile(self, *args): return os.path.join(self.tmpdir, *args) class TestExpandLibsGen(TestCaseWithTmpDir): def test_generate(self): '''Test library descriptor generation''' files = [self.tmpfile(f) for f in [Lib('a'), Obj('b'), Lib('c'), Obj('d'), Obj('e'), Lib('f')]] self.touch(files[:-1]) self.touch([files[-1] + config.LIBS_DESC_SUFFIX]) desc = generate(files) self.assertEqual(desc['OBJS'], [self.tmpfile(Obj(s)) for s in ['b', 'd', 'e']]) self.assertEqual(desc['LIBS'], [self.tmpfile(Lib(s)) for s in ['a', 'c', 'f']]) self.assertRaises(Exception, generate, files + [self.tmpfile(Obj('z'))]) self.assertRaises(Exception, generate, files + [self.tmpfile(Lib('y'))]) class TestExpandInit(TestCaseWithTmpDir): def init(self): ''' Initializes test environment for library expansion tests''' super(TestExpandInit, self).init() # Create 2 fake libraries, each containing 3 objects, and the second # including the first one and another library. os.mkdir(self.tmpfile('libx')) os.mkdir(self.tmpfile('liby')) self.libx_files = [self.tmpfile('libx', Obj(f)) for f in ['g', 'h', 'i']] self.liby_files = [self.tmpfile('liby', Obj(f)) for f in ['j', 'k', 'l']] + [self.tmpfile('liby', Lib('z'))] self.touch(self.libx_files + self.liby_files) with open(self.tmpfile('libx', Lib('x') + config.LIBS_DESC_SUFFIX), 'w') as f: f.write(str(generate(self.libx_files))) with open(self.tmpfile('liby', Lib('y') + config.LIBS_DESC_SUFFIX), 'w') as f: f.write(str(generate(self.liby_files + [self.tmpfile('libx', Lib('x'))]))) # Create various objects and libraries self.arg_files = [self.tmpfile(f) for f in [Lib('a'), Obj('b'), Obj('c'), Lib('d'), Obj('e')]] # We always give library names (LIB_PREFIX/SUFFIX), even for # dynamic/import libraries self.files = self.arg_files + [self.tmpfile(ImportLib('f'))] self.arg_files += [self.tmpfile(Lib('f'))] self.touch(self.files) def assertRelEqual(self, args1, args2): self.assertEqual(args1, [relativize(a) for a in args2]) class TestExpandArgs(TestExpandInit): def test_expand(self): '''Test library expansion''' # Expanding arguments means libraries with a descriptor are expanded # with the descriptor content, and import libraries are used when # a library doesn't exist args = ExpandArgs(['foo', '-bar'] + self.arg_files + [self.tmpfile('liby', Lib('y'))]) self.assertRelEqual(args, ['foo', '-bar'] + self.files + self.liby_files + self.libx_files) # When a library exists at the same time as a descriptor, we still use # the descriptor. self.touch([self.tmpfile('libx', Lib('x'))]) args = ExpandArgs(['foo', '-bar'] + self.arg_files + [self.tmpfile('liby', Lib('y'))]) self.assertRelEqual(args, ['foo', '-bar'] + self.files + self.liby_files + self.libx_files) self.touch([self.tmpfile('liby', Lib('y'))]) args = ExpandArgs(['foo', '-bar'] + self.arg_files + [self.tmpfile('liby', Lib('y'))]) self.assertRelEqual(args, ['foo', '-bar'] + self.files + self.liby_files + self.libx_files) class TestExpandArgsMore(TestExpandInit): def test_makelist(self): '''Test grouping object files in lists''' # ExpandArgsMore does the same as ExpandArgs with ExpandArgsMore(['foo', '-bar'] + self.arg_files + [self.tmpfile('liby', Lib('y'))]) as args: self.assertRelEqual(args, ['foo', '-bar'] + self.files + self.liby_files + self.libx_files) # But also has an extra method replacing object files with a list args.makelist() # self.files has objects at #1, #2, #4 self.assertRelEqual(args[:3], ['foo', '-bar'] + self.files[:1]) self.assertRelEqual(args[4:], [self.files[3]] + self.files[5:] + [self.tmpfile('liby', Lib('z'))]) # Check the list file content objs = [f for f in self.files + self.liby_files + self.libx_files if f.endswith(config.OBJ_SUFFIX)] if config.EXPAND_LIBS_LIST_STYLE == "linkerscript": self.assertNotEqual(args[3][0], '@') filename = args[3] content = ['INPUT("{0}")'.format(relativize(f)) for f in objs] with open(filename, 'r') as f: self.assertEqual([l.strip() for l in f.readlines() if len(l.strip())], content) elif config.EXPAND_LIBS_LIST_STYLE == "list": self.assertEqual(args[3][0], '@') filename = args[3][1:] content = objs with open(filename, 'r') as f: self.assertRelEqual([l.strip() for l in f.readlines() if len(l.strip())], content) tmp = args.tmp # Check that all temporary files are properly removed self.assertEqual(True, all([not os.path.exists(f) for f in tmp])) def test_extract(self): '''Test library extraction''' # Divert subprocess.call subprocess_call = subprocess.call subprocess_check_output = subprocess.check_output def call(args, **kargs): if config.AR == 'lib': self.assertEqual(args[:2], [config.AR, '-NOLOGO']) self.assertTrue(args[2].startswith('-EXTRACT:')) extract = [args[2][len('-EXTRACT:'):]] self.assertTrue(extract) args = args[3:] else: # The command called is always AR_EXTRACT ar_extract = config.AR_EXTRACT.split() self.assertEqual(args[:len(ar_extract)], ar_extract) args = args[len(ar_extract):] # Remaining argument is always one library self.assertEqual(len(args), 1) arg = args[0] self.assertEqual(os.path.splitext(arg)[1], config.LIB_SUFFIX) # Simulate file extraction lib = os.path.splitext(os.path.basename(arg))[0] if config.AR != 'lib': extract = [lib, lib + '2'] extract = [os.path.join(kargs['cwd'], f) for f in extract] if config.AR != 'lib': extract = [Obj(f) for f in extract] if not lib in extracted: extracted[lib] = [] extracted[lib].extend(extract) self.touch(extract) subprocess.call = call def check_output(args, **kargs): # The command called is always AR ar = config.AR self.assertEqual(args[0:3], [ar, '-NOLOGO', '-LIST']) # Remaining argument is always one library self.assertRelEqual([os.path.splitext(arg)[1] for arg in args[3:]], [config.LIB_SUFFIX]) # Simulate LIB -NOLOGO -LIST lib = os.path.splitext(os.path.basename(args[3]))[0] return '%s\n%s\n' % (Obj(lib), Obj(lib + '2')) subprocess.check_output = check_output # ExpandArgsMore does the same as ExpandArgs self.touch([self.tmpfile('liby', Lib('y'))]) for iteration in (1, 2): with ExpandArgsMore(['foo', '-bar'] + self.arg_files + [self.tmpfile('liby', Lib('y'))]) as args: files = self.files + self.liby_files + self.libx_files self.assertRelEqual(args, ['foo', '-bar'] + files) extracted = {} # ExpandArgsMore also has an extra method extracting static libraries # when possible args.extract() # With AR_EXTRACT, it uses the descriptors when there are, and # actually # extracts the remaining libraries extracted_args = [] for f in files: if f.endswith(config.LIB_SUFFIX): base = os.path.splitext(os.path.basename(f))[0] # On the first iteration, we test the behavior of # extracting archives that don't have a copy of their # contents next to them, which is to use the file # extracted from the archive in a temporary directory. # On the second iteration, we test extracting archives # that do have a copy of their contents next to them, # in which case those contents are used instead of the # temporarily extracted files. if iteration == 1: extracted_args.extend(sorted(extracted[base])) else: dirname = os.path.dirname(f[len(self.tmpdir)+1:]) if base.endswith('f'): dirname = os.path.join(dirname, 'foo', 'bar') extracted_args.extend([self.tmpfile(dirname, Obj(base)), self.tmpfile(dirname, Obj(base + '2'))]) else: extracted_args.append(f) self.assertRelEqual(args, ['foo', '-bar'] + extracted_args) tmp = args.tmp # Check that all temporary files are properly removed self.assertEqual(True, all([not os.path.exists(f) for f in tmp])) # Create archives contents next to them for the second iteration. base = os.path.splitext(Lib('_'))[0] self.touch(self.tmpfile(Obj(base.replace('_', suffix))) for suffix in ('a', 'a2', 'd', 'd2')) try: os.makedirs(self.tmpfile('foo', 'bar')) except: pass self.touch(self.tmpfile('foo', 'bar', Obj(base.replace('_', suffix))) for suffix in ('f', 'f2')) self.touch(self.tmpfile('liby', Obj(base.replace('_', suffix))) for suffix in ('z', 'z2')) # Restore subprocess.call and subprocess.check_output subprocess.call = subprocess_call subprocess.check_output = subprocess_check_output class FakeProcess(object): def __init__(self, out, err = ''): self.out = out self.err = err def communicate(self): return (self.out, self.err) OBJDUMPS = { 'foo.o': ''' 00000000 g F .text\t00000001 foo 00000000 g F .text._Z6foobarv\t00000001 _Z6foobarv 00000000 g F .text.hello\t00000001 hello 00000000 g F .text._ZThn4_6foobarv\t00000001 _ZThn4_6foobarv ''', 'bar.o': ''' 00000000 g F .text.hi\t00000001 hi 00000000 g F .text.hot._Z6barbazv\t00000001 .hidden _Z6barbazv ''', } PRINT_ICF = ''' ld: ICF folding section '.text.hello' in file 'foo.o'into '.text.hi' in file 'bar.o' ld: ICF folding section '.foo' in file 'foo.o'into '.foo' in file 'bar.o' ''' class SubprocessPopen(object): def __init__(self, test): self.test = test def __call__(self, args, stdout = None, stderr = None): self.test.assertEqual(stdout, subprocess.PIPE) self.test.assertEqual(stderr, subprocess.PIPE) if args[0] == 'objdump': self.test.assertEqual(args[1], '-t') self.test.assertTrue(args[2] in OBJDUMPS) return FakeProcess(OBJDUMPS[args[2]]) else: return FakeProcess('', PRINT_ICF) class TestSectionFinder(unittest.TestCase): def test_getSections(self): '''Test SectionFinder''' # Divert subprocess.Popen subprocess_popen = subprocess.Popen subprocess.Popen = SubprocessPopen(self) config.EXPAND_LIBS_ORDER_STYLE = 'linkerscript' config.OBJ_SUFFIX = '.o' config.LIB_SUFFIX = '.a' finder = SectionFinder(['foo.o', 'bar.o']) self.assertEqual(finder.getSections('foobar'), []) self.assertEqual(finder.getSections('_Z6barbazv'), ['.text.hot._Z6barbazv']) self.assertEqual(finder.getSections('_Z6foobarv'), ['.text._Z6foobarv', '.text._ZThn4_6foobarv']) self.assertEqual(finder.getSections('_ZThn4_6foobarv'), ['.text._Z6foobarv', '.text._ZThn4_6foobarv']) subprocess.Popen = subprocess_popen class TestSymbolOrder(unittest.TestCase): def test_getOrderedSections(self): '''Test ExpandMoreArgs' _getOrderedSections''' # Divert subprocess.Popen subprocess_popen = subprocess.Popen subprocess.Popen = SubprocessPopen(self) config.EXPAND_LIBS_ORDER_STYLE = 'linkerscript' config.OBJ_SUFFIX = '.o' config.LIB_SUFFIX = '.a' config.LD_PRINT_ICF_SECTIONS = '' args = ExpandArgsMore(['foo', '-bar', 'bar.o', 'foo.o']) self.assertEqual(args._getOrderedSections(['_Z6foobarv', '_Z6barbazv']), ['.text._Z6foobarv', '.text._ZThn4_6foobarv', '.text.hot._Z6barbazv']) self.assertEqual(args._getOrderedSections(['_ZThn4_6foobarv', '_Z6barbazv']), ['.text._Z6foobarv', '.text._ZThn4_6foobarv', '.text.hot._Z6barbazv']) subprocess.Popen = subprocess_popen def test_getFoldedSections(self): '''Test ExpandMoreArgs' _getFoldedSections''' # Divert subprocess.Popen subprocess_popen = subprocess.Popen subprocess.Popen = SubprocessPopen(self) config.LD_PRINT_ICF_SECTIONS = '-Wl,--print-icf-sections' args = ExpandArgsMore(['foo', '-bar', 'bar.o', 'foo.o']) self.assertEqual(args._getFoldedSections(), {'.text.hello': ['.text.hi'], '.text.hi': ['.text.hello']}) subprocess.Popen = subprocess_popen def test_getOrderedSectionsWithICF(self): '''Test ExpandMoreArgs' _getOrderedSections, with ICF''' # Divert subprocess.Popen subprocess_popen = subprocess.Popen subprocess.Popen = SubprocessPopen(self) config.EXPAND_LIBS_ORDER_STYLE = 'linkerscript' config.OBJ_SUFFIX = '.o' config.LIB_SUFFIX = '.a' config.LD_PRINT_ICF_SECTIONS = '-Wl,--print-icf-sections' args = ExpandArgsMore(['foo', '-bar', 'bar.o', 'foo.o']) self.assertEqual(args._getOrderedSections(['hello', '_Z6barbazv']), ['.text.hello', '.text.hi', '.text.hot._Z6barbazv']) self.assertEqual(args._getOrderedSections(['_ZThn4_6foobarv', 'hi', '_Z6barbazv']), ['.text._Z6foobarv', '.text._ZThn4_6foobarv', '.text.hi', '.text.hello', '.text.hot._Z6barbazv']) subprocess.Popen = subprocess_popen if __name__ == '__main__': mozunit.main()