test_pip.py 22 KB

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