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