PageRenderTime 37ms CodeModel.GetById 6ms RepoModel.GetById 1ms app.codeStats 0ms

/Lib/packaging/tests/test_install.py

https://bitbucket.org/mirror/cpython/
Python | 391 lines | 297 code | 53 blank | 41 comment | 48 complexity | c94921aeae696cdf27239cd079c6e628 MD5 | raw file
Possible License(s): Unlicense, 0BSD, BSD-3-Clause
  1. """Tests for the packaging.install module."""
  2. import os
  3. import logging
  4. from tempfile import mkstemp
  5. from sysconfig import is_python_build
  6. from packaging import install
  7. from packaging.pypi.xmlrpc import Client
  8. from packaging.metadata import Metadata
  9. from packaging.tests.support import (LoggingCatcher, TempdirManager, unittest,
  10. fake_dec)
  11. try:
  12. import threading
  13. from packaging.tests.pypi_server import use_xmlrpc_server
  14. except ImportError:
  15. threading = None
  16. use_xmlrpc_server = fake_dec
  17. class InstalledDist:
  18. """Distribution object, represent distributions currently installed on the
  19. system"""
  20. def __init__(self, name, version, deps):
  21. self.metadata = Metadata()
  22. self.name = name
  23. self.version = version
  24. self.metadata['Name'] = name
  25. self.metadata['Version'] = version
  26. self.metadata['Requires-Dist'] = deps
  27. def __repr__(self):
  28. return '<InstalledDist %r>' % self.metadata['Name']
  29. class ToInstallDist:
  30. """Distribution that will be installed"""
  31. def __init__(self, files=False):
  32. self._files = files
  33. self.install_called = False
  34. self.install_called_with = {}
  35. self.uninstall_called = False
  36. self._real_files = []
  37. self.name = "fake"
  38. self.version = "fake"
  39. if files:
  40. for f in range(0, 3):
  41. fp, fn = mkstemp()
  42. os.close(fp)
  43. self._real_files.append(fn)
  44. def _unlink_installed_files(self):
  45. if self._files:
  46. for fn in self._real_files:
  47. os.unlink(fn)
  48. def list_installed_files(self, **args):
  49. if self._files:
  50. return self._real_files
  51. def get_install(self, **args):
  52. return self.list_installed_files()
  53. class MagicMock:
  54. def __init__(self, return_value=None, raise_exception=False):
  55. self.called = False
  56. self._times_called = 0
  57. self._called_with = []
  58. self._return_value = return_value
  59. self._raise = raise_exception
  60. def __call__(self, *args, **kwargs):
  61. self.called = True
  62. self._times_called = self._times_called + 1
  63. self._called_with.append((args, kwargs))
  64. iterable = hasattr(self._raise, '__iter__')
  65. if self._raise:
  66. if ((not iterable and self._raise)
  67. or self._raise[self._times_called - 1]):
  68. raise Exception
  69. return self._return_value
  70. def called_with(self, *args, **kwargs):
  71. return (args, kwargs) in self._called_with
  72. def get_installed_dists(dists):
  73. """Return a list of fake installed dists.
  74. The list is name, version, deps"""
  75. objects = []
  76. for name, version, deps in dists:
  77. objects.append(InstalledDist(name, version, deps))
  78. return objects
  79. class TestInstall(LoggingCatcher, TempdirManager, unittest.TestCase):
  80. def _get_client(self, server, *args, **kwargs):
  81. return Client(server.full_address, *args, **kwargs)
  82. def _get_results(self, output):
  83. """return a list of results"""
  84. installed = [(o.name, str(o.version)) for o in output['install']]
  85. remove = [(o.name, str(o.version)) for o in output['remove']]
  86. conflict = [(o.name, str(o.version)) for o in output['conflict']]
  87. return installed, remove, conflict
  88. @unittest.skipIf(threading is None, 'needs threading')
  89. @use_xmlrpc_server()
  90. def test_existing_deps(self, server):
  91. # Test that the installer get the dependencies from the metadatas
  92. # and ask the index for this dependencies.
  93. # In this test case, we have choxie that is dependent from towel-stuff
  94. # 0.1, which is in-turn dependent on bacon <= 0.2:
  95. # choxie -> towel-stuff -> bacon.
  96. # Each release metadata is not provided in metadata 1.2.
  97. client = self._get_client(server)
  98. archive_path = '%s/distribution.tar.gz' % server.full_address
  99. server.xmlrpc.set_distributions([
  100. {'name': 'choxie',
  101. 'version': '2.0.0.9',
  102. 'requires_dist': ['towel-stuff (0.1)'],
  103. 'url': archive_path},
  104. {'name': 'towel-stuff',
  105. 'version': '0.1',
  106. 'requires_dist': ['bacon (<= 0.2)'],
  107. 'url': archive_path},
  108. {'name': 'bacon',
  109. 'version': '0.1',
  110. 'requires_dist': [],
  111. 'url': archive_path},
  112. ])
  113. installed = get_installed_dists([('bacon', '0.1', [])])
  114. output = install.get_infos("choxie", index=client,
  115. installed=installed)
  116. # we don't have installed bacon as it's already installed system-wide
  117. self.assertEqual(0, len(output['remove']))
  118. self.assertEqual(2, len(output['install']))
  119. readable_output = [(o.name, str(o.version))
  120. for o in output['install']]
  121. self.assertIn(('towel-stuff', '0.1'), readable_output)
  122. self.assertIn(('choxie', '2.0.0.9'), readable_output)
  123. @unittest.skipIf(threading is None, 'needs threading')
  124. @use_xmlrpc_server()
  125. def test_upgrade_existing_deps(self, server):
  126. client = self._get_client(server)
  127. archive_path = '%s/distribution.tar.gz' % server.full_address
  128. server.xmlrpc.set_distributions([
  129. {'name': 'choxie',
  130. 'version': '2.0.0.9',
  131. 'requires_dist': ['towel-stuff (0.1)'],
  132. 'url': archive_path},
  133. {'name': 'towel-stuff',
  134. 'version': '0.1',
  135. 'requires_dist': ['bacon (>= 0.2)'],
  136. 'url': archive_path},
  137. {'name': 'bacon',
  138. 'version': '0.2',
  139. 'requires_dist': [],
  140. 'url': archive_path},
  141. ])
  142. output = install.get_infos("choxie", index=client,
  143. installed=get_installed_dists([('bacon', '0.1', [])]))
  144. installed = [(o.name, str(o.version)) for o in output['install']]
  145. # we need bacon 0.2, but 0.1 is installed.
  146. # So we expect to remove 0.1 and to install 0.2 instead.
  147. remove = [(o.name, str(o.version)) for o in output['remove']]
  148. self.assertIn(('choxie', '2.0.0.9'), installed)
  149. self.assertIn(('towel-stuff', '0.1'), installed)
  150. self.assertIn(('bacon', '0.2'), installed)
  151. self.assertIn(('bacon', '0.1'), remove)
  152. self.assertEqual(0, len(output['conflict']))
  153. @unittest.skipIf(threading is None, 'needs threading')
  154. @use_xmlrpc_server()
  155. def test_conflicts(self, server):
  156. # Tests that conflicts are detected
  157. client = self._get_client(server)
  158. archive_path = '%s/distribution.tar.gz' % server.full_address
  159. # choxie depends on towel-stuff, which depends on bacon.
  160. server.xmlrpc.set_distributions([
  161. {'name': 'choxie',
  162. 'version': '2.0.0.9',
  163. 'requires_dist': ['towel-stuff (0.1)'],
  164. 'url': archive_path},
  165. {'name': 'towel-stuff',
  166. 'version': '0.1',
  167. 'requires_dist': ['bacon (>= 0.2)'],
  168. 'url': archive_path},
  169. {'name': 'bacon',
  170. 'version': '0.2',
  171. 'requires_dist': [],
  172. 'url': archive_path},
  173. ])
  174. # name, version, deps.
  175. already_installed = [('bacon', '0.1', []),
  176. ('chicken', '1.1', ['bacon (0.1)'])]
  177. output = install.get_infos(
  178. 'choxie', index=client,
  179. installed=get_installed_dists(already_installed))
  180. # we need bacon 0.2, but 0.1 is installed.
  181. # So we expect to remove 0.1 and to install 0.2 instead.
  182. installed, remove, conflict = self._get_results(output)
  183. self.assertIn(('choxie', '2.0.0.9'), installed)
  184. self.assertIn(('towel-stuff', '0.1'), installed)
  185. self.assertIn(('bacon', '0.2'), installed)
  186. self.assertIn(('bacon', '0.1'), remove)
  187. self.assertIn(('chicken', '1.1'), conflict)
  188. @unittest.skipIf(threading is None, 'needs threading')
  189. @use_xmlrpc_server()
  190. def test_installation_unexisting_project(self, server):
  191. # Test that the isntalled raises an exception if the project does not
  192. # exists.
  193. client = self._get_client(server)
  194. self.assertRaises(install.InstallationException,
  195. install.get_infos,
  196. 'unexisting project', index=client)
  197. def test_move_files(self):
  198. # test that the files are really moved, and that the new path is
  199. # returned.
  200. path = self.mkdtemp()
  201. newpath = self.mkdtemp()
  202. files = [os.path.join(path, str(x)) for x in range(1, 20)]
  203. for f in files:
  204. open(f, 'ab+').close()
  205. output = [o for o in install._move_files(files, newpath)]
  206. # check that output return the list of old/new places
  207. for file_ in files:
  208. name = os.path.split(file_)[-1]
  209. newloc = os.path.join(newpath, name)
  210. self.assertIn((file_, newloc), output)
  211. # remove the files
  212. for f in [o[1] for o in output]: # o[1] is the new place
  213. os.remove(f)
  214. def test_update_infos(self):
  215. tests = [[
  216. {'foo': ['foobar', 'foo', 'baz'], 'baz': ['foo', 'foo']},
  217. {'foo': ['additional_content', 'yeah'], 'baz': ['test', 'foo']},
  218. {'foo': ['foobar', 'foo', 'baz', 'additional_content', 'yeah'],
  219. 'baz': ['foo', 'foo', 'test', 'foo']},
  220. ]]
  221. for dict1, dict2, expect in tests:
  222. install._update_infos(dict1, dict2)
  223. for key in expect:
  224. self.assertEqual(expect[key], dict1[key])
  225. def test_install_dists_rollback(self):
  226. # if one of the distribution installation fails, call uninstall on all
  227. # installed distributions.
  228. old_install_dist = install._install_dist
  229. old_uninstall = getattr(install, 'uninstall', None)
  230. install._install_dist = MagicMock(return_value=[],
  231. raise_exception=(False, True))
  232. install.remove = MagicMock()
  233. try:
  234. d1 = ToInstallDist()
  235. d2 = ToInstallDist()
  236. path = self.mkdtemp()
  237. self.assertRaises(Exception, install.install_dists, [d1, d2], path)
  238. self.assertTrue(install._install_dist.called_with(d1, path))
  239. self.assertTrue(install.remove.called)
  240. finally:
  241. install._install_dist = old_install_dist
  242. install.remove = old_uninstall
  243. def test_install_dists_success(self):
  244. old_install_dist = install._install_dist
  245. install._install_dist = MagicMock(return_value=[])
  246. try:
  247. # test that the install method is called on each distributions
  248. d1 = ToInstallDist()
  249. d2 = ToInstallDist()
  250. # should call install
  251. path = self.mkdtemp()
  252. install.install_dists([d1, d2], path)
  253. for dist in (d1, d2):
  254. self.assertTrue(install._install_dist.called_with(dist, path))
  255. finally:
  256. install._install_dist = old_install_dist
  257. def test_install_from_infos_conflict(self):
  258. # assert conflicts raise an exception
  259. self.assertRaises(install.InstallationConflict,
  260. install.install_from_infos,
  261. conflicts=[ToInstallDist()])
  262. def test_install_from_infos_remove_success(self):
  263. old_install_dists = install.install_dists
  264. install.install_dists = lambda x, y=None: None
  265. try:
  266. dists = []
  267. for i in range(2):
  268. dists.append(ToInstallDist(files=True))
  269. install.install_from_infos(remove=dists)
  270. # assert that the files have been removed
  271. for dist in dists:
  272. for f in dist.list_installed_files():
  273. self.assertFalse(os.path.exists(f))
  274. finally:
  275. install.install_dists = old_install_dists
  276. def test_install_from_infos_remove_rollback(self):
  277. old_install_dist = install._install_dist
  278. old_uninstall = getattr(install, 'uninstall', None)
  279. install._install_dist = MagicMock(return_value=[],
  280. raise_exception=(False, True))
  281. install.uninstall = MagicMock()
  282. try:
  283. # assert that if an error occurs, the removed files are restored.
  284. remove = []
  285. for i in range(2):
  286. remove.append(ToInstallDist(files=True))
  287. to_install = [ToInstallDist(), ToInstallDist()]
  288. temp_dir = self.mkdtemp()
  289. self.assertRaises(Exception, install.install_from_infos,
  290. install_path=temp_dir, install=to_install,
  291. remove=remove)
  292. # assert that the files are in the same place
  293. # assert that the files have been removed
  294. for dist in remove:
  295. for f in dist.list_installed_files():
  296. self.assertTrue(os.path.exists(f))
  297. dist._unlink_installed_files()
  298. finally:
  299. install._install_dist = old_install_dist
  300. install.uninstall = old_uninstall
  301. def test_install_from_infos_install_succes(self):
  302. old_install_dist = install._install_dist
  303. install._install_dist = MagicMock([])
  304. try:
  305. # assert that the distribution can be installed
  306. install_path = "my_install_path"
  307. to_install = [ToInstallDist(), ToInstallDist()]
  308. install.install_from_infos(install_path, install=to_install)
  309. for dist in to_install:
  310. install._install_dist.called_with(install_path)
  311. finally:
  312. install._install_dist = old_install_dist
  313. def test_install_permission_denied(self):
  314. # if we don't have access to the installation path, we should abort
  315. # immediately
  316. project = os.path.join(os.path.dirname(__file__), 'package.tgz')
  317. # when running from an uninstalled build, a warning is emitted and the
  318. # installation is not attempted
  319. if is_python_build():
  320. self.assertFalse(install.install(project))
  321. self.assertEqual(1, len(self.get_logs(logging.ERROR)))
  322. return
  323. install_path = self.mkdtemp()
  324. old_get_path = install.get_path
  325. install.get_path = lambda path: install_path
  326. old_mod = os.stat(install_path).st_mode
  327. os.chmod(install_path, 0)
  328. try:
  329. self.assertFalse(install.install(project))
  330. finally:
  331. os.chmod(install_path, old_mod)
  332. install.get_path = old_get_path
  333. def test_suite():
  334. suite = unittest.TestSuite()
  335. suite.addTest(unittest.makeSuite(TestInstall))
  336. return suite
  337. if __name__ == '__main__':
  338. unittest.main(defaultTest='test_suite')