unit-expandlibs.py 19 KB

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