test_packaging.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939
  1. # Copyright (c) 2013 New Dream Network, LLC (DreamHost)
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  12. # implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # Copyright (C) 2013 Association of Universities for Research in Astronomy
  17. # (AURA)
  18. #
  19. # Redistribution and use in source and binary forms, with or without
  20. # modification, are permitted provided that the following conditions are met:
  21. #
  22. # 1. Redistributions of source code must retain the above copyright
  23. # notice, this list of conditions and the following disclaimer.
  24. #
  25. # 2. Redistributions in binary form must reproduce the above
  26. # copyright notice, this list of conditions and the following
  27. # disclaimer in the documentation and/or other materials provided
  28. # with the distribution.
  29. #
  30. # 3. The name of AURA and its representatives may not be used to
  31. # endorse or promote products derived from this software without
  32. # specific prior written permission.
  33. #
  34. # THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED
  35. # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  36. # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  37. # DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT,
  38. # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  39. # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
  40. import email
  41. import email.errors
  42. import imp
  43. import os
  44. import re
  45. import sys
  46. import sysconfig
  47. import tempfile
  48. import textwrap
  49. import fixtures
  50. import mock
  51. import pkg_resources
  52. import six
  53. import testscenarios
  54. import testtools
  55. from testtools import matchers
  56. import virtualenv
  57. import wheel.install
  58. from pbr import git
  59. from pbr import packaging
  60. from pbr.tests import base
  61. PBR_ROOT = os.path.abspath(os.path.join(__file__, '..', '..', '..'))
  62. class TestRepo(fixtures.Fixture):
  63. """A git repo for testing with.
  64. Use of TempHomeDir with this fixture is strongly recommended as due to the
  65. lack of config --local in older gits, it will write to the users global
  66. configuration without TempHomeDir.
  67. """
  68. def __init__(self, basedir):
  69. super(TestRepo, self).__init__()
  70. self._basedir = basedir
  71. def setUp(self):
  72. super(TestRepo, self).setUp()
  73. base._run_cmd(['git', 'init', '.'], self._basedir)
  74. base._config_git()
  75. base._run_cmd(['git', 'add', '.'], self._basedir)
  76. def commit(self, message_content='test commit'):
  77. files = len(os.listdir(self._basedir))
  78. path = self._basedir + '/%d' % files
  79. open(path, 'wt').close()
  80. base._run_cmd(['git', 'add', path], self._basedir)
  81. base._run_cmd(['git', 'commit', '-m', message_content], self._basedir)
  82. def uncommit(self):
  83. base._run_cmd(['git', 'reset', '--hard', 'HEAD^'], self._basedir)
  84. def tag(self, version):
  85. base._run_cmd(
  86. ['git', 'tag', '-sm', 'test tag', version], self._basedir)
  87. class GPGKeyFixture(fixtures.Fixture):
  88. """Creates a GPG key for testing.
  89. It's recommended that this be used in concert with a unique home
  90. directory.
  91. """
  92. def setUp(self):
  93. super(GPGKeyFixture, self).setUp()
  94. tempdir = self.useFixture(fixtures.TempDir())
  95. gnupg_version_re = re.compile('^gpg\s.*\s([\d+])\.([\d+])\.([\d+])')
  96. gnupg_version = base._run_cmd(['gpg', '--version'], tempdir.path)
  97. for line in gnupg_version[0].split('\n'):
  98. gnupg_version = gnupg_version_re.match(line)
  99. if gnupg_version:
  100. gnupg_version = (int(gnupg_version.group(1)),
  101. int(gnupg_version.group(2)),
  102. int(gnupg_version.group(3)))
  103. break
  104. else:
  105. if gnupg_version is None:
  106. gnupg_version = (0, 0, 0)
  107. config_file = tempdir.path + '/key-config'
  108. f = open(config_file, 'wt')
  109. try:
  110. if gnupg_version[0] == 2 and gnupg_version[1] >= 1:
  111. f.write("""
  112. %no-protection
  113. %transient-key
  114. """)
  115. f.write("""
  116. %no-ask-passphrase
  117. Key-Type: RSA
  118. Name-Real: Example Key
  119. Name-Comment: N/A
  120. Name-Email: example@example.com
  121. Expire-Date: 2d
  122. Preferences: (setpref)
  123. %commit
  124. """)
  125. finally:
  126. f.close()
  127. # Note that --quick-random (--debug-quick-random in GnuPG 2.x)
  128. # does not have a corresponding preferences file setting and
  129. # must be passed explicitly on the command line instead
  130. if gnupg_version[0] == 1:
  131. gnupg_random = '--quick-random'
  132. elif gnupg_version[0] >= 2:
  133. gnupg_random = '--debug-quick-random'
  134. else:
  135. gnupg_random = ''
  136. base._run_cmd(
  137. ['gpg', '--gen-key', '--batch', gnupg_random, config_file],
  138. tempdir.path)
  139. class Venv(fixtures.Fixture):
  140. """Create a virtual environment for testing with.
  141. :attr path: The path to the environment root.
  142. :attr python: The path to the python binary in the environment.
  143. """
  144. def __init__(self, reason, modules=(), pip_cmd=None):
  145. """Create a Venv fixture.
  146. :param reason: A human readable string to bake into the venv
  147. file path to aid diagnostics in the case of failures.
  148. :param modules: A list of modules to install, defaults to latest
  149. pip, wheel, and the working copy of PBR.
  150. :attr pip_cmd: A list to override the default pip_cmd passed to
  151. python for installing base packages.
  152. """
  153. self._reason = reason
  154. if modules == ():
  155. pbr = 'file://%s#egg=pbr' % PBR_ROOT
  156. modules = ['pip', 'wheel', pbr]
  157. self.modules = modules
  158. if pip_cmd is None:
  159. self.pip_cmd = ['-m', 'pip', 'install']
  160. else:
  161. self.pip_cmd = pip_cmd
  162. def _setUp(self):
  163. path = self.useFixture(fixtures.TempDir()).path
  164. virtualenv.create_environment(path, clear=True)
  165. python = os.path.join(path, 'bin', 'python')
  166. command = [python] + self.pip_cmd + ['-U']
  167. if self.modules and len(self.modules) > 0:
  168. command.extend(self.modules)
  169. self.useFixture(base.CapturedSubprocess(
  170. 'mkvenv-' + self._reason, command))
  171. self.addCleanup(delattr, self, 'path')
  172. self.addCleanup(delattr, self, 'python')
  173. self.path = path
  174. self.python = python
  175. return path, python
  176. class CreatePackages(fixtures.Fixture):
  177. """Creates packages from dict with defaults
  178. :param package_dirs: A dict of package name to directory strings
  179. {'pkg_a': '/tmp/path/to/tmp/pkg_a', 'pkg_b': '/tmp/path/to/tmp/pkg_b'}
  180. """
  181. defaults = {
  182. 'setup.py': textwrap.dedent(six.u("""\
  183. #!/usr/bin/env python
  184. import setuptools
  185. setuptools.setup(
  186. setup_requires=['pbr'],
  187. pbr=True,
  188. )
  189. """)),
  190. 'setup.cfg': textwrap.dedent(six.u("""\
  191. [metadata]
  192. name = {pkg_name}
  193. """))
  194. }
  195. def __init__(self, packages):
  196. """Creates packages from dict with defaults
  197. :param packages: a dict where the keys are the package name and a
  198. value that is a second dict that may be empty, containing keys of
  199. filenames and a string value of the contents.
  200. {'package-a': {'requirements.txt': 'string', 'setup.cfg': 'string'}
  201. """
  202. self.packages = packages
  203. def _writeFile(self, directory, file_name, contents):
  204. path = os.path.abspath(os.path.join(directory, file_name))
  205. path_dir = os.path.dirname(path)
  206. if not os.path.exists(path_dir):
  207. if path_dir.startswith(directory):
  208. os.makedirs(path_dir)
  209. else:
  210. raise ValueError
  211. with open(path, 'wt') as f:
  212. f.write(contents)
  213. def _setUp(self):
  214. tmpdir = self.useFixture(fixtures.TempDir()).path
  215. package_dirs = {}
  216. for pkg_name in self.packages:
  217. pkg_path = os.path.join(tmpdir, pkg_name)
  218. package_dirs[pkg_name] = pkg_path
  219. os.mkdir(pkg_path)
  220. for cf in ['setup.py', 'setup.cfg']:
  221. if cf in self.packages[pkg_name]:
  222. contents = self.packages[pkg_name].pop(cf)
  223. else:
  224. contents = self.defaults[cf].format(pkg_name=pkg_name)
  225. self._writeFile(pkg_path, cf, contents)
  226. for cf in self.packages[pkg_name]:
  227. self._writeFile(pkg_path, cf, self.packages[pkg_name][cf])
  228. self.useFixture(TestRepo(pkg_path)).commit()
  229. self.addCleanup(delattr, self, 'package_dirs')
  230. self.package_dirs = package_dirs
  231. return package_dirs
  232. class TestPackagingInGitRepoWithCommit(base.BaseTestCase):
  233. scenarios = [
  234. ('preversioned', dict(preversioned=True)),
  235. ('postversioned', dict(preversioned=False)),
  236. ]
  237. def setUp(self):
  238. super(TestPackagingInGitRepoWithCommit, self).setUp()
  239. self.repo = self.useFixture(TestRepo(self.package_dir))
  240. self.repo.commit()
  241. def test_authors(self):
  242. self.run_setup('sdist', allow_fail=False)
  243. # One commit, something should be in the authors list
  244. with open(os.path.join(self.package_dir, 'AUTHORS'), 'r') as f:
  245. body = f.read()
  246. self.assertNotEqual(body, '')
  247. def test_changelog(self):
  248. self.run_setup('sdist', allow_fail=False)
  249. with open(os.path.join(self.package_dir, 'ChangeLog'), 'r') as f:
  250. body = f.read()
  251. # One commit, something should be in the ChangeLog list
  252. self.assertNotEqual(body, '')
  253. def test_changelog_handles_astrisk(self):
  254. self.repo.commit(message_content="Allow *.openstack.org to work")
  255. self.run_setup('sdist', allow_fail=False)
  256. with open(os.path.join(self.package_dir, 'ChangeLog'), 'r') as f:
  257. body = f.read()
  258. self.assertIn('\*', body)
  259. def test_changelog_handles_dead_links_in_commit(self):
  260. self.repo.commit(message_content="See os_ for to_do about qemu_.")
  261. self.run_setup('sdist', allow_fail=False)
  262. with open(os.path.join(self.package_dir, 'ChangeLog'), 'r') as f:
  263. body = f.read()
  264. self.assertIn('os\_', body)
  265. self.assertIn('to\_do', body)
  266. self.assertIn('qemu\_', body)
  267. def test_changelog_handles_backticks(self):
  268. self.repo.commit(message_content="Allow `openstack.org` to `work")
  269. self.run_setup('sdist', allow_fail=False)
  270. with open(os.path.join(self.package_dir, 'ChangeLog'), 'r') as f:
  271. body = f.read()
  272. self.assertIn('\`', body)
  273. def test_manifest_exclude_honoured(self):
  274. self.run_setup('sdist', allow_fail=False)
  275. with open(os.path.join(
  276. self.package_dir,
  277. 'pbr_testpackage.egg-info/SOURCES.txt'), 'r') as f:
  278. body = f.read()
  279. self.assertThat(
  280. body, matchers.Not(matchers.Contains('pbr_testpackage/extra.py')))
  281. self.assertThat(body, matchers.Contains('pbr_testpackage/__init__.py'))
  282. def test_install_writes_changelog(self):
  283. stdout, _, _ = self.run_setup(
  284. 'install', '--root', self.temp_dir + 'installed',
  285. allow_fail=False)
  286. self.expectThat(stdout, matchers.Contains('Generating ChangeLog'))
  287. class TestExtrafileInstallation(base.BaseTestCase):
  288. def test_install_glob(self):
  289. stdout, _, _ = self.run_setup(
  290. 'install', '--root', self.temp_dir + 'installed',
  291. allow_fail=False)
  292. self.expectThat(
  293. stdout, matchers.Contains('copying data_files/a.txt'))
  294. self.expectThat(
  295. stdout, matchers.Contains('copying data_files/b.txt'))
  296. class TestPackagingInGitRepoWithoutCommit(base.BaseTestCase):
  297. def setUp(self):
  298. super(TestPackagingInGitRepoWithoutCommit, self).setUp()
  299. self.useFixture(TestRepo(self.package_dir))
  300. self.run_setup('sdist', allow_fail=False)
  301. def test_authors(self):
  302. # No commits, no authors in list
  303. with open(os.path.join(self.package_dir, 'AUTHORS'), 'r') as f:
  304. body = f.read()
  305. self.assertEqual('\n', body)
  306. def test_changelog(self):
  307. # No commits, nothing should be in the ChangeLog list
  308. with open(os.path.join(self.package_dir, 'ChangeLog'), 'r') as f:
  309. body = f.read()
  310. self.assertEqual('CHANGES\n=======\n\n', body)
  311. class TestPackagingWheels(base.BaseTestCase):
  312. def setUp(self):
  313. super(TestPackagingWheels, self).setUp()
  314. self.useFixture(TestRepo(self.package_dir))
  315. # Build the wheel
  316. self.run_setup('bdist_wheel', allow_fail=False)
  317. # Slowly construct the path to the generated whl
  318. dist_dir = os.path.join(self.package_dir, 'dist')
  319. relative_wheel_filename = os.listdir(dist_dir)[0]
  320. absolute_wheel_filename = os.path.join(
  321. dist_dir, relative_wheel_filename)
  322. wheel_file = wheel.install.WheelFile(absolute_wheel_filename)
  323. wheel_name = wheel_file.parsed_filename.group('namever')
  324. # Create a directory path to unpack the wheel to
  325. self.extracted_wheel_dir = os.path.join(dist_dir, wheel_name)
  326. # Extract the wheel contents to the directory we just created
  327. wheel_file.zipfile.extractall(self.extracted_wheel_dir)
  328. wheel_file.zipfile.close()
  329. def test_data_directory_has_wsgi_scripts(self):
  330. # Build the path to the scripts directory
  331. scripts_dir = os.path.join(
  332. self.extracted_wheel_dir, 'pbr_testpackage-0.0.data/scripts')
  333. self.assertTrue(os.path.exists(scripts_dir))
  334. scripts = os.listdir(scripts_dir)
  335. self.assertIn('pbr_test_wsgi', scripts)
  336. self.assertIn('pbr_test_wsgi_with_class', scripts)
  337. self.assertNotIn('pbr_test_cmd', scripts)
  338. self.assertNotIn('pbr_test_cmd_with_class', scripts)
  339. def test_generates_c_extensions(self):
  340. built_package_dir = os.path.join(
  341. self.extracted_wheel_dir, 'pbr_testpackage')
  342. static_object_filename = 'testext.so'
  343. soabi = get_soabi()
  344. if soabi:
  345. static_object_filename = 'testext.{0}.so'.format(soabi)
  346. static_object_path = os.path.join(
  347. built_package_dir, static_object_filename)
  348. self.assertTrue(os.path.exists(built_package_dir))
  349. self.assertTrue(os.path.exists(static_object_path))
  350. class TestPackagingHelpers(testtools.TestCase):
  351. def test_generate_script(self):
  352. group = 'console_scripts'
  353. entry_point = pkg_resources.EntryPoint(
  354. name='test-ep',
  355. module_name='pbr.packaging',
  356. attrs=('LocalInstallScripts',))
  357. header = '#!/usr/bin/env fake-header\n'
  358. template = ('%(group)s %(module_name)s %(import_target)s '
  359. '%(invoke_target)s')
  360. generated_script = packaging.generate_script(
  361. group, entry_point, header, template)
  362. expected_script = (
  363. '#!/usr/bin/env fake-header\nconsole_scripts pbr.packaging '
  364. 'LocalInstallScripts LocalInstallScripts'
  365. )
  366. self.assertEqual(expected_script, generated_script)
  367. def test_generate_script_validates_expectations(self):
  368. group = 'console_scripts'
  369. entry_point = pkg_resources.EntryPoint(
  370. name='test-ep',
  371. module_name='pbr.packaging')
  372. header = '#!/usr/bin/env fake-header\n'
  373. template = ('%(group)s %(module_name)s %(import_target)s '
  374. '%(invoke_target)s')
  375. self.assertRaises(
  376. ValueError, packaging.generate_script, group, entry_point, header,
  377. template)
  378. entry_point = pkg_resources.EntryPoint(
  379. name='test-ep',
  380. module_name='pbr.packaging',
  381. attrs=('attr1', 'attr2', 'attr3'))
  382. self.assertRaises(
  383. ValueError, packaging.generate_script, group, entry_point, header,
  384. template)
  385. class TestPackagingInPlainDirectory(base.BaseTestCase):
  386. def setUp(self):
  387. super(TestPackagingInPlainDirectory, self).setUp()
  388. def test_authors(self):
  389. self.run_setup('sdist', allow_fail=False)
  390. # Not a git repo, no AUTHORS file created
  391. filename = os.path.join(self.package_dir, 'AUTHORS')
  392. self.assertFalse(os.path.exists(filename))
  393. def test_changelog(self):
  394. self.run_setup('sdist', allow_fail=False)
  395. # Not a git repo, no ChangeLog created
  396. filename = os.path.join(self.package_dir, 'ChangeLog')
  397. self.assertFalse(os.path.exists(filename))
  398. def test_install_no_ChangeLog(self):
  399. stdout, _, _ = self.run_setup(
  400. 'install', '--root', self.temp_dir + 'installed',
  401. allow_fail=False)
  402. self.expectThat(
  403. stdout, matchers.Not(matchers.Contains('Generating ChangeLog')))
  404. class TestPresenceOfGit(base.BaseTestCase):
  405. def testGitIsInstalled(self):
  406. with mock.patch.object(git,
  407. '_run_shell_command') as _command:
  408. _command.return_value = 'git version 1.8.4.1'
  409. self.assertEqual(True, git._git_is_installed())
  410. def testGitIsNotInstalled(self):
  411. with mock.patch.object(git,
  412. '_run_shell_command') as _command:
  413. _command.side_effect = OSError
  414. self.assertEqual(False, git._git_is_installed())
  415. class ParseRequirementsTest(base.BaseTestCase):
  416. def test_empty_requirements(self):
  417. actual = packaging.parse_requirements([])
  418. self.assertEqual([], actual)
  419. def test_default_requirements(self):
  420. """Ensure default files used if no files provided."""
  421. tempdir = tempfile.mkdtemp()
  422. requirements = os.path.join(tempdir, 'requirements.txt')
  423. with open(requirements, 'w') as f:
  424. f.write('pbr')
  425. # the defaults are relative to where pbr is called from so we need to
  426. # override them. This is OK, however, as we want to validate that
  427. # defaults are used - not what those defaults are
  428. with mock.patch.object(packaging, 'REQUIREMENTS_FILES', (
  429. requirements,)):
  430. result = packaging.parse_requirements()
  431. self.assertEqual(['pbr'], result)
  432. def test_override_with_env(self):
  433. """Ensure environment variable used if no files provided."""
  434. _, tmp_file = tempfile.mkstemp(prefix='openstack', suffix='.setup')
  435. with open(tmp_file, 'w') as fh:
  436. fh.write("foo\nbar")
  437. self.useFixture(
  438. fixtures.EnvironmentVariable('PBR_REQUIREMENTS_FILES', tmp_file))
  439. self.assertEqual(['foo', 'bar'],
  440. packaging.parse_requirements())
  441. def test_override_with_env_multiple_files(self):
  442. _, tmp_file = tempfile.mkstemp(prefix='openstack', suffix='.setup')
  443. with open(tmp_file, 'w') as fh:
  444. fh.write("foo\nbar")
  445. self.useFixture(
  446. fixtures.EnvironmentVariable('PBR_REQUIREMENTS_FILES',
  447. "no-such-file," + tmp_file))
  448. self.assertEqual(['foo', 'bar'],
  449. packaging.parse_requirements())
  450. def test_index_present(self):
  451. tempdir = tempfile.mkdtemp()
  452. requirements = os.path.join(tempdir, 'requirements.txt')
  453. with open(requirements, 'w') as f:
  454. f.write('-i https://myindex.local')
  455. f.write(' --index-url https://myindex.local')
  456. f.write(' --extra-index-url https://myindex.local')
  457. result = packaging.parse_requirements([requirements])
  458. self.assertEqual([], result)
  459. def test_nested_requirements(self):
  460. tempdir = tempfile.mkdtemp()
  461. requirements = os.path.join(tempdir, 'requirements.txt')
  462. nested = os.path.join(tempdir, 'nested.txt')
  463. with open(requirements, 'w') as f:
  464. f.write('-r ' + nested)
  465. with open(nested, 'w') as f:
  466. f.write('pbr')
  467. result = packaging.parse_requirements([requirements])
  468. self.assertEqual(['pbr'], result)
  469. @mock.patch('warnings.warn')
  470. def test_python_version(self, mock_warn):
  471. with open("requirements-py%d.txt" % sys.version_info[0],
  472. "w") as fh:
  473. fh.write("# this is a comment\nfoobar\n# and another one\nfoobaz")
  474. self.assertEqual(['foobar', 'foobaz'],
  475. packaging.parse_requirements())
  476. mock_warn.assert_called_once_with(mock.ANY, DeprecationWarning)
  477. @mock.patch('warnings.warn')
  478. def test_python_version_multiple_options(self, mock_warn):
  479. with open("requirements-py1.txt", "w") as fh:
  480. fh.write("thisisatrap")
  481. with open("requirements-py%d.txt" % sys.version_info[0],
  482. "w") as fh:
  483. fh.write("# this is a comment\nfoobar\n# and another one\nfoobaz")
  484. self.assertEqual(['foobar', 'foobaz'],
  485. packaging.parse_requirements())
  486. # even though we have multiple offending files, this should only be
  487. # called once
  488. mock_warn.assert_called_once_with(mock.ANY, DeprecationWarning)
  489. class ParseRequirementsTestScenarios(base.BaseTestCase):
  490. versioned_scenarios = [
  491. ('non-versioned', {'versioned': False, 'expected': ['bar']}),
  492. ('versioned', {'versioned': True, 'expected': ['bar>=1.2.3']})
  493. ]
  494. scenarios = [
  495. ('normal', {'url': "foo\nbar", 'expected': ['foo', 'bar']}),
  496. ('normal_with_comments', {
  497. 'url': "# this is a comment\nfoo\n# and another one\nbar",
  498. 'expected': ['foo', 'bar']}),
  499. ('removes_index_lines', {'url': '-f foobar', 'expected': []}),
  500. ]
  501. scenarios = scenarios + testscenarios.multiply_scenarios([
  502. ('ssh_egg_url', {'url': 'git+ssh://foo.com/zipball#egg=bar'}),
  503. ('git_https_egg_url', {'url': 'git+https://foo.com/zipball#egg=bar'}),
  504. ('http_egg_url', {'url': 'https://foo.com/zipball#egg=bar'}),
  505. ], versioned_scenarios)
  506. scenarios = scenarios + testscenarios.multiply_scenarios(
  507. [
  508. ('git_egg_url',
  509. {'url': 'git://foo.com/zipball#egg=bar', 'name': 'bar'})
  510. ], [
  511. ('non-editable', {'editable': False}),
  512. ('editable', {'editable': True}),
  513. ],
  514. versioned_scenarios)
  515. def test_parse_requirements(self):
  516. tmp_file = tempfile.NamedTemporaryFile()
  517. req_string = self.url
  518. if hasattr(self, 'editable') and self.editable:
  519. req_string = ("-e %s" % req_string)
  520. if hasattr(self, 'versioned') and self.versioned:
  521. req_string = ("%s-1.2.3" % req_string)
  522. with open(tmp_file.name, 'w') as fh:
  523. fh.write(req_string)
  524. self.assertEqual(self.expected,
  525. packaging.parse_requirements([tmp_file.name]))
  526. class ParseDependencyLinksTest(base.BaseTestCase):
  527. def setUp(self):
  528. super(ParseDependencyLinksTest, self).setUp()
  529. _, self.tmp_file = tempfile.mkstemp(prefix="openstack",
  530. suffix=".setup")
  531. def test_parse_dependency_normal(self):
  532. with open(self.tmp_file, "w") as fh:
  533. fh.write("http://test.com\n")
  534. self.assertEqual(
  535. ["http://test.com"],
  536. packaging.parse_dependency_links([self.tmp_file]))
  537. def test_parse_dependency_with_git_egg_url(self):
  538. with open(self.tmp_file, "w") as fh:
  539. fh.write("-e git://foo.com/zipball#egg=bar")
  540. self.assertEqual(
  541. ["git://foo.com/zipball#egg=bar"],
  542. packaging.parse_dependency_links([self.tmp_file]))
  543. class TestVersions(base.BaseTestCase):
  544. scenarios = [
  545. ('preversioned', dict(preversioned=True)),
  546. ('postversioned', dict(preversioned=False)),
  547. ]
  548. def setUp(self):
  549. super(TestVersions, self).setUp()
  550. self.repo = self.useFixture(TestRepo(self.package_dir))
  551. self.useFixture(GPGKeyFixture())
  552. self.useFixture(base.DiveDir(self.package_dir))
  553. def test_email_parsing_errors_are_handled(self):
  554. mocked_open = mock.mock_open()
  555. with mock.patch('pbr.packaging.open', mocked_open):
  556. with mock.patch('email.message_from_file') as message_from_file:
  557. message_from_file.side_effect = [
  558. email.errors.MessageError('Test'),
  559. {'Name': 'pbr_testpackage'}]
  560. version = packaging._get_version_from_pkg_metadata(
  561. 'pbr_testpackage')
  562. self.assertTrue(message_from_file.called)
  563. self.assertIsNone(version)
  564. def test_capitalized_headers(self):
  565. self.repo.commit()
  566. self.repo.tag('1.2.3')
  567. self.repo.commit('Sem-Ver: api-break')
  568. version = packaging._get_version_from_git()
  569. self.assertThat(version, matchers.StartsWith('2.0.0.dev1'))
  570. def test_capitalized_headers_partial(self):
  571. self.repo.commit()
  572. self.repo.tag('1.2.3')
  573. self.repo.commit('Sem-ver: api-break')
  574. version = packaging._get_version_from_git()
  575. self.assertThat(version, matchers.StartsWith('2.0.0.dev1'))
  576. def test_tagged_version_has_tag_version(self):
  577. self.repo.commit()
  578. self.repo.tag('1.2.3')
  579. version = packaging._get_version_from_git('1.2.3')
  580. self.assertEqual('1.2.3', version)
  581. def test_non_canonical_tagged_version_bump(self):
  582. self.repo.commit()
  583. self.repo.tag('1.4')
  584. self.repo.commit('Sem-Ver: api-break')
  585. version = packaging._get_version_from_git()
  586. self.assertThat(version, matchers.StartsWith('2.0.0.dev1'))
  587. def test_untagged_version_has_dev_version_postversion(self):
  588. self.repo.commit()
  589. self.repo.tag('1.2.3')
  590. self.repo.commit()
  591. version = packaging._get_version_from_git()
  592. self.assertThat(version, matchers.StartsWith('1.2.4.dev1'))
  593. def test_untagged_pre_release_has_pre_dev_version_postversion(self):
  594. self.repo.commit()
  595. self.repo.tag('1.2.3.0a1')
  596. self.repo.commit()
  597. version = packaging._get_version_from_git()
  598. self.assertThat(version, matchers.StartsWith('1.2.3.0a2.dev1'))
  599. def test_untagged_version_minor_bump(self):
  600. self.repo.commit()
  601. self.repo.tag('1.2.3')
  602. self.repo.commit('sem-ver: deprecation')
  603. version = packaging._get_version_from_git()
  604. self.assertThat(version, matchers.StartsWith('1.3.0.dev1'))
  605. def test_untagged_version_major_bump(self):
  606. self.repo.commit()
  607. self.repo.tag('1.2.3')
  608. self.repo.commit('sem-ver: api-break')
  609. version = packaging._get_version_from_git()
  610. self.assertThat(version, matchers.StartsWith('2.0.0.dev1'))
  611. def test_untagged_version_has_dev_version_preversion(self):
  612. self.repo.commit()
  613. self.repo.tag('1.2.3')
  614. self.repo.commit()
  615. version = packaging._get_version_from_git('1.2.5')
  616. self.assertThat(version, matchers.StartsWith('1.2.5.dev1'))
  617. def test_untagged_version_after_pre_has_dev_version_preversion(self):
  618. self.repo.commit()
  619. self.repo.tag('1.2.3.0a1')
  620. self.repo.commit()
  621. version = packaging._get_version_from_git('1.2.5')
  622. self.assertThat(version, matchers.StartsWith('1.2.5.dev1'))
  623. def test_untagged_version_after_rc_has_dev_version_preversion(self):
  624. self.repo.commit()
  625. self.repo.tag('1.2.3.0a1')
  626. self.repo.commit()
  627. version = packaging._get_version_from_git('1.2.3')
  628. self.assertThat(version, matchers.StartsWith('1.2.3.0a2.dev1'))
  629. def test_preversion_too_low_simple(self):
  630. # That is, the target version is either already released or not high
  631. # enough for the semver requirements given api breaks etc.
  632. self.repo.commit()
  633. self.repo.tag('1.2.3')
  634. self.repo.commit()
  635. # Note that we can't target 1.2.3 anymore - with 1.2.3 released we
  636. # need to be working on 1.2.4.
  637. err = self.assertRaises(
  638. ValueError, packaging._get_version_from_git, '1.2.3')
  639. self.assertThat(err.args[0], matchers.StartsWith('git history'))
  640. def test_preversion_too_low_semver_headers(self):
  641. # That is, the target version is either already released or not high
  642. # enough for the semver requirements given api breaks etc.
  643. self.repo.commit()
  644. self.repo.tag('1.2.3')
  645. self.repo.commit('sem-ver: feature')
  646. # Note that we can't target 1.2.4, the feature header means we need
  647. # to be working on 1.3.0 or above.
  648. err = self.assertRaises(
  649. ValueError, packaging._get_version_from_git, '1.2.4')
  650. self.assertThat(err.args[0], matchers.StartsWith('git history'))
  651. def test_get_kwargs_corner_cases(self):
  652. # No tags:
  653. git_dir = self.repo._basedir + '/.git'
  654. get_kwargs = lambda tag: packaging._get_increment_kwargs(git_dir, tag)
  655. def _check_combinations(tag):
  656. self.repo.commit()
  657. self.assertEqual(dict(), get_kwargs(tag))
  658. self.repo.commit('sem-ver: bugfix')
  659. self.assertEqual(dict(), get_kwargs(tag))
  660. self.repo.commit('sem-ver: feature')
  661. self.assertEqual(dict(minor=True), get_kwargs(tag))
  662. self.repo.uncommit()
  663. self.repo.commit('sem-ver: deprecation')
  664. self.assertEqual(dict(minor=True), get_kwargs(tag))
  665. self.repo.uncommit()
  666. self.repo.commit('sem-ver: api-break')
  667. self.assertEqual(dict(major=True), get_kwargs(tag))
  668. self.repo.commit('sem-ver: deprecation')
  669. self.assertEqual(dict(major=True, minor=True), get_kwargs(tag))
  670. _check_combinations('')
  671. self.repo.tag('1.2.3')
  672. _check_combinations('1.2.3')
  673. def test_invalid_tag_ignored(self):
  674. # Fix for bug 1356784 - we treated any tag as a version, not just those
  675. # that are valid versions.
  676. self.repo.commit()
  677. self.repo.tag('1')
  678. self.repo.commit()
  679. # when the tree is tagged and its wrong:
  680. self.repo.tag('badver')
  681. version = packaging._get_version_from_git()
  682. self.assertThat(version, matchers.StartsWith('1.0.1.dev1'))
  683. # When the tree isn't tagged, we also fall through.
  684. self.repo.commit()
  685. version = packaging._get_version_from_git()
  686. self.assertThat(version, matchers.StartsWith('1.0.1.dev2'))
  687. # We don't fall through x.y versions
  688. self.repo.commit()
  689. self.repo.tag('1.2')
  690. self.repo.commit()
  691. self.repo.tag('badver2')
  692. version = packaging._get_version_from_git()
  693. self.assertThat(version, matchers.StartsWith('1.2.1.dev1'))
  694. # Or x.y.z versions
  695. self.repo.commit()
  696. self.repo.tag('1.2.3')
  697. self.repo.commit()
  698. self.repo.tag('badver3')
  699. version = packaging._get_version_from_git()
  700. self.assertThat(version, matchers.StartsWith('1.2.4.dev1'))
  701. # Or alpha/beta/pre versions
  702. self.repo.commit()
  703. self.repo.tag('1.2.4.0a1')
  704. self.repo.commit()
  705. self.repo.tag('badver4')
  706. version = packaging._get_version_from_git()
  707. self.assertThat(version, matchers.StartsWith('1.2.4.0a2.dev1'))
  708. # Non-release related tags are ignored.
  709. self.repo.commit()
  710. self.repo.tag('2')
  711. self.repo.commit()
  712. self.repo.tag('non-release-tag/2014.12.16-1')
  713. version = packaging._get_version_from_git()
  714. self.assertThat(version, matchers.StartsWith('2.0.1.dev1'))
  715. def test_valid_tag_honoured(self):
  716. # Fix for bug 1370608 - we converted any target into a 'dev version'
  717. # even if there was a distance of 0 - indicating that we were on the
  718. # tag itself.
  719. self.repo.commit()
  720. self.repo.tag('1.3.0.0a1')
  721. version = packaging._get_version_from_git()
  722. self.assertEqual('1.3.0.0a1', version)
  723. def test_skip_write_git_changelog(self):
  724. # Fix for bug 1467440
  725. self.repo.commit()
  726. self.repo.tag('1.2.3')
  727. os.environ['SKIP_WRITE_GIT_CHANGELOG'] = '1'
  728. version = packaging._get_version_from_git('1.2.3')
  729. self.assertEqual('1.2.3', version)
  730. def tearDown(self):
  731. super(TestVersions, self).tearDown()
  732. os.environ.pop('SKIP_WRITE_GIT_CHANGELOG', None)
  733. class TestRequirementParsing(base.BaseTestCase):
  734. def test_requirement_parsing(self):
  735. pkgs = {
  736. 'test_reqparse':
  737. {
  738. 'requirements.txt': textwrap.dedent("""\
  739. bar
  740. quux<1.0; python_version=='2.6'
  741. requests-aws>=0.1.4 # BSD License (3 clause)
  742. Routes>=1.12.3,!=2.0,!=2.1;python_version=='2.7'
  743. requests-kerberos>=0.6;python_version=='2.7' # MIT
  744. """),
  745. 'setup.cfg': textwrap.dedent("""\
  746. [metadata]
  747. name = test_reqparse
  748. [extras]
  749. test =
  750. foo
  751. baz>3.2 :python_version=='2.7' # MIT
  752. bar>3.3 :python_version=='2.7' # MIT # Apache
  753. """)},
  754. }
  755. pkg_dirs = self.useFixture(CreatePackages(pkgs)).package_dirs
  756. pkg_dir = pkg_dirs['test_reqparse']
  757. # pkg_resources.split_sections uses None as the title of an
  758. # anonymous section instead of the empty string. Weird.
  759. expected_requirements = {
  760. None: ['bar', 'requests-aws>=0.1.4'],
  761. ":(python_version=='2.6')": ['quux<1.0'],
  762. ":(python_version=='2.7')": ['Routes!=2.0,!=2.1,>=1.12.3',
  763. 'requests-kerberos>=0.6'],
  764. 'test': ['foo'],
  765. "test:(python_version=='2.7')": ['baz>3.2', 'bar>3.3']
  766. }
  767. venv = self.useFixture(Venv('reqParse'))
  768. bin_python = venv.python
  769. # Two things are tested by this
  770. # 1) pbr properly parses markers from requiremnts.txt and setup.cfg
  771. # 2) bdist_wheel causes pbr to not evaluate markers
  772. self._run_cmd(bin_python, ('setup.py', 'bdist_wheel'),
  773. allow_fail=False, cwd=pkg_dir)
  774. egg_info = os.path.join(pkg_dir, 'test_reqparse.egg-info')
  775. requires_txt = os.path.join(egg_info, 'requires.txt')
  776. with open(requires_txt, 'rt') as requires:
  777. generated_requirements = dict(
  778. pkg_resources.split_sections(requires))
  779. # NOTE(dhellmann): We have to spell out the comparison because
  780. # the rendering for version specifiers in a range is not
  781. # consistent across versions of setuptools.
  782. for section, expected in expected_requirements.items():
  783. exp_parsed = [
  784. pkg_resources.Requirement.parse(s)
  785. for s in expected
  786. ]
  787. gen_parsed = [
  788. pkg_resources.Requirement.parse(s)
  789. for s in generated_requirements[section]
  790. ]
  791. self.assertEqual(exp_parsed, gen_parsed)
  792. def get_soabi():
  793. soabi = None
  794. try:
  795. soabi = sysconfig.get_config_var('SOABI')
  796. arch = sysconfig.get_config_var('MULTIARCH')
  797. except IOError:
  798. pass
  799. if soabi and arch and 'pypy' in sysconfig.get_scheme_names():
  800. soabi = '%s-%s' % (soabi, arch)
  801. if soabi is None and 'pypy' in sysconfig.get_scheme_names():
  802. # NOTE(sigmavirus24): PyPy only added support for the SOABI config var
  803. # to sysconfig in 2015. That was well after 2.2.1 was published in the
  804. # Ubuntu 14.04 archive.
  805. for suffix, _, _ in imp.get_suffixes():
  806. if suffix.startswith('.pypy') and suffix.endswith('.so'):
  807. soabi = suffix.split('.')[1]
  808. break
  809. return soabi