test_pip.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. # -*- coding: utf-8 -*-
  2. '''
  3. tests.integration.modules.pip
  4. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  5. '''
  6. # Import python libs
  7. from __future__ import absolute_import, print_function, unicode_literals
  8. import os
  9. import re
  10. import sys
  11. import pprint
  12. import shutil
  13. import tempfile
  14. # Import Salt Testing libs
  15. from tests.support.case import ModuleCase
  16. from tests.support.unit import skipIf
  17. from tests.support.paths import TMP
  18. # Import salt libs
  19. import salt.utils.files
  20. import salt.utils.path
  21. import salt.utils.platform
  22. from salt.modules.virtualenv_mod import KNOWN_BINARY_NAMES
  23. @skipIf(salt.utils.path.which_bin(KNOWN_BINARY_NAMES) is None, 'virtualenv not installed')
  24. class PipModuleTest(ModuleCase):
  25. def setUp(self):
  26. super(PipModuleTest, self).setUp()
  27. # Restore the environ
  28. def cleanup_environ(environ):
  29. os.environ.clear()
  30. os.environ.update(environ)
  31. self.addCleanup(cleanup_environ, os.environ.copy())
  32. self.venv_test_dir = tempfile.mkdtemp(dir=TMP)
  33. # Remove the venv test directory
  34. self.addCleanup(shutil.rmtree, self.venv_test_dir, ignore_errors=True)
  35. self.venv_dir = os.path.join(self.venv_test_dir, 'venv')
  36. for key in os.environ.copy():
  37. if key.startswith('PIP_'):
  38. os.environ.pop(key)
  39. self.pip_temp = os.path.join(self.venv_test_dir, '.pip-temp')
  40. # Remove the pip-temp directory
  41. self.addCleanup(shutil.rmtree, self.pip_temp, ignore_errors=True)
  42. if not os.path.isdir(self.pip_temp):
  43. os.makedirs(self.pip_temp)
  44. os.environ['PIP_SOURCE_DIR'] = os.environ['PIP_BUILD_DIR'] = ''
  45. for item in ('venv_dir', 'venv_test_dir', 'pip_temp'):
  46. self.addCleanup(delattr, self, item)
  47. def _create_virtualenv(self, path):
  48. '''
  49. The reason why the virtualenv creation is proxied by this function is mostly
  50. because under windows, we can't seem to properly create a virtualenv off of
  51. another virtualenv(we can on linux) and also because, we really don't want to
  52. test virtualenv creation off of another virtualenv, we want a virtualenv created
  53. from the original python.
  54. Also, one windows, we must also point to the virtualenv binary outside the existing
  55. virtualenv because it will fail otherwise
  56. '''
  57. try:
  58. if salt.utils.platform.is_windows():
  59. python = os.path.join(sys.real_prefix, os.path.basename(sys.executable))
  60. else:
  61. python = os.path.join(sys.real_prefix, 'bin', os.path.basename(sys.executable))
  62. # We're running off a virtualenv, and we don't want to create a virtualenv off of
  63. # a virtualenv
  64. kwargs = {'python': python}
  65. except AttributeError:
  66. # We're running off of the system python
  67. kwargs = {}
  68. self.run_function('virtualenv.create', [path], **kwargs)
  69. def _check_download_error(self, ret):
  70. '''
  71. Checks to see if a download error looks transitory
  72. '''
  73. return any(w in ret for w in ['URLError', 'Download error'])
  74. def pip_successful_install(self, target, expect=('irc3-plugins-test', 'pep8',)):
  75. '''
  76. isolate regex for extracting `successful install` message from pip
  77. '''
  78. expect = set(expect)
  79. expect_str = '|'.join(expect)
  80. success = re.search(
  81. r'^.*Successfully installed\s([^\n]+)(?:Clean.*)?',
  82. target,
  83. re.M | re.S)
  84. success_for = re.findall(
  85. r'({0})(?:-(?:[\d\.-]))?'.format(expect_str),
  86. success.groups()[0]
  87. ) if success else []
  88. return expect.issubset(set(success_for))
  89. def test_issue_2087_missing_pip(self):
  90. # Let's create the testing virtualenv
  91. self._create_virtualenv(self.venv_dir)
  92. # Let's remove the pip binary
  93. pip_bin = os.path.join(self.venv_dir, 'bin', 'pip')
  94. site_dir = self.run_function('virtualenv.get_distribution_path', [self.venv_dir, 'pip'])
  95. if salt.utils.platform.is_windows():
  96. pip_bin = os.path.join(self.venv_dir, 'Scripts', 'pip.exe')
  97. site_dir = os.path.join(self.venv_dir, 'lib', 'site-packages')
  98. if not os.path.isfile(pip_bin):
  99. self.skipTest(
  100. 'Failed to find the pip binary to the test virtualenv'
  101. )
  102. os.remove(pip_bin)
  103. # Also remove the pip dir from site-packages
  104. # This is needed now that we're using python -m pip instead of the
  105. # pip binary directly. python -m pip will still work even if the
  106. # pip binary is missing
  107. shutil.rmtree(os.path.join(site_dir, 'pip'))
  108. # Let's run a pip depending functions
  109. for func in ('pip.freeze', 'pip.list'):
  110. ret = self.run_function(func, bin_env=self.venv_dir)
  111. self.assertIn(
  112. 'Command required for \'{0}\' not found: '
  113. 'Could not find a `pip` binary'.format(func),
  114. ret
  115. )
  116. def test_requirements_as_list_of_chains__cwd_set__absolute_file_path(self):
  117. self._create_virtualenv(self.venv_dir)
  118. # Create a requirements file that depends on another one.
  119. req1_filename = os.path.join(self.venv_dir, 'requirements1.txt')
  120. req1b_filename = os.path.join(self.venv_dir, 'requirements1b.txt')
  121. req2_filename = os.path.join(self.venv_dir, 'requirements2.txt')
  122. req2b_filename = os.path.join(self.venv_dir, 'requirements2b.txt')
  123. with salt.utils.files.fopen(req1_filename, 'w') as f:
  124. f.write('-r requirements1b.txt\n')
  125. with salt.utils.files.fopen(req1b_filename, 'w') as f:
  126. f.write('irc3-plugins-test\n')
  127. with salt.utils.files.fopen(req2_filename, 'w') as f:
  128. f.write('-r requirements2b.txt\n')
  129. with salt.utils.files.fopen(req2b_filename, 'w') as f:
  130. f.write('pep8\n')
  131. requirements_list = [req1_filename, req2_filename]
  132. ret = self.run_function(
  133. 'pip.install', requirements=requirements_list,
  134. bin_env=self.venv_dir, cwd=self.venv_dir
  135. )
  136. if not isinstance(ret, dict):
  137. self.fail(
  138. 'The \'pip.install\' command did not return the excepted dictionary. Output:\n{}'.format(ret)
  139. )
  140. try:
  141. self.assertEqual(ret['retcode'], 0)
  142. found = self.pip_successful_install(ret['stdout'])
  143. self.assertTrue(found)
  144. except KeyError as exc:
  145. self.fail(
  146. 'The returned dictionary is missing an expected key. Error: \'{}\'. Dictionary: {}'.format(
  147. exc,
  148. pprint.pformat(ret)
  149. )
  150. )
  151. def test_requirements_as_list_of_chains__cwd_not_set__absolute_file_path(self):
  152. self._create_virtualenv(self.venv_dir)
  153. # Create a requirements file that depends on another one.
  154. req1_filename = os.path.join(self.venv_dir, 'requirements1.txt')
  155. req1b_filename = os.path.join(self.venv_dir, 'requirements1b.txt')
  156. req2_filename = os.path.join(self.venv_dir, 'requirements2.txt')
  157. req2b_filename = os.path.join(self.venv_dir, 'requirements2b.txt')
  158. with salt.utils.files.fopen(req1_filename, 'w') as f:
  159. f.write('-r requirements1b.txt\n')
  160. with salt.utils.files.fopen(req1b_filename, 'w') as f:
  161. f.write('irc3-plugins-test\n')
  162. with salt.utils.files.fopen(req2_filename, 'w') as f:
  163. f.write('-r requirements2b.txt\n')
  164. with salt.utils.files.fopen(req2b_filename, 'w') as f:
  165. f.write('pep8\n')
  166. requirements_list = [req1_filename, req2_filename]
  167. ret = self.run_function(
  168. 'pip.install', requirements=requirements_list, bin_env=self.venv_dir
  169. )
  170. if not isinstance(ret, dict):
  171. self.fail(
  172. 'The \'pip.install\' command did not return the excepted dictionary. Output:\n{}'.format(ret)
  173. )
  174. try:
  175. self.assertEqual(ret['retcode'], 0)
  176. found = self.pip_successful_install(ret['stdout'])
  177. self.assertTrue(found)
  178. except KeyError as exc:
  179. self.fail(
  180. 'The returned dictionary is missing an expected key. Error: \'{}\'. Dictionary: {}'.format(
  181. exc,
  182. pprint.pformat(ret)
  183. )
  184. )
  185. def test_requirements_as_list__absolute_file_path(self):
  186. self._create_virtualenv(self.venv_dir)
  187. req1_filename = os.path.join(self.venv_dir, 'requirements.txt')
  188. req2_filename = os.path.join(self.venv_dir, 'requirements2.txt')
  189. with salt.utils.files.fopen(req1_filename, 'w') as f:
  190. f.write('irc3-plugins-test\n')
  191. with salt.utils.files.fopen(req2_filename, 'w') as f:
  192. f.write('pep8\n')
  193. requirements_list = [req1_filename, req2_filename]
  194. ret = self.run_function(
  195. 'pip.install', requirements=requirements_list, bin_env=self.venv_dir
  196. )
  197. if not isinstance(ret, dict):
  198. self.fail(
  199. 'The \'pip.install\' command did not return the excepted dictionary. Output:\n{}'.format(ret)
  200. )
  201. try:
  202. self.assertEqual(ret['retcode'], 0)
  203. found = self.pip_successful_install(ret['stdout'])
  204. self.assertTrue(found)
  205. except KeyError as exc:
  206. self.fail(
  207. 'The returned dictionary is missing an expected key. Error: \'{}\'. Dictionary: {}'.format(
  208. exc,
  209. pprint.pformat(ret)
  210. )
  211. )
  212. def test_requirements_as_list__non_absolute_file_path(self):
  213. self._create_virtualenv(self.venv_dir)
  214. # Create a requirements file that depends on another one.
  215. req1_filename = 'requirements.txt'
  216. req2_filename = 'requirements2.txt'
  217. req_cwd = self.venv_dir
  218. req1_filepath = os.path.join(req_cwd, req1_filename)
  219. req2_filepath = os.path.join(req_cwd, req2_filename)
  220. with salt.utils.files.fopen(req1_filepath, 'w') as f:
  221. f.write('irc3-plugins-test\n')
  222. with salt.utils.files.fopen(req2_filepath, 'w') as f:
  223. f.write('pep8\n')
  224. requirements_list = [req1_filename, req2_filename]
  225. ret = self.run_function(
  226. 'pip.install', requirements=requirements_list,
  227. bin_env=self.venv_dir, cwd=req_cwd
  228. )
  229. if not isinstance(ret, dict):
  230. self.fail(
  231. 'The \'pip.install\' command did not return the excepted dictionary. Output:\n{}'.format(ret)
  232. )
  233. try:
  234. self.assertEqual(ret['retcode'], 0)
  235. found = self.pip_successful_install(ret['stdout'])
  236. self.assertTrue(found)
  237. except KeyError as exc:
  238. self.fail(
  239. 'The returned dictionary is missing an expected key. Error: \'{}\'. Dictionary: {}'.format(
  240. exc,
  241. pprint.pformat(ret)
  242. )
  243. )
  244. def test_chained_requirements__absolute_file_path(self):
  245. self._create_virtualenv(self.venv_dir)
  246. # Create a requirements file that depends on another one.
  247. req1_filename = os.path.join(self.venv_dir, 'requirements.txt')
  248. req2_filename = os.path.join(self.venv_dir, 'requirements2.txt')
  249. with salt.utils.files.fopen(req1_filename, 'w') as f:
  250. f.write('-r requirements2.txt')
  251. with salt.utils.files.fopen(req2_filename, 'w') as f:
  252. f.write('pep8')
  253. ret = self.run_function(
  254. 'pip.install', requirements=req1_filename, bin_env=self.venv_dir
  255. )
  256. if not isinstance(ret, dict):
  257. self.fail(
  258. 'The \'pip.install\' command did not return the excepted dictionary. Output:\n{}'.format(ret)
  259. )
  260. try:
  261. self.assertEqual(ret['retcode'], 0)
  262. self.assertIn('installed pep8', ret['stdout'])
  263. except KeyError as exc:
  264. self.fail(
  265. 'The returned dictionary is missing an expected key. Error: \'{}\'. Dictionary: {}'.format(
  266. exc,
  267. pprint.pformat(ret)
  268. )
  269. )
  270. def test_chained_requirements__non_absolute_file_path(self):
  271. self._create_virtualenv(self.venv_dir)
  272. # Create a requirements file that depends on another one.
  273. req_basepath = (self.venv_dir)
  274. req1_filename = 'requirements.txt'
  275. req2_filename = 'requirements2.txt'
  276. req1_file = os.path.join(self.venv_dir, req1_filename)
  277. req2_file = os.path.join(self.venv_dir, req2_filename)
  278. with salt.utils.files.fopen(req1_file, 'w') as f:
  279. f.write('-r requirements2.txt')
  280. with salt.utils.files.fopen(req2_file, 'w') as f:
  281. f.write('pep8')
  282. ret = self.run_function(
  283. 'pip.install', requirements=req1_filename, cwd=req_basepath,
  284. bin_env=self.venv_dir
  285. )
  286. if not isinstance(ret, dict):
  287. self.fail(
  288. 'The \'pip.install\' command did not return the excepted dictionary. Output:\n{}'.format(ret)
  289. )
  290. try:
  291. self.assertEqual(ret['retcode'], 0)
  292. self.assertIn('installed pep8', ret['stdout'])
  293. except KeyError as exc:
  294. self.fail(
  295. 'The returned dictionary is missing an expected key. Error: \'{}\'. Dictionary: {}'.format(
  296. exc,
  297. pprint.pformat(ret)
  298. )
  299. )
  300. def test_issue_4805_nested_requirements(self):
  301. self._create_virtualenv(self.venv_dir)
  302. # Create a requirements file that depends on another one.
  303. req1_filename = os.path.join(self.venv_dir, 'requirements.txt')
  304. req2_filename = os.path.join(self.venv_dir, 'requirements2.txt')
  305. with salt.utils.files.fopen(req1_filename, 'w') as f:
  306. f.write('-r requirements2.txt')
  307. with salt.utils.files.fopen(req2_filename, 'w') as f:
  308. f.write('pep8')
  309. ret = self.run_function(
  310. 'pip.install', requirements=req1_filename, bin_env=self.venv_dir, timeout=300)
  311. if not isinstance(ret, dict):
  312. self.fail(
  313. 'The \'pip.install\' command did not return the excepted dictionary. Output:\n{}'.format(ret)
  314. )
  315. try:
  316. if self._check_download_error(ret['stdout']):
  317. self.skipTest('Test skipped due to pip download error')
  318. self.assertEqual(ret['retcode'], 0)
  319. self.assertIn('installed pep8', ret['stdout'])
  320. except KeyError as exc:
  321. self.fail(
  322. 'The returned dictionary is missing an expected key. Error: \'{}\'. Dictionary: {}'.format(
  323. exc,
  324. pprint.pformat(ret)
  325. )
  326. )
  327. def test_pip_uninstall(self):
  328. # Let's create the testing virtualenv
  329. self._create_virtualenv(self.venv_dir)
  330. ret = self.run_function('pip.install', ['pep8'], bin_env=self.venv_dir)
  331. if not isinstance(ret, dict):
  332. self.fail(
  333. 'The \'pip.install\' command did not return the excepted dictionary. Output:\n{}'.format(ret)
  334. )
  335. try:
  336. if self._check_download_error(ret['stdout']):
  337. self.skipTest('Test skipped due to pip download error')
  338. self.assertEqual(ret['retcode'], 0)
  339. self.assertIn('installed pep8', ret['stdout'])
  340. except KeyError as exc:
  341. self.fail(
  342. 'The returned dictionary is missing an expected key. Error: \'{}\'. Dictionary: {}'.format(
  343. exc,
  344. pprint.pformat(ret)
  345. )
  346. )
  347. ret = self.run_function(
  348. 'pip.uninstall', ['pep8'], bin_env=self.venv_dir
  349. )
  350. if not isinstance(ret, dict):
  351. self.fail(
  352. 'The \'pip.uninstall\' command did not return the excepted dictionary. Output:\n{}'.format(ret)
  353. )
  354. try:
  355. self.assertEqual(ret['retcode'], 0)
  356. self.assertIn('uninstalled pep8', ret['stdout'])
  357. except KeyError as exc:
  358. self.fail(
  359. 'The returned dictionary is missing an expected key. Error: \'{}\'. Dictionary: {}'.format(
  360. exc,
  361. pprint.pformat(ret)
  362. )
  363. )
  364. def test_pip_install_upgrade(self):
  365. # Create the testing virtualenv
  366. self._create_virtualenv(self.venv_dir)
  367. ret = self.run_function(
  368. 'pip.install', ['pep8==1.3.4'], bin_env=self.venv_dir
  369. )
  370. if not isinstance(ret, dict):
  371. self.fail(
  372. 'The \'pip.install\' command did not return the excepted dictionary. Output:\n{}'.format(ret)
  373. )
  374. try:
  375. if self._check_download_error(ret['stdout']):
  376. self.skipTest('Test skipped due to pip download error')
  377. self.assertEqual(ret['retcode'], 0)
  378. self.assertIn('installed pep8', ret['stdout'])
  379. except KeyError as exc:
  380. self.fail(
  381. 'The returned dictionary is missing an expected key. Error: \'{}\'. Dictionary: {}'.format(
  382. exc,
  383. pprint.pformat(ret)
  384. )
  385. )
  386. ret = self.run_function(
  387. 'pip.install',
  388. ['pep8'],
  389. bin_env=self.venv_dir,
  390. upgrade=True
  391. )
  392. if not isinstance(ret, dict):
  393. self.fail(
  394. 'The \'pip.install\' command did not return the excepted dictionary. Output:\n{}'.format(ret)
  395. )
  396. try:
  397. if self._check_download_error(ret['stdout']):
  398. self.skipTest('Test skipped due to pip download error')
  399. self.assertEqual(ret['retcode'], 0)
  400. self.assertIn('installed pep8', ret['stdout'])
  401. except KeyError as exc:
  402. self.fail(
  403. 'The returned dictionary is missing an expected key. Error: \'{}\'. Dictionary: {}'.format(
  404. exc,
  405. pprint.pformat(ret)
  406. )
  407. )
  408. ret = self.run_function(
  409. 'pip.uninstall', ['pep8'], bin_env=self.venv_dir
  410. )
  411. if not isinstance(ret, dict):
  412. self.fail(
  413. 'The \'pip.uninstall\' command did not return the excepted dictionary. Output:\n{}'.format(ret)
  414. )
  415. try:
  416. self.assertEqual(ret['retcode'], 0)
  417. self.assertIn('uninstalled pep8', ret['stdout'])
  418. except KeyError as exc:
  419. self.fail(
  420. 'The returned dictionary is missing an expected key. Error: \'{}\'. Dictionary: {}'.format(
  421. exc,
  422. pprint.pformat(ret)
  423. )
  424. )
  425. def test_pip_install_multiple_editables(self):
  426. editables = [
  427. 'git+https://github.com/jek/blinker.git#egg=Blinker',
  428. 'git+https://github.com/saltstack/salt-testing.git#egg=SaltTesting'
  429. ]
  430. # Create the testing virtualenv
  431. self._create_virtualenv(self.venv_dir)
  432. ret = self.run_function(
  433. 'pip.install', [],
  434. editable='{0}'.format(','.join(editables)),
  435. bin_env=self.venv_dir
  436. )
  437. if not isinstance(ret, dict):
  438. self.fail(
  439. 'The \'pip.install\' command did not return the excepted dictionary. Output:\n{}'.format(ret)
  440. )
  441. try:
  442. if self._check_download_error(ret['stdout']):
  443. self.skipTest('Test skipped due to pip download error')
  444. self.assertEqual(ret['retcode'], 0)
  445. self.assertIn(
  446. 'Successfully installed Blinker SaltTesting', ret['stdout']
  447. )
  448. except KeyError as exc:
  449. self.fail(
  450. 'The returned dictionary is missing an expected key. Error: \'{}\'. Dictionary: {}'.format(
  451. exc,
  452. pprint.pformat(ret)
  453. )
  454. )
  455. def test_pip_install_multiple_editables_and_pkgs(self):
  456. editables = [
  457. 'git+https://github.com/jek/blinker.git#egg=Blinker',
  458. 'git+https://github.com/saltstack/salt-testing.git#egg=SaltTesting'
  459. ]
  460. # Create the testing virtualenv
  461. self._create_virtualenv(self.venv_dir)
  462. ret = self.run_function(
  463. 'pip.install', ['pep8'],
  464. editable='{0}'.format(','.join(editables)),
  465. bin_env=self.venv_dir
  466. )
  467. if not isinstance(ret, dict):
  468. self.fail(
  469. 'The \'pip.install\' command did not return the excepted dictionary. Output:\n{}'.format(ret)
  470. )
  471. try:
  472. if self._check_download_error(ret['stdout']):
  473. self.skipTest('Test skipped due to pip download error')
  474. self.assertEqual(ret['retcode'], 0)
  475. for package in ('Blinker', 'SaltTesting', 'pep8'):
  476. self.assertRegex(
  477. ret['stdout'],
  478. r'(?:.*)(Successfully installed)(?:.*)({0})(?:.*)'.format(package)
  479. )
  480. except KeyError as exc:
  481. self.fail(
  482. 'The returned dictionary is missing an expected key. Error: \'{}\'. Dictionary: {}'.format(
  483. exc,
  484. pprint.pformat(ret)
  485. )
  486. )
  487. @skipIf(not os.path.isfile('pip3'), 'test where pip3 is installed')
  488. @skipIf(salt.utils.platform.is_windows(), 'test specific for linux usage of /bin/python')
  489. def test_system_pip3(self):
  490. self.run_function('pip.install', pkgs=['lazyimport==0.0.1'], bin_env='/bin/pip3')
  491. ret1 = self.run_function('cmd.run', '/bin/pip3 freeze | grep lazyimport')
  492. self.run_function('pip.uninstall', pkgs=['lazyimport'], bin_env='/bin/pip3')
  493. ret2 = self.run_function('cmd.run', '/bin/pip3 freeze | grep lazyimport')
  494. assert 'lazyimport==0.0.1' in ret1
  495. assert ret2 == ''